Files
fusion/packages/cli/src/update-cache.ts
gsxdsm 74dacd0758 FN-9188: respect externally managed updates
Managed deployments can now suppress self-update offers while unsupported installs receive actionable guidance.

- Add an install-level environment policy for externally managed updates across CLI, server, and dashboard surfaces.
- Detect missing npm shell failures and replace raw stderr with deployment-aware remediation.
- Document managed-update behavior and add regression coverage for checks, installs, automation, and UI actions.

Files changed:
 .changeset/fn-9188-managed-updates.md              |  7 +++
 docs/architecture.md                               |  6 +--
 docs/cli-reference.md                              |  2 +-
 docs/dashboard-guide.md                            |  2 +-
 docs/settings-reference.md                         |  4 +-
 packages/cli/src/__tests__/update-cache.test.ts    | 11 ++++-
 .../update-command-externally-managed.test.ts      | 39 +++++++++++++++
 packages/cli/src/commands/update.ts                | 42 +++++++++++-----
 packages/cli/src/update-cache.ts                   |  3 +-
 .../core/src/__tests__/update-management.test.ts   | 15 ++++++
 packages/core/src/config/update-management.ts      | 21 ++++++++
 packages/core/src/index.gate.ts                    |  5 ++
 packages/core/src/index.ts                         |  5 ++
 packages/dashboard/app/api/client/health.ts        |  3 ++
 .../dashboard/app/components/SettingsModal.tsx     |  4 ++
 .../__tests__/SettingsModal.general.test.tsx       | 20 ++++++++
 .../__tests__/UpdateAvailableBanner.test.tsx       | 26 ++++++++++
 .../components/__tests__/settings-mobile.test.tsx  | 24 +++++++++-
 .../__tests__/SystemControlsArea.test.tsx          | 20 ++++++++
 .../command-center/areas/SystemControlsArea.tsx    |  6 +++
 .../app/hooks/__tests__/useUpdateCheck.test.ts     |  1 +
 packages/dashboard/app/hooks/useUpdateCheck.ts     |  3 +-
 .../dashboard/src/__tests__/auto-update.test.ts    | 10 ++++
 .../dashboard/src/__tests__/update-check.test.ts   | 56 ++++++++++++++++++++++
 packages/dashboard/src/auto-update.ts              |  9 +++-
 .../__tests__/register-update-check-routes.test.ts | 24 +++++++++-
 .../src/routes/register-update-check-routes.ts     | 47 +++++++++++++++++-
 packages/dashboard/src/update-check.ts             | 50 ++++++++++++++++---
 28 files changed, 433 insertions(+), 32 deletions(-)

Fusion-Task-Id: FN-9188

Fusion-Task-Lineage: 60b114b9-8bf7-434f-9d26-0c954aea45ba

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-21 10:18:45 -07:00

87 lines
2.6 KiB
TypeScript

import { readFileSync } from "node:fs";
import { join } from "node:path";
import { GlobalSettingsStore, resolveGlobalDir, resolveUpdatesExternallyManaged } from "@fusion/core";
import type { UpdateChannel } from "@fusion/core";
type CachedUpdateStatus = {
updateAvailable: boolean;
latestVersion: string;
currentVersion: string;
channel?: UpdateChannel;
};
type UpdateCachePayload = {
updateAvailable?: unknown;
latestVersion?: unknown;
currentVersion?: unknown;
channel?: unknown;
};
export function getCachedUpdateStatus(currentVersion?: string): CachedUpdateStatus | null {
try {
const cachePath = join(resolveGlobalDir(), "update-check.json");
const raw = readFileSync(cachePath, "utf-8");
const parsed = JSON.parse(raw) as UpdateCachePayload;
if (
parsed.updateAvailable === true &&
typeof parsed.latestVersion === "string" &&
parsed.latestVersion.length > 0 &&
typeof parsed.currentVersion === "string" &&
parsed.currentVersion.length > 0
) {
if (
typeof currentVersion === "string" &&
currentVersion.length > 0 &&
parsed.currentVersion !== currentVersion
) {
return null;
}
return {
updateAvailable: true,
latestVersion: parsed.latestVersion,
currentVersion: parsed.currentVersion,
channel: parsed.channel === "beta" ? "beta" : parsed.channel === "stable" ? "stable" : undefined,
};
}
return null;
} catch {
return null;
}
}
export async function isUpdateCheckEnabled(): Promise<boolean> {
if (resolveUpdatesExternallyManaged()) return false;
const store = new GlobalSettingsStore();
await store.init();
const settings = await store.getSettings();
return settings.updateCheckEnabled !== false;
}
/*
FNXC:UpdateChannels 2026-07-19-13:00:
The persisted `updateChannel` global setting selects the release track for every
update surface. `fn update --channel <stable|beta>` both uses AND persists the
choice so the dashboard and desktop follow along. Absent/invalid = "stable".
*/
export async function getConfiguredUpdateChannel(): Promise<UpdateChannel> {
try {
const store = new GlobalSettingsStore();
await store.init();
const settings = await store.getSettings();
return settings.updateChannel === "beta" ? "beta" : "stable";
} catch {
return "stable";
}
}
export async function persistUpdateChannel(channel: UpdateChannel): Promise<void> {
const store = new GlobalSettingsStore();
await store.init();
const settings = await store.getSettings();
if (settings.updateChannel === channel) return;
await store.updateSettings({ updateChannel: channel });
}