feat(FN-XXXX): add fnBinaryCheckEnabled global setting
Defaults to true so existing behaviour (dashboard probes PATH for `fn` / `fusion` and surfaces install / version-mismatch states) is unchanged. When the user toggles it off in Settings → General → CLI Binary: - `GET /system/fn-binary/status` short-circuits before `detectFnBinary()` and returns `state: "skipped"`. No `<bin> --version` subprocess is spawned at all, so an outdated globally-installed CLI cannot run as a side effect of opening the dashboard. - The install banner is hidden when `state` is `"skipped"`. - `POST /system/fn-binary/install` rejects with HTTP 409 since install is the user action that the status check informs. - The Settings UI gains a checkbox under the existing CLI binary panel. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
5
.changeset/fn-binary-check-setting.md
Normal file
5
.changeset/fn-binary-check-setting.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Add a global `fnBinaryCheckEnabled` setting that lets users opt out of the dashboard's `fn`/`fusion` CLI binary probe. Default remains true (probe runs as before). When set to false, `GET /system/fn-binary/status` returns `state: "skipped"` without spawning a subprocess, the install banner stays hidden, and `POST /system/fn-binary/install` rejects with HTTP 409. Useful when the running dev process is the source of truth and shelling out to whichever globally-installed `runfusion.ai` happens to be on PATH is unwanted.
|
||||
@@ -37,6 +37,7 @@ export const DEFAULT_GLOBAL_SETTINGS = {
|
||||
favoriteModels: undefined,
|
||||
openrouterModelSync: true,
|
||||
updateCheckEnabled: true,
|
||||
fnBinaryCheckEnabled: true,
|
||||
updateCheckFrequency: "daily",
|
||||
showGitHubStarButton: true,
|
||||
modelOnboardingComplete: undefined,
|
||||
|
||||
@@ -1319,6 +1319,13 @@ export interface GlobalSettings {
|
||||
* shows update notices in the CLI and dashboard. The actual cadence is
|
||||
* governed by `updateCheckFrequency`. Disabled = no automatic checks at all. */
|
||||
updateCheckEnabled?: boolean;
|
||||
/** When true (default), the dashboard probes PATH for a globally-installed
|
||||
* `fn`/`fusion` CLI binary so it can advertise install/upgrade actions in
|
||||
* the UI. The probe spawns `<bin> --version`, which executes whichever
|
||||
* `runfusion.ai` is on PATH. Set to false to skip the probe entirely —
|
||||
* useful when the local dev process is the source of truth and shelling
|
||||
* out to an outdated globally-installed binary is unwanted. */
|
||||
fnBinaryCheckEnabled?: boolean;
|
||||
/** When false, hides the "Star on GitHub" button in the Settings modal
|
||||
* header. Defaults to true (visible). The button is also hidden once the
|
||||
* user has clicked it (tracked client-side in localStorage). */
|
||||
|
||||
@@ -1309,7 +1309,7 @@ export interface FnBinaryStatus {
|
||||
invocation: string;
|
||||
};
|
||||
expectedVersion: string;
|
||||
state: "installed" | "missing" | "version-mismatch";
|
||||
state: "installed" | "missing" | "version-mismatch" | "skipped";
|
||||
install: { npm: string; curl: string; package: string };
|
||||
}
|
||||
|
||||
|
||||
@@ -99,6 +99,9 @@ export function CliBinaryInstallBanner({ onOpenSettings }: Props) {
|
||||
if (dismissed) return null;
|
||||
if (!status) return null;
|
||||
if (status.state === "installed") return null;
|
||||
// Honour the global `fnBinaryCheckEnabled` opt-out — when checks are
|
||||
// disabled the install banner would be misleading.
|
||||
if (status.state === "skipped") return null;
|
||||
|
||||
return (
|
||||
<div className="cli-binary-banner" role="status">
|
||||
|
||||
@@ -20,6 +20,7 @@ const STATE_LABELS: Record<FnBinaryStatus["state"], { text: string; tone: "ok" |
|
||||
installed: { text: "Installed", tone: "ok" },
|
||||
missing: { text: "Not installed", tone: "err" },
|
||||
"version-mismatch": { text: "Version mismatch", tone: "warn" },
|
||||
skipped: { text: "Check disabled", tone: "warn" },
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1846,6 +1846,26 @@ export function SettingsModal({
|
||||
</small>
|
||||
</div>
|
||||
<CliBinaryPanel />
|
||||
<div className="form-group">
|
||||
<label htmlFor="fnBinaryCheckEnabled" className="checkbox-label">
|
||||
<input
|
||||
id="fnBinaryCheckEnabled"
|
||||
type="checkbox"
|
||||
checked={form.fnBinaryCheckEnabled !== false}
|
||||
onChange={(e) =>
|
||||
setForm((f) => ({ ...f, fnBinaryCheckEnabled: e.target.checked }))
|
||||
}
|
||||
/>
|
||||
Check for the <code>fn</code> CLI binary on PATH
|
||||
</label>
|
||||
<small>
|
||||
When enabled, the dashboard probes for a globally-installed{" "}
|
||||
<code>fn</code> / <code>fusion</code> CLI by spawning{" "}
|
||||
<code><bin> --version</code>. Disable this if your local
|
||||
dev process is the source of truth and you don't want any
|
||||
outdated globally-installed binary executed during the probe.
|
||||
</small>
|
||||
</div>
|
||||
<h4 className="settings-section-heading settings-section-heading--spaced">Updates</h4>
|
||||
<div className="form-group">
|
||||
<label htmlFor="updateCheckEnabled" className="checkbox-label">
|
||||
|
||||
@@ -129,8 +129,44 @@ function runNpmInstall(): Promise<InstallResult> {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a status payload for the case where the user has disabled the
|
||||
* fn-binary check via the global setting. We still return a well-formed
|
||||
* response (the UI consumes the same shape regardless) but skip the
|
||||
* subprocess probe entirely. `state: "skipped"` lets the dashboard hide
|
||||
* install / version-mismatch surfaces without inferring "missing".
|
||||
*/
|
||||
function buildSkippedStatusPayload(expectedVersion: string) {
|
||||
return {
|
||||
binary: {
|
||||
installed: false,
|
||||
invocation: FN_INSTALL_NPM,
|
||||
} satisfies FnBinaryStatus,
|
||||
expectedVersion,
|
||||
state: "skipped" as const,
|
||||
install: {
|
||||
npm: FN_INSTALL_NPM,
|
||||
curl: FN_INSTALL_CURL,
|
||||
package: FN_NPM_PACKAGE,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const registerFnBinaryRoutes: ApiRouteRegistrar = (ctx) => {
|
||||
const { router, rethrowAsApiError } = ctx;
|
||||
const { router, rethrowAsApiError, store } = ctx;
|
||||
|
||||
async function isCheckEnabled(): Promise<boolean> {
|
||||
try {
|
||||
const settings = await store.getSettings();
|
||||
// Default true — only treat an explicit false as opt-out.
|
||||
return settings.fnBinaryCheckEnabled !== false;
|
||||
} catch {
|
||||
// If settings can't be read, fall back to the safe default and
|
||||
// perform the probe so the dashboard's onboarding banner still
|
||||
// renders.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /system/fn-binary/status
|
||||
@@ -138,11 +174,18 @@ export const registerFnBinaryRoutes: ApiRouteRegistrar = (ctx) => {
|
||||
* Probes PATH for `fn` then `fusion`, returning install state and the
|
||||
* canonical install commands. No auth — this is read-only introspection
|
||||
* the dashboard banner needs before the user signs in.
|
||||
*
|
||||
* Honours `fnBinaryCheckEnabled` (global setting, default true). When
|
||||
* disabled the route returns `state: "skipped"` without spawning a probe.
|
||||
*/
|
||||
router.get("/system/fn-binary/status", async (_req, res) => {
|
||||
try {
|
||||
const binary = await detectFnBinary();
|
||||
const expectedVersion = getCliPackageVersion();
|
||||
if (!(await isCheckEnabled())) {
|
||||
res.json(buildSkippedStatusPayload(expectedVersion));
|
||||
return;
|
||||
}
|
||||
const binary = await detectFnBinary();
|
||||
res.json(buildStatusPayload(binary, expectedVersion));
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError) throw err;
|
||||
@@ -155,9 +198,18 @@ export const registerFnBinaryRoutes: ApiRouteRegistrar = (ctx) => {
|
||||
*
|
||||
* Runs `npm install -g runfusion.ai`. Returns the install result and the
|
||||
* post-install probe so the UI can refresh its state in one round trip.
|
||||
* Disabled when `fnBinaryCheckEnabled` is false — install is the user
|
||||
* action that the status check informs, so it follows the same gate.
|
||||
*/
|
||||
router.post("/system/fn-binary/install", async (_req, res) => {
|
||||
try {
|
||||
if (!(await isCheckEnabled())) {
|
||||
throw new ApiError(
|
||||
409,
|
||||
"fn-binary checks are disabled in global settings (fnBinaryCheckEnabled=false). Re-enable them to install via the dashboard.",
|
||||
{ code: "FN_BINARY_CHECK_DISABLED" },
|
||||
);
|
||||
}
|
||||
const installResult = await runNpmInstall();
|
||||
// Re-probe even on failure — the binary may already exist from a
|
||||
// previous attempt and we want the UI to reflect reality.
|
||||
|
||||
Reference in New Issue
Block a user