import { useCallback, useEffect, useState } from "react"; import { fetchFnBinaryStatus, installFnBinary, type FnBinaryInstallResult, type FnBinaryStatus, } from "../api/legacy"; import "./CliBinaryPanel.css"; interface Props { /** * When true, the panel is mounted but should not auto-fetch on render. * Used by the first-launch banner so it can show a button without * forcing a probe before the user opts in. */ defer?: boolean; } const STATE_LABELS: Record = { installed: { text: "Installed", tone: "ok" }, missing: { text: "Not installed", tone: "err" }, "version-mismatch": { text: "Version mismatch", tone: "warn" }, skipped: { text: "Check disabled", tone: "warn" }, }; /** * Settings panel for the `fn` / `fusion` global CLI binary. * * Shows current install state, a one-click install button (runs * `npm install -g runfusion.ai` server-side), and two copy-to-clipboard * commands so users with non-default npm setups can install themselves. */ export function CliBinaryPanel({ defer = false }: Props) { const [status, setStatus] = useState(null); const [loading, setLoading] = useState(false); const [installing, setInstalling] = useState(false); const [installResult, setInstallResult] = useState(null); const [error, setError] = useState(null); const [copied, setCopied] = useState(null); const refresh = useCallback(async () => { setLoading(true); setError(null); try { const next = await fetchFnBinaryStatus(); setStatus(next); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setLoading(false); } }, []); useEffect(() => { if (defer) return; void refresh(); }, [defer, refresh]); const onInstall = useCallback(async () => { setInstalling(true); setInstallResult(null); setError(null); try { const response = await installFnBinary(); setStatus({ binary: response.binary, expectedVersion: response.expectedVersion, state: response.state, install: response.install, }); setInstallResult(response.installResult); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setInstalling(false); } }, []); const copy = useCallback(async (label: string, value: string) => { try { await navigator.clipboard.writeText(value); setCopied(label); setTimeout(() => setCopied((c) => (c === label ? null : c)), 1500); } catch { // Clipboard API unavailable — leave button silent rather than throwing. } }, []); const stateMeta = status ? STATE_LABELS[status.state] : null; return (

CLI Binary

{stateMeta && ( {stateMeta.text} )}
Installing the global CLI lets you run fn and fusion from any terminal. Automations and scripts work without it via npx, but a global install is faster and more convenient. {loading && !status &&

Checking…

} {status && (
{status.binary.installed ? (
  • Binary: {status.binary.binary}
  • {status.binary.path && (
  • Path: {status.binary.path}
  • )}
  • Version: {status.binary.version ?? "unknown"} (expected {status.expectedVersion})
) : (

Neither fn nor fusion was found on PATH.

)}
{[ { label: "npm", command: status.install.npm }, { label: "curl", command: status.install.curl }, ].map(({ label, command }) => (
{command}
))}
)} {installResult && (
{installResult.success ? `Install succeeded in ${(installResult.durationMs / 1000).toFixed(1)}s` : `Install failed (exit ${installResult.exitCode ?? "n/a"})`} {installResult.permissionsHint && (

{installResult.permissionsHint}

)} {installResult.stdout && (
{installResult.stdout}
)} {installResult.stderr && (
              {installResult.stderr}
            
)}
)} {error &&

{error}

}
); }