File size: 4,101 Bytes
f778c12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
// Root `nodes` command registration: wires node status, pairing, invoke, media, and plugin extensions.
import type { Command } from "commander";
import { formatDocsLink } from "../../../packages/terminal-core/src/links.js";
import { theme } from "../../../packages/terminal-core/src/theme.js";
import { resolveCliArgvInvocation } from "../argv-invocation.js";
import { formatHelpExamples } from "../help-format.js";
import { withConsoleLogsRoutedToStderrForJson } from "../json-output-mode.js";
import { setCommandJsonMode } from "../program/json-mode.js";
import { isNodesMachineOutput } from "./output-mode.js";
import { registerNodesCameraCommands } from "./register.camera.js";
import { registerNodesInvokeCommands } from "./register.invoke.js";
import { registerNodesLocationCommands } from "./register.location.js";
import { registerNodesNotifyCommand } from "./register.notify.js";
import { registerNodesPairingCommands } from "./register.pairing.js";
import { registerNodesPushCommand } from "./register.push.js";
import { registerNodesScreenCommands } from "./register.screen.js";
import { registerNodesStatusCommands } from "./register.status.js";

/** Register the `nodes` command group and lazy plugin-provided node commands. */
export async function registerNodesCli(program: Command, argv: readonly string[] = process.argv) {
  const nodes = program
    .command("nodes")
    .description("Manage gateway-owned nodes (pairing, status, invoke, and media)")
    .addHelpText(
      "after",
      () =>
        `\n${theme.heading("Examples:")}\n${formatHelpExamples([
          ["openclaw nodes status", "List known nodes with live status."],
          ["openclaw nodes pending", "Show pending node pairing requests."],
          ["openclaw nodes remove --node <id|name|ip>", "Remove a stale paired node entry."],
          [
            'openclaw nodes invoke --node <id> --command system.which --params \'{"bins":["uname"]}\'',
            "Invoke a node command directly.",
          ],
          ["openclaw nodes camera snap --node <id>", "Capture a photo from a node camera."],
        ])}\n\n${theme.muted("Docs:")} ${formatDocsLink("/cli/nodes", "docs.openclaw.ai/cli/nodes")}\n`,
    );

  registerNodesStatusCommands(nodes);
  registerNodesPairingCommands(nodes);
  registerNodesInvokeCommands(nodes);
  registerNodesNotifyCommand(nodes);
  registerNodesPushCommand(nodes);
  registerNodesCameraCommands(nodes);
  registerNodesScreenCommands(nodes);
  registerNodesLocationCommands(nodes);
  setCommandJsonMode(nodes, "output", ({ argv: commandArgv }) => isNodesMachineOutput(commandArgv));

  // Built-in `nodes` subcommands (status/list/pairing/invoke/...) must stay on the lightweight
  // path: loading plugin CLI/runtime to resolve them only adds startup cost. Plugin-provided node
  // subcommands (e.g. `nodes canvas`) are not registered above, so only pay the plugin load when
  // the invoked subcommand is not already a built-in.
  if (!shouldRegisterNodesPluginCommands(nodes, argv)) {
    return;
  }
  const { registerPluginCliCommandsFromValidatedConfig } = await import("../../plugins/cli.js");
  await withConsoleLogsRoutedToStderrForJson(
    argv,
    async () =>
      await registerPluginCliCommandsFromValidatedConfig(program, undefined, undefined, {
        mode: "lazy",
        primary: "nodes",
      }),
  );
}

/** Plugin node subcommands are only resolved when the invocation is not a built-in nodes command. */
function shouldRegisterNodesPluginCommands(nodes: Command, argv: readonly string[]): boolean {
  const { commandPath } = resolveCliArgvInvocation([...argv]);
  if (commandPath[0] !== "nodes") {
    // Eager registration (root help/completion) needs the full command tree, plugins included.
    return true;
  }
  const requestedSubcommand = commandPath[1];
  if (!requestedSubcommand) {
    // Bare `openclaw nodes` listing should still surface plugin-provided subcommands.
    return true;
  }
  const builtInSubcommands = new Set(nodes.commands.map((command) => command.name()));
  return !builtInSubcommands.has(requestedSubcommand);
}