diff --git a/packages/app/src/desktop/components/desktop-updates-section.tsx b/packages/app/src/desktop/components/desktop-updates-section.tsx index 86b385077..01fef73e9 100644 --- a/packages/app/src/desktop/components/desktop-updates-section.tsx +++ b/packages/app/src/desktop/components/desktop-updates-section.tsx @@ -14,6 +14,7 @@ import { Copy, FileText, Smartphone, + Activity, } from "lucide-react-native"; import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet"; import { Button } from "@/components/ui/button"; @@ -22,6 +23,7 @@ import { confirmDialog } from "@/utils/confirm-dialog"; import { openExternalUrl } from "@/utils/open-external-url"; import { getLocalDaemonVersion, isVersionMismatch } from "@/desktop/updates/desktop-updates"; import { + getCliDaemonStatus, getCliSymlinkInstructions, getDesktopDaemonLogs, getDesktopDaemonPairing, @@ -62,6 +64,9 @@ export function LocalDaemonSection({ appVersion, showLifecycleControls }: LocalD const [cliSymlinkInstructions, setCliSymlinkInstructions] = useState(null); const [pairingStatusMessage, setPairingStatusMessage] = useState(null); + const [cliStatusOutput, setCliStatusOutput] = useState(null); + const [isCliStatusModalOpen, setIsCliStatusModalOpen] = useState(false); + const [isLoadingCliStatus, setIsLoadingCliStatus] = useState(false); const loadDaemonData = useCallback(() => { if (!showSection) { @@ -315,6 +320,33 @@ export function LocalDaemonSection({ appVersion, showLifecycleControls }: LocalD }); }, [pairingOffer?.url]); + const handleOpenCliStatus = useCallback(async () => { + setIsLoadingCliStatus(true); + try { + setCliStatusOutput(await getCliDaemonStatus()); + setIsCliStatusModalOpen(true); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + setCliStatusOutput(`Failed to fetch daemon status: ${message}`); + setIsCliStatusModalOpen(true); + } finally { + setIsLoadingCliStatus(false); + } + }, []); + + const handleCopyCliStatus = useCallback(() => { + if (!cliStatusOutput) { + return; + } + void Clipboard.setStringAsync(cliStatusOutput) + .then(() => { + Alert.alert("Copied", "Status copied to clipboard."); + }) + .catch((error) => { + console.error("[Settings] Failed to copy daemon status", error); + }); + }, [cliStatusOutput]); + if (!showSection) { return null; } @@ -458,6 +490,23 @@ export function LocalDaemonSection({ appVersion, showLifecycleControls }: LocalD Pair device + + + Full status + + Runs `paseo daemon status` and shows the output. + + + + {daemonVersionMismatch ? ( @@ -524,6 +573,28 @@ export function LocalDaemonSection({ appVersion, showLifecycleControls }: LocalD + + setIsCliStatusModalOpen(false)} + title="Daemon status" + testID="daemon-cli-status-dialog" + snapPoints={["60%", "85%"]} + > + + + {cliStatusOutput ?? ""} + + + + + + + ); } diff --git a/packages/app/src/desktop/daemon/desktop-daemon.ts b/packages/app/src/desktop/daemon/desktop-daemon.ts index 5dbca01d8..a88beca23 100644 --- a/packages/app/src/desktop/daemon/desktop-daemon.ts +++ b/packages/app/src/desktop/daemon/desktop-daemon.ts @@ -162,6 +162,14 @@ export async function getCliSymlinkInstructions(): Promise { + const raw = await invokeDesktopCommand("cli_daemon_status"); + if (typeof raw !== "string") { + throw new Error("Unexpected CLI daemon status response."); + } + return raw; +} + export type LocalTransportEventUnlisten = () => void; export type LocalTransportEventHandler = (payload: LocalTransportEventPayload) => void; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index e138e1435..c50882984 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -4,4 +4,4 @@ const program = createCli(); if (process.argv.length <= 2) { process.argv.push("onboard"); } -program.parse(); +program.parse(process.argv, { from: "node" }); diff --git a/packages/desktop/src/daemon/daemon-manager.ts b/packages/desktop/src/daemon/daemon-manager.ts index 4b8cb18dd..056ab5f4a 100644 --- a/packages/desktop/src/daemon/daemon-manager.ts +++ b/packages/desktop/src/daemon/daemon-manager.ts @@ -21,6 +21,7 @@ import { createNodeEntrypointInvocation, resolveDaemonRunnerEntrypoint, runCliJsonCommand, + runCliTextCommand, } from "./runtime-paths.js"; const DAEMON_LOG_FILENAME = "daemon.log"; @@ -398,6 +399,10 @@ function getDaemonLogs(): DesktopDaemonLogs { }; } +function getCliDaemonStatus(): string { + return runCliTextCommand(["daemon", "status"]); +} + async function getDaemonPairing(): Promise { const status = resolveStatus(); if (status.status !== "running") { @@ -521,6 +526,7 @@ export function createDaemonCommandHandlers(): Record getDaemonLogs(), desktop_daemon_pairing: () => getDaemonPairing(), cli_symlink_instructions: () => getCliSymlinkInstructions(), + cli_daemon_status: () => getCliDaemonStatus(), write_attachment_base64: (args) => writeAttachmentBase64(args ?? {}), copy_attachment_file: (args) => copyAttachmentFileToManagedStorage(args ?? {}), read_file_base64: (args) => readManagedFileBase64(args ?? {}), diff --git a/packages/desktop/src/daemon/runtime-paths.ts b/packages/desktop/src/daemon/runtime-paths.ts index e630bca06..e56f47bc2 100644 --- a/packages/desktop/src/daemon/runtime-paths.ts +++ b/packages/desktop/src/daemon/runtime-paths.ts @@ -246,6 +246,26 @@ export function runCliPassthroughCommand(args: string[]): number { return result.signal ? 1 : 0; } +export function runCliTextCommand(args: string[]): string { + const invocation = createCliInvocation(args); + const result = spawnSync(invocation.command, invocation.args, { + env: invocation.env, + encoding: "utf-8", + stdio: ["ignore", "pipe", "pipe"], + }); + + if (result.error) { + throw result.error; + } + + if (result.status !== 0) { + const stderr = typeof result.stderr === "string" ? result.stderr.trim() : ""; + throw new Error(stderr.length > 0 ? stderr : `CLI command failed with exit code ${result.status}`); + } + + return typeof result.stdout === "string" ? result.stdout.trimEnd() : ""; +} + export function runCliJsonCommand(args: string[]): unknown { const invocation = createCliInvocation(args); const result = spawnSync(invocation.command, invocation.args, {