File size: 6,525 Bytes
eb3f11e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
// Root Commander help, global options, banner, version, and example formatting.
import type { Command } from "commander";
import { formatDocsLink } from "../../../packages/terminal-core/src/links.js";
import { isRich, theme } from "../../../packages/terminal-core/src/theme.js";
import { resolveCommitHash } from "../../infra/git-commit.js";
import { formatConsoleDiagnosticBlock } from "../../logging/json-console-line.js";
import { escapeRegExp } from "../../utils.js";
import { isRootVersionInvocation } from "../argv.js";
import { formatCliBannerLine, hasEmittedCliBanner } from "../banner.js";
import { CLI_NAME } from "../cli-name.js";
import { CLI_LOG_LEVEL_VALUES, parseCliLogLevelOption } from "../log-level-option.js";
import {
  getCommanderErrorCommandNames,
  getCommanderErrorCommandPath,
} from "./commander-parse-facts.js";
import { getCoreCliCommandsWithSubcommands } from "./core-command-descriptors.js";
import { formatCliParseErrorOutput } from "./error-output.js";
import { getSubCliCommandsWithSubcommands } from "./subcli-descriptors.js";

const CLI_NAME_PATTERN = escapeRegExp(CLI_NAME);
const ROOT_COMMANDS_WITH_SUBCOMMANDS = new Set([
  ...getCoreCliCommandsWithSubcommands(),
  ...getSubCliCommandsWithSubcommands(),
]);
const ROOT_COMMANDS_HINT =
  "Hint: commands suffixed with * have subcommands. Run <command> --help for details.";

const EXAMPLES = [
  ["openclaw onboard", "Run guided setup for a local Gateway, workspace, auth, and channels."],
  ["openclaw setup", "Create the baseline config, workspace, and session folders."],
  ["openclaw configure", "Change models, Gateway, channels, plugins, skills, and health checks."],
  ["openclaw status", "Check Gateway, channel, model, and recent-session status."],
  ["openclaw doctor --fix", "Repair common config, service, plugin, and channel problems."],
  ["openclaw channels add", "Add or update a chat channel account with guided prompts."],
  ["openclaw channels status", "See connected messaging accounts and login state."],
  ["openclaw --dev gateway", "Run a dev Gateway (isolated state/config) on ws://127.0.0.1:19001."],
  ["openclaw gateway run --force", "Start the Gateway and replace anything bound to its port."],
  ["openclaw models status", "Show model/provider auth health before running agents."],
  ["openclaw plugins list", "Inspect enabled, disabled, and installed plugins."],
  [
    'openclaw agent --to +15555550123 --message "Run summary" --deliver',
    "Run one agent turn through the Gateway and optionally deliver the reply.",
  ],
  [
    'openclaw message send --channel telegram --target @mychat --message "Hi"',
    "Send via your Telegram bot.",
  ],
] as const;

export function formatProgramHelpOutput(str: string): string {
  // Commander emits plain section labels; decorate them after command-specific help renders.
  let output = str;
  const isRootHelp = new RegExp(
    `^Usage:\\s+${CLI_NAME_PATTERN}\\s+\\[options\\]\\s+\\[command\\]\\s*$`,
    "m",
  ).test(output);
  if (isRootHelp && /^Commands:/m.test(output)) {
    output = output.replace(/^Commands:/m, `Commands:\n  ${theme.muted(ROOT_COMMANDS_HINT)}`);
  }

  return output
    .replace(/^Usage:/gm, theme.heading("Usage:"))
    .replace(/^Options:/gm, theme.heading("Options:"))
    .replace(/^Commands:/gm, theme.heading("Commands:"));
}

export function configureProgramHelp(
  program: Command,
  ctx: { programVersion: string },
  options?: { commandsWithSubcommands?: ReadonlySet<string> },
) {
  const commandsWithSubcommands = new Set([
    ...ROOT_COMMANDS_WITH_SUBCOMMANDS,
    ...(options?.commandsWithSubcommands ?? []),
  ]);

  program
    .name(CLI_NAME)
    .description("")
    .version(ctx.programVersion)
    .option(
      "--container <name>",
      "Run the CLI inside a running Podman/Docker container named <name> (default: env OPENCLAW_CONTAINER)",
    )
    .option(
      "--dev",
      "Dev profile: isolate state under ~/.openclaw-dev, default gateway port 19001, and shift derived ports (browser/canvas)",
    )
    .option(
      "--profile <name>",
      "Use a named profile (isolates OPENCLAW_STATE_DIR/OPENCLAW_CONFIG_PATH under ~/.openclaw-<name>)",
    )
    .option(
      "--log-level <level>",
      `Global log level override for file + console (${CLI_LOG_LEVEL_VALUES})`,
      parseCliLogLevelOption,
    );

  program.option("--no-color", "Disable ANSI colors", false);
  program.helpOption("-h, --help", "Display help for command");
  program.helpCommand("help [command]", "Display help for command");

  program.configureHelp({
    // sort options and subcommands alphabetically
    sortSubcommands: true,
    sortOptions: true,
    optionTerm: (option) => theme.option(option.flags),
    subcommandTerm: (cmd) => {
      const isRootCommand = cmd.parent === program;
      const hasSubcommands = isRootCommand && commandsWithSubcommands.has(cmd.name());
      return theme.command(hasSubcommands ? `${cmd.name()} *` : cmd.name());
    },
  });

  program.configureOutput({
    writeOut: (str) => {
      process.stdout.write(formatProgramHelpOutput(str));
    },
    writeErr: (str) => {
      const message = formatProgramHelpOutput(str);
      process.stderr.write(formatConsoleDiagnosticBlock({ level: "error", message }));
    },
    outputError: (str, write) => {
      write(
        formatCliParseErrorOutput(str, {
          argv: process.argv,
          commandPath: getCommanderErrorCommandPath(program),
          commandNames: getCommanderErrorCommandNames(program),
        }),
      );
    },
  });

  if (isRootVersionInvocation(process.argv)) {
    const commit = resolveCommitHash({ moduleUrl: import.meta.url });
    console.log(
      commit ? `OpenClaw ${ctx.programVersion} (${commit})` : `OpenClaw ${ctx.programVersion}`,
    );
    process.exit(0);
  }

  program.addHelpText("beforeAll", () => {
    if (hasEmittedCliBanner() || process.env.OPENCLAW_SUPPRESS_HELP_BANNER === "1") {
      return "";
    }
    const rich = isRich();
    const line = formatCliBannerLine(ctx.programVersion, { richTty: rich, mode: "default" });
    return `\n${line}\n`;
  });

  const fmtExamples = EXAMPLES.map(
    ([cmd, desc]) => `  ${theme.command(cmd)}\n    ${theme.muted(desc)}`,
  ).join("\n");

  program.addHelpText("afterAll", ({ command }) => {
    if (command !== program) {
      return "";
    }
    const docs = formatDocsLink("/cli", "docs.openclaw.ai/cli");
    return `\n${theme.heading("Examples:")}\n${fmtExamples}\n\n${theme.muted("Docs:")} ${docs}\n`;
  });
}