feat: add exec approvals tooling and service status
This commit is contained in:
87
src/cli/exec-approvals-cli.test.ts
Normal file
87
src/cli/exec-approvals-cli.test.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { Command } from "commander";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const callGatewayFromCli = vi.fn(
|
||||
async (method: string, _opts: unknown, params?: unknown) => {
|
||||
if (method.endsWith(".get")) {
|
||||
return {
|
||||
path: "/tmp/exec-approvals.json",
|
||||
exists: true,
|
||||
hash: "hash-1",
|
||||
file: { version: 1, agents: {} },
|
||||
};
|
||||
}
|
||||
return { method, params };
|
||||
},
|
||||
);
|
||||
|
||||
const runtimeLogs: string[] = [];
|
||||
const runtimeErrors: string[] = [];
|
||||
const defaultRuntime = {
|
||||
log: (msg: string) => runtimeLogs.push(msg),
|
||||
error: (msg: string) => runtimeErrors.push(msg),
|
||||
exit: (code: number) => {
|
||||
throw new Error(`__exit__:${code}`);
|
||||
},
|
||||
};
|
||||
|
||||
vi.mock("./gateway-rpc.js", () => ({
|
||||
callGatewayFromCli: (method: string, opts: unknown, params?: unknown) =>
|
||||
callGatewayFromCli(method, opts, params),
|
||||
}));
|
||||
|
||||
vi.mock("./nodes-cli/rpc.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("./nodes-cli/rpc.js")>(
|
||||
"./nodes-cli/rpc.js",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
resolveNodeId: vi.fn(async () => "node-1"),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../runtime.js", () => ({
|
||||
defaultRuntime,
|
||||
}));
|
||||
|
||||
describe("exec approvals CLI", () => {
|
||||
it("loads gateway approvals by default", async () => {
|
||||
runtimeLogs.length = 0;
|
||||
runtimeErrors.length = 0;
|
||||
callGatewayFromCli.mockClear();
|
||||
|
||||
const { registerExecApprovalsCli } = await import("./exec-approvals-cli.js");
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
registerExecApprovalsCli(program);
|
||||
|
||||
await program.parseAsync(["approvals", "get"], { from: "user" });
|
||||
|
||||
expect(callGatewayFromCli).toHaveBeenCalledWith(
|
||||
"exec.approvals.get",
|
||||
expect.anything(),
|
||||
{},
|
||||
);
|
||||
expect(runtimeErrors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("loads node approvals when --node is set", async () => {
|
||||
runtimeLogs.length = 0;
|
||||
runtimeErrors.length = 0;
|
||||
callGatewayFromCli.mockClear();
|
||||
|
||||
const { registerExecApprovalsCli } = await import("./exec-approvals-cli.js");
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
registerExecApprovalsCli(program);
|
||||
|
||||
await program.parseAsync(["approvals", "get", "--node", "macbook"], { from: "user" });
|
||||
|
||||
expect(callGatewayFromCli).toHaveBeenCalledWith(
|
||||
"exec.approvals.node.get",
|
||||
expect.anything(),
|
||||
{ nodeId: "node-1" },
|
||||
);
|
||||
expect(runtimeErrors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
243
src/cli/exec-approvals-cli.ts
Normal file
243
src/cli/exec-approvals-cli.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
import fs from "node:fs/promises";
|
||||
import JSON5 from "json5";
|
||||
import type { Command } from "commander";
|
||||
|
||||
import type { ExecApprovalsAgent, ExecApprovalsFile } from "../infra/exec-approvals.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { formatDocsLink } from "../terminal/links.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
import { callGatewayFromCli } from "./gateway-rpc.js";
|
||||
import { nodesCallOpts, resolveNodeId } from "./nodes-cli/rpc.js";
|
||||
import type { NodesRpcOpts } from "./nodes-cli/types.js";
|
||||
|
||||
type ExecApprovalsSnapshot = {
|
||||
path: string;
|
||||
exists: boolean;
|
||||
hash: string;
|
||||
file: ExecApprovalsFile;
|
||||
};
|
||||
|
||||
type ExecApprovalsCliOpts = NodesRpcOpts & {
|
||||
node?: string;
|
||||
file?: string;
|
||||
stdin?: boolean;
|
||||
agent?: string;
|
||||
};
|
||||
|
||||
async function readStdin(): Promise<string> {
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of process.stdin) {
|
||||
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
||||
}
|
||||
return Buffer.concat(chunks).toString("utf8");
|
||||
}
|
||||
|
||||
async function resolveTargetNodeId(opts: ExecApprovalsCliOpts): Promise<string | null> {
|
||||
const raw = opts.node?.trim() ?? "";
|
||||
if (!raw) return null;
|
||||
return await resolveNodeId(opts as NodesRpcOpts, raw);
|
||||
}
|
||||
|
||||
async function loadSnapshot(
|
||||
opts: ExecApprovalsCliOpts,
|
||||
nodeId: string | null,
|
||||
): Promise<ExecApprovalsSnapshot> {
|
||||
const method = nodeId ? "exec.approvals.node.get" : "exec.approvals.get";
|
||||
const params = nodeId ? { nodeId } : {};
|
||||
const snapshot = (await callGatewayFromCli(method, opts, params)) as ExecApprovalsSnapshot;
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
async function saveSnapshot(
|
||||
opts: ExecApprovalsCliOpts,
|
||||
nodeId: string | null,
|
||||
file: ExecApprovalsFile,
|
||||
baseHash: string,
|
||||
): Promise<ExecApprovalsSnapshot> {
|
||||
const method = nodeId ? "exec.approvals.node.set" : "exec.approvals.set";
|
||||
const params = nodeId ? { nodeId, file, baseHash } : { file, baseHash };
|
||||
const snapshot = (await callGatewayFromCli(method, opts, params)) as ExecApprovalsSnapshot;
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
function resolveAgentKey(value?: string | null): string {
|
||||
const trimmed = value?.trim() ?? "";
|
||||
return trimmed ? trimmed : "default";
|
||||
}
|
||||
|
||||
function normalizeAllowlistEntry(entry: { pattern?: string } | null): string | null {
|
||||
const pattern = entry?.pattern?.trim() ?? "";
|
||||
return pattern ? pattern : null;
|
||||
}
|
||||
|
||||
function ensureAgent(file: ExecApprovalsFile, agentKey: string): ExecApprovalsAgent {
|
||||
const agents = file.agents ?? {};
|
||||
const entry = agents[agentKey] ?? {};
|
||||
file.agents = agents;
|
||||
return entry;
|
||||
}
|
||||
|
||||
function isEmptyAgent(agent: ExecApprovalsAgent): boolean {
|
||||
const allowlist = Array.isArray(agent.allowlist) ? agent.allowlist : [];
|
||||
return (
|
||||
!agent.security &&
|
||||
!agent.ask &&
|
||||
!agent.askFallback &&
|
||||
agent.autoAllowSkills === undefined &&
|
||||
allowlist.length === 0
|
||||
);
|
||||
}
|
||||
|
||||
export function registerExecApprovalsCli(program: Command) {
|
||||
const approvals = program
|
||||
.command("approvals")
|
||||
.alias("exec-approvals")
|
||||
.description("Manage exec approvals (gateway or node host)")
|
||||
.addHelpText(
|
||||
"after",
|
||||
() =>
|
||||
`\n${theme.muted("Docs:")} ${formatDocsLink("/cli/approvals", "docs.clawd.bot/cli/approvals")}\n`,
|
||||
);
|
||||
|
||||
const getCmd = approvals
|
||||
.command("get")
|
||||
.description("Fetch exec approvals snapshot")
|
||||
.option("--node <node>", "Target node id/name/IP (defaults to gateway)")
|
||||
.action(async (opts: ExecApprovalsCliOpts) => {
|
||||
const nodeId = await resolveTargetNodeId(opts);
|
||||
const snapshot = await loadSnapshot(opts, nodeId);
|
||||
const payload = opts.json ? JSON.stringify(snapshot) : JSON.stringify(snapshot, null, 2);
|
||||
defaultRuntime.log(payload);
|
||||
});
|
||||
nodesCallOpts(getCmd);
|
||||
|
||||
const setCmd = approvals
|
||||
.command("set")
|
||||
.description("Replace exec approvals with a JSON file")
|
||||
.option("--node <node>", "Target node id/name/IP (defaults to gateway)")
|
||||
.option("--file <path>", "Path to JSON file to upload")
|
||||
.option("--stdin", "Read JSON from stdin", false)
|
||||
.action(async (opts: ExecApprovalsCliOpts) => {
|
||||
if (!opts.file && !opts.stdin) {
|
||||
defaultRuntime.error("Provide --file or --stdin.");
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
}
|
||||
if (opts.file && opts.stdin) {
|
||||
defaultRuntime.error("Use either --file or --stdin (not both).");
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
}
|
||||
const nodeId = await resolveTargetNodeId(opts);
|
||||
const snapshot = await loadSnapshot(opts, nodeId);
|
||||
if (!snapshot.hash) {
|
||||
defaultRuntime.error("Exec approvals hash missing; reload and retry.");
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
}
|
||||
const raw = opts.stdin ? await readStdin() : await fs.readFile(String(opts.file), "utf8");
|
||||
let file: ExecApprovalsFile;
|
||||
try {
|
||||
file = JSON5.parse(raw) as ExecApprovalsFile;
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Failed to parse approvals JSON: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
}
|
||||
file.version = 1;
|
||||
const next = await saveSnapshot(opts, nodeId, file, snapshot.hash);
|
||||
const payload = opts.json ? JSON.stringify(next) : JSON.stringify(next, null, 2);
|
||||
defaultRuntime.log(payload);
|
||||
});
|
||||
nodesCallOpts(setCmd);
|
||||
|
||||
const allowlist = approvals
|
||||
.command("allowlist")
|
||||
.description("Edit the per-agent allowlist");
|
||||
|
||||
const allowlistAdd = allowlist
|
||||
.command("add <pattern>")
|
||||
.description("Add a glob pattern to an allowlist")
|
||||
.option("--node <node>", "Target node id/name/IP (defaults to gateway)")
|
||||
.option("--agent <id>", "Agent id (defaults to \"default\")")
|
||||
.action(async (pattern: string, opts: ExecApprovalsCliOpts) => {
|
||||
const trimmed = pattern.trim();
|
||||
if (!trimmed) {
|
||||
defaultRuntime.error("Pattern required.");
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
}
|
||||
const nodeId = await resolveTargetNodeId(opts);
|
||||
const snapshot = await loadSnapshot(opts, nodeId);
|
||||
if (!snapshot.hash) {
|
||||
defaultRuntime.error("Exec approvals hash missing; reload and retry.");
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
}
|
||||
const file = snapshot.file ?? { version: 1 };
|
||||
file.version = 1;
|
||||
const agentKey = resolveAgentKey(opts.agent);
|
||||
const agent = ensureAgent(file, agentKey);
|
||||
const allowlistEntries = Array.isArray(agent.allowlist) ? agent.allowlist : [];
|
||||
if (allowlistEntries.some((entry) => normalizeAllowlistEntry(entry) === trimmed)) {
|
||||
defaultRuntime.log("Already allowlisted.");
|
||||
return;
|
||||
}
|
||||
allowlistEntries.push({ pattern: trimmed, lastUsedAt: Date.now() });
|
||||
agent.allowlist = allowlistEntries;
|
||||
file.agents = { ...(file.agents ?? {}), [agentKey]: agent };
|
||||
const next = await saveSnapshot(opts, nodeId, file, snapshot.hash);
|
||||
const payload = opts.json ? JSON.stringify(next) : JSON.stringify(next, null, 2);
|
||||
defaultRuntime.log(payload);
|
||||
});
|
||||
nodesCallOpts(allowlistAdd);
|
||||
|
||||
const allowlistRemove = allowlist
|
||||
.command("remove <pattern>")
|
||||
.description("Remove a glob pattern from an allowlist")
|
||||
.option("--node <node>", "Target node id/name/IP (defaults to gateway)")
|
||||
.option("--agent <id>", "Agent id (defaults to \"default\")")
|
||||
.action(async (pattern: string, opts: ExecApprovalsCliOpts) => {
|
||||
const trimmed = pattern.trim();
|
||||
if (!trimmed) {
|
||||
defaultRuntime.error("Pattern required.");
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
}
|
||||
const nodeId = await resolveTargetNodeId(opts);
|
||||
const snapshot = await loadSnapshot(opts, nodeId);
|
||||
if (!snapshot.hash) {
|
||||
defaultRuntime.error("Exec approvals hash missing; reload and retry.");
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
}
|
||||
const file = snapshot.file ?? { version: 1 };
|
||||
file.version = 1;
|
||||
const agentKey = resolveAgentKey(opts.agent);
|
||||
const agent = ensureAgent(file, agentKey);
|
||||
const allowlistEntries = Array.isArray(agent.allowlist) ? agent.allowlist : [];
|
||||
const nextEntries = allowlistEntries.filter(
|
||||
(entry) => normalizeAllowlistEntry(entry) !== trimmed,
|
||||
);
|
||||
if (nextEntries.length === allowlistEntries.length) {
|
||||
defaultRuntime.log("Pattern not found.");
|
||||
return;
|
||||
}
|
||||
if (nextEntries.length === 0) {
|
||||
delete agent.allowlist;
|
||||
} else {
|
||||
agent.allowlist = nextEntries;
|
||||
}
|
||||
if (isEmptyAgent(agent)) {
|
||||
const agents = { ...(file.agents ?? {}) };
|
||||
delete agents[agentKey];
|
||||
file.agents = Object.keys(agents).length > 0 ? agents : undefined;
|
||||
} else {
|
||||
file.agents = { ...(file.agents ?? {}), [agentKey]: agent };
|
||||
}
|
||||
const next = await saveSnapshot(opts, nodeId, file, snapshot.hash);
|
||||
const payload = opts.json ? JSON.stringify(next) : JSON.stringify(next, null, 2);
|
||||
defaultRuntime.log(payload);
|
||||
});
|
||||
nodesCallOpts(allowlistRemove);
|
||||
}
|
||||
@@ -55,6 +55,14 @@ export function registerNodeCli(program: Command) {
|
||||
.command("daemon")
|
||||
.description("Manage the headless node daemon service (launchd/systemd/schtasks)");
|
||||
|
||||
node
|
||||
.command("status")
|
||||
.description("Show node service status")
|
||||
.option("--json", "Output JSON", false)
|
||||
.action(async (opts) => {
|
||||
await runNodeDaemonStatus(opts);
|
||||
});
|
||||
|
||||
daemon
|
||||
.command("status")
|
||||
.description("Show node daemon status")
|
||||
|
||||
@@ -8,6 +8,7 @@ import { registerDaemonCli } from "../daemon-cli.js";
|
||||
import { registerDnsCli } from "../dns-cli.js";
|
||||
import { registerDirectoryCli } from "../directory-cli.js";
|
||||
import { registerDocsCli } from "../docs-cli.js";
|
||||
import { registerExecApprovalsCli } from "../exec-approvals-cli.js";
|
||||
import { registerGatewayCli } from "../gateway-cli.js";
|
||||
import { registerHooksCli } from "../hooks-cli.js";
|
||||
import { registerWebhooksCli } from "../webhooks-cli.js";
|
||||
@@ -19,6 +20,7 @@ import { registerPairingCli } from "../pairing-cli.js";
|
||||
import { registerPluginsCli } from "../plugins-cli.js";
|
||||
import { registerSandboxCli } from "../sandbox-cli.js";
|
||||
import { registerSecurityCli } from "../security-cli.js";
|
||||
import { registerServiceCli } from "../service-cli.js";
|
||||
import { registerSkillsCli } from "../skills-cli.js";
|
||||
import { registerTuiCli } from "../tui-cli.js";
|
||||
import { registerUpdateCli } from "../update-cli.js";
|
||||
@@ -27,8 +29,10 @@ export function registerSubCliCommands(program: Command) {
|
||||
registerAcpCli(program);
|
||||
registerDaemonCli(program);
|
||||
registerGatewayCli(program);
|
||||
registerServiceCli(program);
|
||||
registerLogsCli(program);
|
||||
registerModelsCli(program);
|
||||
registerExecApprovalsCli(program);
|
||||
registerNodesCli(program);
|
||||
registerNodeCli(program);
|
||||
registerSandboxCli(program);
|
||||
|
||||
59
src/cli/service-cli.coverage.test.ts
Normal file
59
src/cli/service-cli.coverage.test.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Command } from "commander";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const runDaemonStatus = vi.fn(async () => {});
|
||||
const runNodeDaemonStatus = vi.fn(async () => {});
|
||||
|
||||
vi.mock("./daemon-cli/runners.js", () => ({
|
||||
runDaemonInstall: vi.fn(async () => {}),
|
||||
runDaemonRestart: vi.fn(async () => {}),
|
||||
runDaemonStart: vi.fn(async () => {}),
|
||||
runDaemonStatus: (opts: unknown) => runDaemonStatus(opts),
|
||||
runDaemonStop: vi.fn(async () => {}),
|
||||
runDaemonUninstall: vi.fn(async () => {}),
|
||||
}));
|
||||
|
||||
vi.mock("./node-cli/daemon.js", () => ({
|
||||
runNodeDaemonInstall: vi.fn(async () => {}),
|
||||
runNodeDaemonRestart: vi.fn(async () => {}),
|
||||
runNodeDaemonStart: vi.fn(async () => {}),
|
||||
runNodeDaemonStatus: (opts: unknown) => runNodeDaemonStatus(opts),
|
||||
runNodeDaemonStop: vi.fn(async () => {}),
|
||||
runNodeDaemonUninstall: vi.fn(async () => {}),
|
||||
}));
|
||||
|
||||
vi.mock("./deps.js", () => ({
|
||||
createDefaultDeps: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("service CLI coverage", () => {
|
||||
it("routes service gateway status to daemon status", async () => {
|
||||
runDaemonStatus.mockClear();
|
||||
runNodeDaemonStatus.mockClear();
|
||||
|
||||
const { registerServiceCli } = await import("./service-cli.js");
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
registerServiceCli(program);
|
||||
|
||||
await program.parseAsync(["service", "gateway", "status"], { from: "user" });
|
||||
|
||||
expect(runDaemonStatus).toHaveBeenCalledTimes(1);
|
||||
expect(runNodeDaemonStatus).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it("routes service node status to node daemon status", async () => {
|
||||
runDaemonStatus.mockClear();
|
||||
runNodeDaemonStatus.mockClear();
|
||||
|
||||
const { registerServiceCli } = await import("./service-cli.js");
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
registerServiceCli(program);
|
||||
|
||||
await program.parseAsync(["service", "node", "status"], { from: "user" });
|
||||
|
||||
expect(runNodeDaemonStatus).toHaveBeenCalledTimes(1);
|
||||
expect(runDaemonStatus).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
157
src/cli/service-cli.ts
Normal file
157
src/cli/service-cli.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import type { Command } from "commander";
|
||||
import { formatDocsLink } from "../terminal/links.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
import { createDefaultDeps } from "./deps.js";
|
||||
import {
|
||||
runDaemonInstall,
|
||||
runDaemonRestart,
|
||||
runDaemonStart,
|
||||
runDaemonStatus,
|
||||
runDaemonStop,
|
||||
runDaemonUninstall,
|
||||
} from "./daemon-cli/runners.js";
|
||||
import {
|
||||
runNodeDaemonInstall,
|
||||
runNodeDaemonRestart,
|
||||
runNodeDaemonStart,
|
||||
runNodeDaemonStatus,
|
||||
runNodeDaemonStop,
|
||||
runNodeDaemonUninstall,
|
||||
} from "./node-cli/daemon.js";
|
||||
|
||||
export function registerServiceCli(program: Command) {
|
||||
const service = program
|
||||
.command("service")
|
||||
.description("Manage Gateway and node host services (launchd/systemd/schtasks)")
|
||||
.addHelpText(
|
||||
"after",
|
||||
() =>
|
||||
`\n${theme.muted("Docs:")} ${formatDocsLink("/cli/service", "docs.clawd.bot/cli/service")}\n`,
|
||||
);
|
||||
|
||||
const gateway = service.command("gateway").description("Manage the Gateway service");
|
||||
|
||||
gateway
|
||||
.command("status")
|
||||
.description("Show gateway service status + probe the Gateway")
|
||||
.option("--url <url>", "Gateway WebSocket URL (defaults to config/remote/local)")
|
||||
.option("--token <token>", "Gateway token (if required)")
|
||||
.option("--password <password>", "Gateway password (password auth)")
|
||||
.option("--timeout <ms>", "Timeout in ms", "10000")
|
||||
.option("--no-probe", "Skip RPC probe")
|
||||
.option("--deep", "Scan system-level services", false)
|
||||
.option("--json", "Output JSON", false)
|
||||
.action(async (opts) => {
|
||||
await runDaemonStatus({
|
||||
rpc: opts,
|
||||
probe: Boolean(opts.probe),
|
||||
deep: Boolean(opts.deep),
|
||||
json: Boolean(opts.json),
|
||||
});
|
||||
});
|
||||
|
||||
gateway
|
||||
.command("install")
|
||||
.description("Install the Gateway service (launchd/systemd/schtasks)")
|
||||
.option("--port <port>", "Gateway port")
|
||||
.option("--runtime <runtime>", "Service runtime (node|bun). Default: node")
|
||||
.option("--token <token>", "Gateway token (token auth)")
|
||||
.option("--force", "Reinstall/overwrite if already installed", false)
|
||||
.option("--json", "Output JSON", false)
|
||||
.action(async (opts) => {
|
||||
await runDaemonInstall(opts);
|
||||
});
|
||||
|
||||
gateway
|
||||
.command("uninstall")
|
||||
.description("Uninstall the Gateway service (launchd/systemd/schtasks)")
|
||||
.option("--json", "Output JSON", false)
|
||||
.action(async (opts) => {
|
||||
await runDaemonUninstall(opts);
|
||||
});
|
||||
|
||||
gateway
|
||||
.command("start")
|
||||
.description("Start the Gateway service (launchd/systemd/schtasks)")
|
||||
.option("--json", "Output JSON", false)
|
||||
.action(async (opts) => {
|
||||
await runDaemonStart(opts);
|
||||
});
|
||||
|
||||
gateway
|
||||
.command("stop")
|
||||
.description("Stop the Gateway service (launchd/systemd/schtasks)")
|
||||
.option("--json", "Output JSON", false)
|
||||
.action(async (opts) => {
|
||||
await runDaemonStop(opts);
|
||||
});
|
||||
|
||||
gateway
|
||||
.command("restart")
|
||||
.description("Restart the Gateway service (launchd/systemd/schtasks)")
|
||||
.option("--json", "Output JSON", false)
|
||||
.action(async (opts) => {
|
||||
await runDaemonRestart(opts);
|
||||
});
|
||||
|
||||
const node = service.command("node").description("Manage the node host service");
|
||||
|
||||
node
|
||||
.command("status")
|
||||
.description("Show node host service status")
|
||||
.option("--json", "Output JSON", false)
|
||||
.action(async (opts) => {
|
||||
await runNodeDaemonStatus(opts);
|
||||
});
|
||||
|
||||
node
|
||||
.command("install")
|
||||
.description("Install the node host service (launchd/systemd/schtasks)")
|
||||
.option("--host <host>", "Gateway bridge host")
|
||||
.option("--port <port>", "Gateway bridge port")
|
||||
.option("--tls", "Use TLS for the bridge connection", false)
|
||||
.option("--tls-fingerprint <sha256>", "Expected TLS certificate fingerprint (sha256)")
|
||||
.option("--node-id <id>", "Override node id (clears pairing token)")
|
||||
.option("--display-name <name>", "Override node display name")
|
||||
.option("--runtime <runtime>", "Service runtime (node|bun). Default: node")
|
||||
.option("--force", "Reinstall/overwrite if already installed", false)
|
||||
.option("--json", "Output JSON", false)
|
||||
.action(async (opts) => {
|
||||
await runNodeDaemonInstall(opts);
|
||||
});
|
||||
|
||||
node
|
||||
.command("uninstall")
|
||||
.description("Uninstall the node host service (launchd/systemd/schtasks)")
|
||||
.option("--json", "Output JSON", false)
|
||||
.action(async (opts) => {
|
||||
await runNodeDaemonUninstall(opts);
|
||||
});
|
||||
|
||||
node
|
||||
.command("start")
|
||||
.description("Start the node host service (launchd/systemd/schtasks)")
|
||||
.option("--json", "Output JSON", false)
|
||||
.action(async (opts) => {
|
||||
await runNodeDaemonStart(opts);
|
||||
});
|
||||
|
||||
node
|
||||
.command("stop")
|
||||
.description("Stop the node host service (launchd/systemd/schtasks)")
|
||||
.option("--json", "Output JSON", false)
|
||||
.action(async (opts) => {
|
||||
await runNodeDaemonStop(opts);
|
||||
});
|
||||
|
||||
node
|
||||
.command("restart")
|
||||
.description("Restart the node host service (launchd/systemd/schtasks)")
|
||||
.option("--json", "Output JSON", false)
|
||||
.action(async (opts) => {
|
||||
await runNodeDaemonRestart(opts);
|
||||
});
|
||||
|
||||
// Build default deps (parity with daemon CLI).
|
||||
void createDefaultDeps();
|
||||
}
|
||||
Reference in New Issue
Block a user