Updated the architecture documentation to document the app CPU metric in the system stats section. Fusion-Task-Id: FN-3211
154 lines
5.6 KiB
JavaScript
Executable File
154 lines
5.6 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
// runfusion.ai — tiny alias for @runfusion/fusion.
|
|
//
|
|
// Exposes four bins: runfusion.ai, runfusion, fn, fusion. Installing this
|
|
// package globally (`npm i -g runfusion.ai`) therefore also puts `fn` and
|
|
// `fusion` on PATH, even though `@runfusion/fusion` is only a dependency
|
|
// (npm does not link dep bins globally).
|
|
//
|
|
// When invoked as `runfusion.ai` / `runfusion` with no args, defaults to
|
|
// launching the dashboard. When invoked as `fn` / `fusion`, forwards args
|
|
// verbatim so behavior matches the main CLI exactly (e.g. bare `fn` prints
|
|
// help, not `fn dashboard`).
|
|
|
|
import { basename, join } from "node:path";
|
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
import { homedir } from "node:os";
|
|
import { createRequire } from "node:module";
|
|
|
|
const processRef = globalThis.process;
|
|
const args = processRef.argv.slice(2);
|
|
const invokedAs = basename(processRef.argv[1] || "").replace(/\.(js|cjs|mjs|exe)$/i, "");
|
|
const isAliasInvocation = invokedAs === "runfusion.ai" || invokedAs === "runfusion";
|
|
|
|
if (isAliasInvocation && args.length === 0) {
|
|
// No subcommand → default to dashboard.
|
|
processRef.argv = [processRef.argv[0], processRef.argv[1], "dashboard"];
|
|
}
|
|
|
|
maybeAnnounceUpdateAndRefresh();
|
|
|
|
await import("@runfusion/fusion/dist/bin.js");
|
|
|
|
// ──────────────────────────────────────────────────────────────────────────
|
|
// Update notice & background refresh.
|
|
//
|
|
// Reads the existing `~/.fusion/update-check.json` cache that the dashboard
|
|
// server writes (packages/dashboard/src/update-check.ts). If a newer version
|
|
// is available, prints a one-line stderr notice. To avoid coupling the
|
|
// launcher to the dashboard bundle and to keep it fast, we never import
|
|
// dashboard code here — we just consume the cache file by its known shape.
|
|
//
|
|
// If the cache is missing or older than 24h, fires a non-blocking fetch
|
|
// against the npm registry and rewrites the cache so users who never open
|
|
// the dashboard still pick up updates eventually. Skipped in CI, when
|
|
// stderr isn't a TTY, or when FUSION_NO_UPDATE_CHECK=1 is set.
|
|
function maybeAnnounceUpdateAndRefresh() {
|
|
try {
|
|
if (globalThis.process.env.FUSION_NO_UPDATE_CHECK === "1") return;
|
|
if (globalThis.process.env.CI) return;
|
|
if (!globalThis.process.stderr.isTTY) return;
|
|
|
|
const fusionDir = resolveFusionDir();
|
|
const cachePath = join(fusionDir, "update-check.json");
|
|
const currentVersion = readBundledFusionVersion();
|
|
|
|
let cache = null;
|
|
try {
|
|
cache = JSON.parse(readFileSync(cachePath, "utf-8"));
|
|
} catch { /* missing or corrupt — treat as no cache */ }
|
|
|
|
if (
|
|
cache &&
|
|
cache.updateAvailable === true &&
|
|
typeof cache.latestVersion === "string" &&
|
|
cache.currentVersion === currentVersion
|
|
) {
|
|
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
globalThis.process.stderr.write(
|
|
yellow(
|
|
`\nFusion ${cache.latestVersion} is available (you have ${currentVersion}).\n`,
|
|
) +
|
|
dim(` Update: npx runfusion.ai@latest\n Disable: FUSION_NO_UPDATE_CHECK=1\n\n`),
|
|
);
|
|
}
|
|
|
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
const stale =
|
|
!cache ||
|
|
typeof cache.lastChecked !== "number" ||
|
|
Date.now() - cache.lastChecked > DAY_MS ||
|
|
cache.currentVersion !== currentVersion;
|
|
if (stale && currentVersion) {
|
|
backgroundRefresh(fusionDir, cachePath, currentVersion).catch(() => {});
|
|
}
|
|
} catch {
|
|
// Update check is best-effort — never block or fail the launcher.
|
|
}
|
|
}
|
|
|
|
function resolveFusionDir() {
|
|
const home = globalThis.process.env.HOME || globalThis.process.env.USERPROFILE || homedir();
|
|
const preferred = join(home, ".fusion");
|
|
if (existsSync(preferred)) return preferred;
|
|
const legacy = join(home, ".pi", "fusion");
|
|
if (existsSync(legacy)) return legacy;
|
|
return preferred;
|
|
}
|
|
|
|
function readBundledFusionVersion() {
|
|
try {
|
|
const require = createRequire(import.meta.url);
|
|
const pkgPath = require.resolve("@runfusion/fusion/package.json");
|
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
return typeof pkg.version === "string" ? pkg.version : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function backgroundRefresh(fusionDir, cachePath, currentVersion) {
|
|
const controller = new globalThis.AbortController();
|
|
const timeout = globalThis.setTimeout(() => controller.abort(), 1500);
|
|
try {
|
|
const response = await globalThis.fetch("https://registry.npmjs.org/@runfusion%2Ffusion", {
|
|
signal: controller.signal,
|
|
});
|
|
if (!response.ok) return;
|
|
const payload = await response.json();
|
|
const latestVersion = payload?.["dist-tags"]?.latest;
|
|
if (typeof latestVersion !== "string") return;
|
|
|
|
const result = {
|
|
currentVersion,
|
|
latestVersion,
|
|
updateAvailable: isRemoteNewer(latestVersion, currentVersion),
|
|
lastChecked: Date.now(),
|
|
};
|
|
|
|
try {
|
|
mkdirSync(fusionDir, { recursive: true });
|
|
writeFileSync(cachePath, JSON.stringify(result, null, 2), "utf-8");
|
|
} catch { /* best-effort */ }
|
|
} finally {
|
|
globalThis.clearTimeout(timeout);
|
|
}
|
|
}
|
|
|
|
function isRemoteNewer(remote, current) {
|
|
const parse = (v) =>
|
|
String(v)
|
|
.split(".")
|
|
.slice(0, 3)
|
|
.map((p) => Number.parseInt(p, 10))
|
|
.map((n) => (Number.isFinite(n) ? n : 0));
|
|
const r = parse(remote);
|
|
const c = parse(current);
|
|
for (let i = 0; i < 3; i++) {
|
|
if ((r[i] ?? 0) > (c[i] ?? 0)) return true;
|
|
if ((r[i] ?? 0) < (c[i] ?? 0)) return false;
|
|
}
|
|
return false;
|
|
}
|