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>
73 lines
2.4 KiB
TypeScript
73 lines
2.4 KiB
TypeScript
import { useCallback, useEffect, useState } from "react";
|
|
import { checkForUpdate } from "../api";
|
|
import type { UpdateInstallResponse } from "../api";
|
|
import { pendingUpdateInstallState, usePendingUpdateInstall } from "./usePendingUpdateInstall";
|
|
|
|
const UPDATE_BANNER_DISMISSED_KEY = "kb-update-banner-dismissed";
|
|
|
|
export interface UseUpdateCheckResult {
|
|
updateAvailable: boolean;
|
|
latestVersion: string | null;
|
|
currentVersion: string | null;
|
|
loading: boolean;
|
|
dismissed: boolean;
|
|
pendingInstall?: UpdateInstallResponse;
|
|
dismiss: () => void;
|
|
}
|
|
|
|
export function useUpdateCheck(): UseUpdateCheckResult {
|
|
const pendingInstall = usePendingUpdateInstall({ hydrate: false });
|
|
const [loading, setLoading] = useState(true);
|
|
const [updateAvailable, setUpdateAvailable] = useState(false);
|
|
const [latestVersion, setLatestVersion] = useState<string | null>(null);
|
|
const [currentVersion, setCurrentVersion] = useState<string | null>(null);
|
|
const [dismissed, setDismissed] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const isDismissed = sessionStorage.getItem(UPDATE_BANNER_DISMISSED_KEY) === "true";
|
|
setDismissed(isDismissed);
|
|
|
|
let cancelled = false;
|
|
|
|
void checkForUpdate()
|
|
.then((result) => {
|
|
// Record before ordinary update state so a hydrated pending restart never
|
|
// flashes a second Update now action through a competing stale response.
|
|
pendingUpdateInstallState.record(result.pendingInstall);
|
|
// Externally managed deployments are disabled by the server and must never mount an update offer.
|
|
if (cancelled || result.disabled || result.externallyManaged) return;
|
|
|
|
setUpdateAvailable(result.updateAvailable === true);
|
|
setLatestVersion(typeof result.latestVersion === "string" ? result.latestVersion : null);
|
|
setCurrentVersion(typeof result.currentVersion === "string" ? result.currentVersion : null);
|
|
})
|
|
.catch(() => {
|
|
// Fail silently. Update checks are best-effort.
|
|
})
|
|
.finally(() => {
|
|
if (!cancelled) {
|
|
setLoading(false);
|
|
}
|
|
});
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, []);
|
|
|
|
const dismiss = useCallback(() => {
|
|
setDismissed(true);
|
|
sessionStorage.setItem(UPDATE_BANNER_DISMISSED_KEY, "true");
|
|
}, []);
|
|
|
|
return {
|
|
updateAvailable,
|
|
latestVersion,
|
|
currentVersion,
|
|
loading,
|
|
dismissed,
|
|
pendingInstall,
|
|
dismiss,
|
|
};
|
|
}
|