Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/lib/command-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ export const COMMANDS: readonly CommandDef[] = [
{
usage: "nemoclaw <name> config get",
description: "Get sandbox configuration",
flags: "[--key <dotpath>] [--format json|yaml]",
group: "Sandbox Management",
scope: "sandbox",
hidden: true,
Expand Down
4 changes: 4 additions & 0 deletions src/lib/connect-cli-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ["<name> 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 }),
};
Expand Down
7 changes: 7 additions & 0 deletions src/lib/legacy-oclif-dispatch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/lib/legacy-oclif-dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
20 changes: 16 additions & 4 deletions src/lib/sandbox-doctor-cli-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ["<name> 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<void> {
const [sandboxName, ...actionArgs] = this.argv;
await runSandboxDoctor(sandboxName, actionArgs);
const { args, flags } = await this.parse(SandboxDoctorCliCommand);
await runSandboxDoctor(args.sandboxName, flags.json ? ["--json"] : []);
}
}
16 changes: 11 additions & 5 deletions src/lib/sandbox-inspection-cli-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ["<name> status"];
static examples = ["<%= config.bin %> alpha status"];
static args = {
sandboxName: sandboxNameArg,
};
Expand All @@ -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 = ["<name> policy-list"];
static examples = ["<%= config.bin %> alpha policy-list"];
static args = {
sandboxName: sandboxNameArg,
};
Expand All @@ -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 = ["<name> channels list"];
static examples = ["<%= config.bin %> alpha channels list"];
static args = {
sandboxName: sandboxNameArg,
};
Expand All @@ -79,21 +82,24 @@ export class SandboxConfigGetCommand extends Command {
static summary = "Get sandbox configuration";
static description = "Read sanitized sandbox agent configuration.";
static usage = ["<name> 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<void> {
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",
Expand Down
21 changes: 21 additions & 0 deletions test/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1350,6 +1350,11 @@ describe("CLI dispatch", () => {
expect(status.out).toContain("<name> 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("<name> 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("<name> logs");
Expand Down Expand Up @@ -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("<name> config get");
expect(config.out).toContain("--format json|yaml");
expect(config.out).not.toContain("sandbox:config:get");
});

Expand Down Expand Up @@ -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);
Expand Down
Loading