// Commander registration for foreground node host and node service lifecycle commands. import { Option, type Command } from "commander"; import { formatDocsLink } from "../../../packages/terminal-core/src/links.js"; import { theme } from "../../../packages/terminal-core/src/theme.js"; import { loadNodeHostConfig } from "../../node-host/config.js"; import { runNodeHost } from "../../node-host/runner.js"; import { runNodeHostWorker } from "../../node-host/worker.js"; import { defaultRuntime } from "../../runtime.js"; import { inheritOptionFromParent } from "../command-options.js"; import { formatInvalidPortOption } from "../error-format.js"; import { formatHelpExamples } from "../help-format.js"; import { addNodeCommandOptions } from "./command-options.js"; import { runNodeDaemonInstall, runNodeDaemonRestart, runNodeDaemonStart, runNodeDaemonStatus, runNodeDaemonStop, runNodeDaemonUninstall, } from "./daemon.js"; import { resolveNodeGatewayOptions, resolveNodePairGatewayOptions } from "./gateway-options.js"; import { runNodeIdentityShow } from "./identity.js"; export function registerNodeCli(program: Command) { const node = addNodeCommandOptions( program.command("node").description("Run and manage the headless node host service"), ).addHelpText( "after", () => `\n${theme.heading("Examples:")}\n${formatHelpExamples([ ["openclaw node run --host 127.0.0.1 --port 18789", "Run the node host in the foreground."], ["openclaw node status", "Check node host service status."], ["openclaw node install", "Install the node host service."], ["openclaw node start", "Start the installed node host service."], ["openclaw node restart", "Restart the installed node host service."], ])}\n\n${theme.muted("Docs:")} ${formatDocsLink("/cli/node", "docs.openclaw.ai/cli/node")}\n`, ); node .command("worker", { hidden: true }) .description("Run the private macOS app node-host worker") .action(async () => { await runNodeHostWorker(); }); addNodeCommandOptions(node.command("run").description("Run the headless node host (foreground)")) .option( "--pair ", "Pair with a setup code or oc-pair URL; explicit gateway flags take precedence", ) .option("--host ", "Gateway host") .option("--port ", "Gateway port") .option("--context-path ", "Gateway WebSocket context path (e.g. /openclaw-gw)") .option("--tls", "Use TLS for the gateway connection") .option("--no-tls", "Disable TLS for the gateway connection") .option("--tls-fingerprint ", "Expected TLS certificate fingerprint (sha256)") .option("--node-id ", "Override the generated node instance id") .option("--display-name ", "Override node display name") .addOption(new Option("--ephemeral").hideHelp()) .option("--share-installed-apps", "Share installed macOS applications with the Gateway") .option("--no-share-installed-apps", "Disable installed application sharing") .action(async (opts, command: Command) => { let pair; let gatewayOptions; try { pair = opts.pair ? resolveNodePairGatewayOptions(opts.pair) : undefined; const existing = await loadNodeHostConfig(); gatewayOptions = resolveNodeGatewayOptions(opts, existing, pair); } catch (error) { defaultRuntime.error(error instanceof Error ? error.message : String(error)); defaultRuntime.exit(1); return; } const { host, port, contextPath, tls, tlsFingerprint, cloudflareAccess, gatewayCandidates } = gatewayOptions; if (port === null) { defaultRuntime.error(formatInvalidPortOption("--port")); defaultRuntime.exit(1); return; } if (opts.tls === false && opts.tlsFingerprint !== undefined) { defaultRuntime.error("--no-tls cannot be combined with --tls-fingerprint"); defaultRuntime.exit(1); return; } await runNodeHost({ gatewayHost: host, gatewayPort: port, gatewayTls: tls, gatewayTlsFingerprint: tlsFingerprint, gatewayContextPath: contextPath, gatewayCloudflareAccess: cloudflareAccess, gatewayCandidates, gatewayBootstrapToken: pair?.bootstrapToken, preferGatewayBootstrapToken: pair !== undefined, ...(opts.ephemeral === true ? { forceWorkerRuns: true, ephemeral: true } : {}), nodeId: opts.nodeId, displayName: opts.displayName, installedAppsSharing: opts.shareInstalledApps, commands: opts.commands ?? inheritOptionFromParent(command, "commands"), allCommands: opts.allCommands ?? inheritOptionFromParent(command, "allCommands"), }); }); node .command("status") .description("Show node host status") .option("--json", "Output JSON", false) .action(async (opts) => { await runNodeDaemonStatus(opts); }); node .command("identity") .description("Print the node host device identity (device id + public key)") .option("--json", "Output JSON", false) .action((opts) => { runNodeIdentityShow(opts); }); addNodeCommandOptions( node.command("install").description("Install the node host service (launchd/systemd/schtasks)"), ) .option("--host ", "Gateway host") .option("--port ", "Gateway port") .option("--context-path ", "Gateway WebSocket context path (e.g. /openclaw-gw)") .option("--tls", "Use TLS for the gateway connection") .option("--no-tls", "Disable TLS for the gateway connection") .option("--tls-fingerprint ", "Expected TLS certificate fingerprint (sha256)") .option("--node-id ", "Override the generated node instance id") .option("--display-name ", "Override node display name") .option("--share-installed-apps", "Share installed macOS applications with the Gateway") .option("--no-share-installed-apps", "Disable installed application sharing") .option("--runtime ", "Service runtime (node|bun). Default: node") .option("--force", "Reinstall/overwrite if already installed", false) .option("--json", "Output JSON", false) .action(async (opts, command: Command) => { await runNodeDaemonInstall({ ...opts, commands: opts.commands ?? inheritOptionFromParent(command, "commands"), allCommands: opts.allCommands ?? inheritOptionFromParent(command, "allCommands"), }); }); for (const [name, action] of [ ["uninstall", runNodeDaemonUninstall], ["stop", runNodeDaemonStop], ["start", runNodeDaemonStart], ["restart", runNodeDaemonRestart], ] as const) { node .command(name) .description( `${name.charAt(0).toUpperCase()}${name.slice(1)} the node host service (launchd/systemd/schtasks)`, ) .option("--json", "Output JSON", false) .action(async (opts) => { await action(opts); }); } }