diff --git a/src/lib/command-registry.ts b/src/lib/command-registry.ts index d2bee73ca38..37a9f8c46c1 100644 --- a/src/lib/command-registry.ts +++ b/src/lib/command-registry.ts @@ -283,6 +283,7 @@ export const COMMANDS: readonly CommandDef[] = [ { usage: "nemoclaw config get", description: "Get sandbox configuration", + flags: "[--key ] [--format json|yaml]", group: "Sandbox Management", scope: "sandbox", hidden: true, diff --git a/src/lib/connect-cli-command.ts b/src/lib/connect-cli-command.ts index cd490ceed10..a932374a754 100644 --- a/src/lib/connect-cli-command.ts +++ b/src/lib/connect-cli-command.ts @@ -14,6 +14,10 @@ export default class ConnectCliCommand extends Command { static summary = "Shell into a running sandbox"; static description = "Connect to a running sandbox."; static usage = [" connect [--probe-only]"]; + static examples = [ + "<%= config.bin %> alpha connect", + "<%= config.bin %> alpha connect --probe-only", + ]; static args = { sandboxName: Args.string({ name: "sandbox", description: "Sandbox name", required: true }), }; diff --git a/src/lib/legacy-oclif-dispatch.test.ts b/src/lib/legacy-oclif-dispatch.test.ts index bcaffa0723b..b1c65824f45 100644 --- a/src/lib/legacy-oclif-dispatch.test.ts +++ b/src/lib/legacy-oclif-dispatch.test.ts @@ -29,6 +29,13 @@ describe("resolveSandboxOclifDispatch", () => { }); }); + it("keeps sandbox doctor help public", () => { + expect(resolveSandboxOclifDispatch("alpha", "doctor", ["--help"])).toEqual({ + kind: "help", + usage: "doctor [--json]", + }); + }); + it("keeps sandbox logs help public with supported filters", () => { expect(resolveSandboxOclifDispatch("alpha", "logs", ["--help"])).toEqual({ kind: "help", diff --git a/src/lib/legacy-oclif-dispatch.ts b/src/lib/legacy-oclif-dispatch.ts index 1bbcfdd6f95..e8527d89c19 100644 --- a/src/lib/legacy-oclif-dispatch.ts +++ b/src/lib/legacy-oclif-dispatch.ts @@ -111,6 +111,7 @@ export function resolveSandboxOclifDispatch( } return { kind: "oclif", commandId: "sandbox:logs", args: [sandboxName, ...actionArgs] }; case "doctor": + if (hasHelpFlag(actionArgs)) return { kind: "help", usage: "doctor [--json]" }; return { kind: "oclif", commandId: "sandbox:doctor", args: [sandboxName, ...actionArgs] }; case "policy-add": if (hasHelpFlag(actionArgs)) { diff --git a/src/lib/sandbox-doctor-cli-command.ts b/src/lib/sandbox-doctor-cli-command.ts index 9b03c3defb0..b837cee6df9 100644 --- a/src/lib/sandbox-doctor-cli-command.ts +++ b/src/lib/sandbox-doctor-cli-command.ts @@ -3,19 +3,31 @@ /* v8 ignore start -- thin oclif adapter covered through CLI integration tests. */ -import { Command } from "@oclif/core"; +import { Args, Command, Flags } from "@oclif/core"; import { runSandboxDoctor } from "./sandbox-doctor-action"; export default class SandboxDoctorCliCommand extends Command { static id = "sandbox:doctor"; - static strict = false; + static strict = true; static summary = "Diagnose sandbox and gateway health"; static description = "Run host, gateway, sandbox, inference, messaging, and local service diagnostics."; static usage = [" doctor [--json]"]; + static examples = ["<%= config.bin %> alpha doctor", "<%= config.bin %> alpha doctor --json"]; + static args = { + sandboxName: Args.string({ + name: "sandbox", + description: "Sandbox name", + required: true, + }), + }; + static flags = { + help: Flags.help({ char: "h" }), + json: Flags.boolean({ description: "Emit machine-readable JSON diagnostics" }), + }; public async run(): Promise { - const [sandboxName, ...actionArgs] = this.argv; - await runSandboxDoctor(sandboxName, actionArgs); + const { args, flags } = await this.parse(SandboxDoctorCliCommand); + await runSandboxDoctor(args.sandboxName, flags.json ? ["--json"] : []); } } diff --git a/src/lib/sandbox-inspection-cli-command.ts b/src/lib/sandbox-inspection-cli-command.ts index db4df2675a1..a342894580c 100644 --- a/src/lib/sandbox-inspection-cli-command.ts +++ b/src/lib/sandbox-inspection-cli-command.ts @@ -22,6 +22,7 @@ export class SandboxStatusCommand extends Command { static summary = "Sandbox health and NIM status"; static description = "Show sandbox health, OpenShell gateway state, and local NIM status."; static usage = [" status"]; + static examples = ["<%= config.bin %> alpha status"]; static args = { sandboxName: sandboxNameArg, }; @@ -41,6 +42,7 @@ export class SandboxPolicyListCommand extends Command { static summary = "List policy presets"; static description = "List built-in and custom policy presets and show which are applied."; static usage = [" policy-list"]; + static examples = ["<%= config.bin %> alpha policy-list"]; static args = { sandboxName: sandboxNameArg, }; @@ -60,6 +62,7 @@ export class SandboxChannelsListCommand extends Command { static summary = "List supported messaging channels"; static description = "List supported messaging channels for a sandbox."; static usage = [" channels list"]; + static examples = ["<%= config.bin %> alpha channels list"]; static args = { sandboxName: sandboxNameArg, }; @@ -79,21 +82,24 @@ export class SandboxConfigGetCommand extends Command { static summary = "Get sandbox configuration"; static description = "Read sanitized sandbox agent configuration."; static usage = [" config get [--key dotpath] [--format json|yaml]"]; + static examples = [ + "<%= config.bin %> alpha config get", + "<%= config.bin %> alpha config get --key model --format yaml", + ]; static args = { sandboxName: sandboxNameArg, }; static flags = { help: Flags.help({ char: "h" }), key: Flags.string({ description: "Dotpath to read from the sanitized config" }), - format: Flags.string({ description: "Output format (json or yaml)" }), + format: Flags.string({ + description: "Output format", + options: ["json", "yaml"], + }), }; public async run(): Promise { const { args, flags } = await this.parse(SandboxConfigGetCommand); - if (flags.format && flags.format !== "json" && flags.format !== "yaml") { - console.error(` Unknown format: ${flags.format}. Use json or yaml.`); - process.exit(1); - } sandboxConfig.configGet(args.sandboxName, { key: flags.key ?? null, format: flags.format ?? "json", diff --git a/test/cli.test.ts b/test/cli.test.ts index 92ef05c5e58..90577972602 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -1350,6 +1350,11 @@ describe("CLI dispatch", () => { expect(status.out).toContain(" status"); expect(status.out).not.toContain("sandbox:status"); + const doctor = runWithEnv("alpha doctor --help", { HOME: home }); + expect(doctor.code).toBe(0); + expect(doctor.out).toContain(" doctor [--json]"); + expect(doctor.out).not.toContain("sandbox:doctor"); + const logs = runWithEnv("alpha logs --help", { HOME: home }); expect(logs.code).toBe(0); expect(logs.out).toContain(" logs"); @@ -1390,6 +1395,7 @@ describe("CLI dispatch", () => { const config = runWithEnv("alpha config get --help", { HOME: home }); expect(config.code).toBe(0); expect(config.out).toContain(" config get"); + expect(config.out).toContain("--format json|yaml"); expect(config.out).not.toContain("sandbox:config:get"); }); @@ -1432,6 +1438,21 @@ describe("CLI dispatch", () => { expect(start.out).toContain("Channel 'telegram' is already enabled for 'alpha'. Nothing to do."); }); + it("diagnostic commands reject invalid parser-owned flags before dispatch", () => { + const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-cli-diagnostics-invalid-flags-")); + writeSandboxRegistry(home); + + const badConfigFormat = runWithEnv("alpha config get --format xml 2>&1", { HOME: home }); + expect(badConfigFormat.code).not.toBe(0); + expect(badConfigFormat.out).toContain("--format"); + expect(badConfigFormat.out).toContain("json"); + expect(badConfigFormat.out).toContain("yaml"); + + const badDoctorFlag = runWithEnv("alpha doctor --bogus 2>&1", { HOME: home }); + expect(badDoctorFlag.code).not.toBe(0); + expect(badDoctorFlag.out).toContain("Nonexistent flag: --bogus"); + }); + it("shields help keeps public sandbox-scoped usage", () => { const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-cli-shields-help-")); writeSandboxRegistry(home);