diff --git a/src/lib/onboard-cli-commands.ts b/src/lib/onboard-cli-commands.ts index f57e36b712..5d2bb5ad79 100644 --- a/src/lib/onboard-cli-commands.ts +++ b/src/lib/onboard-cli-commands.ts @@ -3,45 +3,122 @@ /* v8 ignore start -- thin oclif adapters covered through CLI integration tests. */ -import { Command } from "@oclif/core"; +import { Command, Flags } from "@oclif/core"; import { runOnboardAction, runSetupAction, runSetupSparkAction } from "./global-cli-actions"; +import { NOTICE_ACCEPT_FLAG } from "./usage-notice"; + +const acceptFlagName = NOTICE_ACCEPT_FLAG.replace(/^--/, ""); + +const onboardUsage = [ + `onboard [--non-interactive] [--resume | --fresh] [--recreate-sandbox] [--from ] [--name ] [--agent ] [--control-ui-port ] [${NOTICE_ACCEPT_FLAG}]`, +]; + +const onboardExamples = [ + "<%= config.bin %> onboard", + "<%= config.bin %> onboard --name alpha", + "<%= config.bin %> onboard --resume", + "<%= config.bin %> onboard --fresh", + "<%= config.bin %> onboard --from ./Dockerfile --name alpha", + `<%= config.bin %> onboard --non-interactive --name alpha ${NOTICE_ACCEPT_FLAG}`, +]; + +type OnboardFlags = { + "non-interactive"?: boolean; + resume?: boolean; + fresh?: boolean; + "recreate-sandbox"?: boolean; + from?: string; + name?: string; + agent?: string; + "control-ui-port"?: number; + [acceptFlagName]?: boolean; +}; + +function buildOnboardFlags(): Record { + return { + help: Flags.help({ char: "h" }), + "non-interactive": Flags.boolean({ description: "Run without interactive prompts" }), + resume: Flags.boolean({ description: "Resume an interrupted onboarding session" }), + fresh: Flags.boolean({ description: "Ignore any saved onboarding session" }), + "recreate-sandbox": Flags.boolean({ description: "Delete and recreate an existing sandbox" }), + from: Flags.string({ description: "Path to a Dockerfile to use as the sandbox image source" }), + name: Flags.string({ description: "Sandbox name" }), + agent: Flags.string({ description: "Agent runtime to onboard" }), + "control-ui-port": Flags.integer({ + description: "Host port for the local control UI", + max: 65535, + min: 1024, + }), + [acceptFlagName]: Flags.boolean({ description: "Accept the third-party software notice" }), + } as Record; +} + +function toLegacyOnboardArgs(flags: OnboardFlags): string[] { + const args: string[] = []; + if (flags["non-interactive"]) args.push("--non-interactive"); + if (flags.resume) args.push("--resume"); + if (flags.fresh) args.push("--fresh"); + if (flags["recreate-sandbox"]) args.push("--recreate-sandbox"); + if (flags.from !== undefined) args.push("--from", flags.from); + if (flags.name !== undefined) args.push("--name", flags.name); + if (flags.agent !== undefined) args.push("--agent", flags.agent); + if (flags["control-ui-port"] !== undefined) { + args.push("--control-ui-port", String(flags["control-ui-port"])); + } + if (flags[acceptFlagName]) args.push(NOTICE_ACCEPT_FLAG); + return args; +} export class OnboardCliCommand extends Command { static id = "onboard"; - static strict = false; + static strict = true; static summary = "Configure inference endpoint and credentials"; static description = "Configure inference, credentials, and sandbox settings."; - static usage = ["onboard [flags]"]; + static usage = onboardUsage; + static examples = onboardExamples; + static flags = buildOnboardFlags(); public async run(): Promise { - this.parsed = true; - await runOnboardAction(this.argv); + const { flags } = await this.parse(OnboardCliCommand); + await runOnboardAction(toLegacyOnboardArgs(flags as OnboardFlags)); } } export class SetupCliCommand extends Command { static id = "setup"; - static strict = false; + static strict = true; static summary = "Deprecated alias for nemoclaw onboard"; static description = "Deprecated alias for onboard."; static usage = ["setup [flags]"]; + static examples = ["<%= config.bin %> setup --name alpha"]; + static flags = buildOnboardFlags(); public async run(): Promise { - this.parsed = true; - await runSetupAction(this.argv); + if (this.argv.includes("--help") || this.argv.includes("-h")) { + await runSetupAction(["--help"]); + return; + } + const { flags } = await this.parse(SetupCliCommand); + await runSetupAction(toLegacyOnboardArgs(flags as OnboardFlags)); } } export class SetupSparkCliCommand extends Command { static id = "setup-spark"; - static strict = false; + static strict = true; static summary = "Deprecated alias for nemoclaw onboard"; static description = "Deprecated alias for onboard."; static usage = ["setup-spark [flags]"]; + static examples = ["<%= config.bin %> setup-spark --name alpha"]; + static flags = buildOnboardFlags(); public async run(): Promise { - this.parsed = true; - await runSetupSparkAction(this.argv); + if (this.argv.includes("--help") || this.argv.includes("-h")) { + await runSetupSparkAction(["--help"]); + return; + } + const { flags } = await this.parse(SetupSparkCliCommand); + await runSetupSparkAction(toLegacyOnboardArgs(flags as OnboardFlags)); } } diff --git a/test/cli.test.ts b/test/cli.test.ts index a59d6f7e13..5e87d3f062 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -12,6 +12,7 @@ import { execTimeout, testTimeout, testTimeoutOptions } from "./helpers/timeouts const CLI = path.join(import.meta.dirname, "..", "bin", "nemoclaw.js"); const HERMES_CLI = path.join(import.meta.dirname, "..", "bin", "nemohermes.js"); +const PARSER_EXIT_CODE = 2; type CliRunResult = { code: number; @@ -902,26 +903,27 @@ describe("CLI dispatch", () => { it("onboard --help exits 0 and shows usage", () => { const r = run("onboard --help"); expect(r.code).toBe(0); - expect(r.out.includes("Usage: nemoclaw onboard")).toBeTruthy(); - expect(r.out.includes("--from ")).toBeTruthy(); + expect(r.out).toContain("USAGE"); + expect(r.out).toContain("nemoclaw onboard"); + expect(r.out).toContain("--from "); }); it("unknown onboard option exits 1", () => { const r = run("onboard --non-interactiv"); - expect(r.code).toBe(1); - expect(r.out.includes("Unknown onboard option")).toBeTruthy(); + expect(r.code).toBe(PARSER_EXIT_CODE); + expect(r.out).toContain("Nonexistent flag: --non-interactiv"); }); it("accepts onboard --resume in CLI parsing", () => { const r = run("onboard --resume --non-interactiv"); - expect(r.code).toBe(1); - expect(r.out.includes("Unknown onboard option(s): --non-interactiv")).toBeTruthy(); + expect(r.code).toBe(PARSER_EXIT_CODE); + expect(r.out).toContain("Nonexistent flag: --non-interactiv"); }); it("accepts the third-party software flag in onboard CLI parsing", () => { const r = run("onboard --yes-i-accept-third-party-software --non-interactiv"); - expect(r.code).toBe(1); - expect(r.out.includes("Unknown onboard option(s): --non-interactiv")).toBeTruthy(); + expect(r.code).toBe(PARSER_EXIT_CODE); + expect(r.out).toContain("Nonexistent flag: --non-interactiv"); }); it("setup --help exits 0 and shows onboard usage", () => { @@ -934,9 +936,8 @@ describe("CLI dispatch", () => { it("setup forwards unknown options into onboard parsing", () => { const r = run("setup --non-interactiv"); - expect(r.code).toBe(1); - expect(r.out.includes("deprecated")).toBeTruthy(); - expect(r.out.includes("Unknown onboard option(s): --non-interactiv")).toBeTruthy(); + expect(r.code).toBe(PARSER_EXIT_CODE); + expect(r.out).toContain("Nonexistent flag: --non-interactiv"); }); it("setup forwards --resume into onboard parsing", () => {