diff --git a/package.json b/package.json index 8ea03f4879..552cef83c5 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "bin": "nemoclaw", "commands": { "strategy": "explicit", - "target": "./dist/lib/oclif-commands.js" + "target": "./dist/lib/commands/index.js" } }, "scripts": { diff --git a/src/lib/channels-mutate-cli-commands.ts b/src/lib/channels-mutate-cli-commands.ts deleted file mode 100644 index af2d9b9433..0000000000 --- a/src/lib/channels-mutate-cli-commands.ts +++ /dev/null @@ -1,119 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { Args, Command, Flags } from "@oclif/core"; - -type ChannelsRuntimeBridge = { - sandboxChannelsAdd: (sandboxName: string, args?: string[]) => Promise; - sandboxChannelsRemove: (sandboxName: string, args?: string[]) => Promise; - sandboxChannelsStart: (sandboxName: string, args?: string[]) => Promise; - sandboxChannelsStop: (sandboxName: string, args?: string[]) => Promise; -}; - -let runtimeBridgeFactory = (): ChannelsRuntimeBridge => { - const actions = require("./policy-channel-actions") as { - addSandboxChannel: ChannelsRuntimeBridge["sandboxChannelsAdd"]; - removeSandboxChannel: ChannelsRuntimeBridge["sandboxChannelsRemove"]; - startSandboxChannel: ChannelsRuntimeBridge["sandboxChannelsStart"]; - stopSandboxChannel: ChannelsRuntimeBridge["sandboxChannelsStop"]; - }; - return { - sandboxChannelsAdd: actions.addSandboxChannel, - sandboxChannelsRemove: actions.removeSandboxChannel, - sandboxChannelsStart: actions.startSandboxChannel, - sandboxChannelsStop: actions.stopSandboxChannel, - }; -}; - -export function setChannelsRuntimeBridgeFactoryForTest( - factory: () => ChannelsRuntimeBridge, -): void { - runtimeBridgeFactory = factory; -} - -function getRuntimeBridge(): ChannelsRuntimeBridge { - return runtimeBridgeFactory(); -} - -const sandboxNameArg = Args.string({ name: "sandbox", description: "Sandbox name", required: true }); -const channelArg = Args.string({ name: "channel", description: "Messaging channel", required: true }); - -function buildArgs(channel: string | undefined, flags: { "dry-run"?: boolean }): string[] { - const args: string[] = []; - if (channel) args.push(channel); - if (flags["dry-run"]) args.push("--dry-run"); - return args; -} - -const channelMutationArgs = { - sandboxName: sandboxNameArg, - channel: channelArg, -}; -const channelMutationFlags = { - help: Flags.help({ char: "h" }), - "dry-run": Flags.boolean({ description: "Preview the change without applying it" }), -}; - -export class ChannelsAddCommand extends Command { - static id = "sandbox:channels:add"; - static strict = true; - static summary = "Save messaging channel credentials and rebuild"; - static description = "Store credentials for a messaging channel and queue a sandbox rebuild."; - static usage = [" channels add [--dry-run]"]; - static examples = ["<%= config.bin %> alpha channels add telegram"]; - static args = channelMutationArgs; - static flags = channelMutationFlags; - - public async run(): Promise { - const { args, flags } = await this.parse(ChannelsAddCommand); - await getRuntimeBridge().sandboxChannelsAdd(args.sandboxName, buildArgs(args.channel, flags)); - } -} - -export class ChannelsRemoveCommand extends Command { - static id = "sandbox:channels:remove"; - static strict = true; - static summary = "Clear messaging channel credentials and rebuild"; - static description = "Remove credentials for a messaging channel and queue a sandbox rebuild."; - static usage = [" channels remove [--dry-run]"]; - static examples = ["<%= config.bin %> alpha channels remove slack --dry-run"]; - static args = channelMutationArgs; - static flags = channelMutationFlags; - - public async run(): Promise { - const { args, flags } = await this.parse(ChannelsRemoveCommand); - await getRuntimeBridge().sandboxChannelsRemove(args.sandboxName, buildArgs(args.channel, flags)); - } -} - -export class ChannelsStopCommand extends Command { - static id = "sandbox:channels:stop"; - static strict = true; - static summary = "Disable channel without wiping credentials"; - static description = "Disable a messaging channel while keeping credentials in the gateway."; - static usage = [" channels stop [--dry-run]"]; - static examples = ["<%= config.bin %> alpha channels stop discord"]; - static args = channelMutationArgs; - static flags = channelMutationFlags; - - public async run(): Promise { - const { args, flags } = await this.parse(ChannelsStopCommand); - await getRuntimeBridge().sandboxChannelsStop(args.sandboxName, buildArgs(args.channel, flags)); - } -} - -export class ChannelsStartCommand extends Command { - static id = "sandbox:channels:start"; - static strict = true; - static summary = "Re-enable a stopped messaging channel"; - static description = "Re-enable a previously stopped messaging channel."; - static usage = [" channels start [--dry-run]"]; - static examples = ["<%= config.bin %> alpha channels start discord"]; - static args = channelMutationArgs; - static flags = channelMutationFlags; - - public async run(): Promise { - const { args, flags } = await this.parse(ChannelsStartCommand); - await getRuntimeBridge().sandboxChannelsStart(args.sandboxName, buildArgs(args.channel, flags)); - } -} diff --git a/src/lib/cli/oclif-metadata.ts b/src/lib/cli/oclif-metadata.ts index 447994af39..faa5851492 100644 --- a/src/lib/cli/oclif-metadata.ts +++ b/src/lib/cli/oclif-metadata.ts @@ -15,9 +15,9 @@ export type OclifCommandMetadata = { function loadOclifCommands(): Record | null { for (const modulePath of [ - "../oclif-commands", - "../oclif-commands.js", - "../../../dist/lib/oclif-commands.js", + "../commands", + "../commands/index.js", + "../../../dist/lib/commands/index.js", ]) { try { const registry = require(modulePath) as { diff --git a/src/lib/command-display-metadata.test.ts b/src/lib/command-display-metadata.test.ts index d71eaf3b20..41d47c7543 100644 --- a/src/lib/command-display-metadata.test.ts +++ b/src/lib/command-display-metadata.test.ts @@ -4,7 +4,7 @@ import { describe, expect, it } from "vitest"; import { COMMANDS, visibleCommands } from "./command-registry"; -import commands from "../../dist/lib/oclif-commands.js"; +import commands from "../../dist/lib/commands/index.js"; describe("public command display metadata", () => { it("maps every command display entry to a registered oclif command", () => { diff --git a/src/lib/credentials-cli-command-source.test.ts b/src/lib/commands/credentials.test.ts similarity index 91% rename from src/lib/credentials-cli-command-source.test.ts rename to src/lib/commands/credentials.test.ts index 655c236e80..6c2156dfe8 100644 --- a/src/lib/credentials-cli-command-source.test.ts +++ b/src/lib/commands/credentials.test.ts @@ -9,17 +9,15 @@ const mocks = vi.hoisted(() => ({ runOpenshellProviderCommand: vi.fn(), })); -vi.mock("./credentials", () => ({ prompt: mocks.prompt })); -vi.mock("./global-cli-actions", () => ({ +vi.mock("../credentials", () => ({ prompt: mocks.prompt })); +vi.mock("../global-cli-actions", () => ({ recoverNamedGatewayRuntime: mocks.recoverNamedGatewayRuntime, runOpenshellProviderCommand: mocks.runOpenshellProviderCommand, })); -import { - CredentialsCommand, - CredentialsListCommand, - CredentialsResetCommand, -} from "./credentials-cli-command"; +import CredentialsCommand from "./credentials"; +import CredentialsListCommand from "./credentials/list"; +import CredentialsResetCommand from "./credentials/reset"; const rootDir = process.cwd(); diff --git a/src/lib/commands/credentials.ts b/src/lib/commands/credentials.ts new file mode 100644 index 0000000000..bbcb929016 --- /dev/null +++ b/src/lib/commands/credentials.ts @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Command, Flags } from "@oclif/core"; + +import { printCredentialsUsage } from "./credentials/common"; + +export default class CredentialsCommand extends Command { + static id = "credentials"; + static strict = true; + static summary = "Manage provider credentials"; + static description = + "List or reset provider credentials registered with the OpenShell gateway."; + static usage = ["credentials "]; + static examples = [ + "<%= config.bin %> credentials list", + "<%= config.bin %> credentials reset nvidia-prod --yes", + ]; + static flags = { + help: Flags.help({ char: "h" }), + }; + + public async run(): Promise { + await this.parse(CredentialsCommand); + printCredentialsUsage(this.log.bind(this)); + } +} diff --git a/src/lib/commands/credentials/common.ts b/src/lib/commands/credentials/common.ts new file mode 100644 index 0000000000..2fb9c7dcc1 --- /dev/null +++ b/src/lib/commands/credentials/common.ts @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { CLI_DISPLAY_NAME, CLI_NAME } from "../../branding"; +import { recoverNamedGatewayRuntime } from "../../global-cli-actions"; + +// Suffixes that mark per-sandbox messaging integrations in the gateway's +// provider list. These are managed by `channels`, not `credentials`. +const BRIDGE_PROVIDER_SUFFIXES: readonly string[] = [ + "-telegram-bridge", + "-discord-bridge", + "-slack-bridge", + "-slack-app", +]; + +export function isBridgeProviderName(name: string): boolean { + return BRIDGE_PROVIDER_SUFFIXES.some((suffix) => name.endsWith(suffix)); +} + +export function printCredentialsUsage(log: (message?: string) => void = console.log): void { + log(""); + log(` Usage: ${CLI_NAME} credentials `); + log(""); + log(" Subcommands:"); + log(" list List provider credentials registered with the OpenShell gateway"); + log(" reset [--yes] Remove a provider credential so onboard re-prompts"); + log(""); + log(" Credentials live in the OpenShell gateway. Inspect with `openshell provider list`."); + log(" Nothing is persisted to host disk; deploy/non-onboard commands read from env vars."); + log(""); +} + +export async function recoverGatewayOrExit(kind: "query" | "reach"): Promise { + const recovery = await recoverNamedGatewayRuntime(); + if (recovery.recovered) return; + + if (kind === "query") { + console.error(` Could not query the ${CLI_DISPLAY_NAME} OpenShell gateway. Is it running?`); + } else { + console.error(` Could not reach the ${CLI_DISPLAY_NAME} OpenShell gateway. Is it running?`); + } + console.error(` Run 'openshell gateway start --name nemoclaw' or '${CLI_NAME} onboard' first.`); + process.exit(1); +} diff --git a/src/lib/commands/credentials/list.ts b/src/lib/commands/credentials/list.ts new file mode 100644 index 0000000000..73d9b27287 --- /dev/null +++ b/src/lib/commands/credentials/list.ts @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Command, Flags } from "@oclif/core"; + +import { CLI_NAME } from "../../branding"; +import { runOpenshellProviderCommand } from "../../global-cli-actions"; +import { OPENSHELL_OPERATION_TIMEOUT_MS } from "../../openshell-timeouts"; +import { isBridgeProviderName, recoverGatewayOrExit } from "./common"; + +export default class CredentialsListCommand extends Command { + static id = "credentials:list"; + static strict = true; + static summary = "List stored credential providers"; + static description = "List provider credentials registered with the OpenShell gateway."; + static usage = ["credentials list"]; + static examples = ["<%= config.bin %> credentials list"]; + static flags = { + help: Flags.help({ char: "h" }), + }; + + public async run(): Promise { + await this.parse(CredentialsListCommand); + await recoverGatewayOrExit("query"); + + const result = runOpenshellProviderCommand(["provider", "list", "--names"], { + ignoreError: true, + stdio: ["ignore", "pipe", "pipe"], + timeout: OPENSHELL_OPERATION_TIMEOUT_MS, + }); + if (result.status !== 0) { + console.error(" Could not query OpenShell gateway. Is it running?"); + console.error(` Run 'openshell gateway start --name nemoclaw' or '${CLI_NAME} onboard' first.`); + process.exit(1); + } + + const allNames = String(result.stdout || "") + .split("\n") + .map((name) => name.trim()) + .filter((name) => name.length > 0); + const credentialNames = allNames.filter((name) => !isBridgeProviderName(name)).sort(); + const bridgeNames = allNames.filter((name) => isBridgeProviderName(name)); + + if (credentialNames.length === 0) { + this.log(" No provider credentials registered."); + } else { + this.log(" Providers registered with the OpenShell gateway:"); + for (const name of credentialNames) { + this.log(` ${name}`); + } + } + if (bridgeNames.length > 0) { + this.log(""); + this.log(` ${String(bridgeNames.length)} per-sandbox messaging bridge(s) are also registered.`); + this.log(` Manage those with \`${CLI_NAME} channels list/remove/stop\` — not this command.`); + } + } +} diff --git a/src/lib/commands/credentials/reset.ts b/src/lib/commands/credentials/reset.ts new file mode 100644 index 0000000000..d02418afc5 --- /dev/null +++ b/src/lib/commands/credentials/reset.ts @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Args, Command, Flags } from "@oclif/core"; + +import { CLI_NAME } from "../../branding"; +import { prompt as askPrompt } from "../../credentials"; +import { runOpenshellProviderCommand } from "../../global-cli-actions"; +import { OPENSHELL_OPERATION_TIMEOUT_MS } from "../../openshell-timeouts"; +import { isBridgeProviderName, recoverGatewayOrExit } from "./common"; + +export default class CredentialsResetCommand extends Command { + static id = "credentials:reset"; + static strict = true; + static summary = "Remove a provider credential"; + static description = "Remove a provider credential so onboard re-prompts for it."; + static usage = ["credentials reset [--yes]"]; + static examples = [ + "<%= config.bin %> credentials reset nvidia-prod", + "<%= config.bin %> credentials reset nvidia-prod --yes", + ]; + static args = { + provider: Args.string({ + name: "PROVIDER", + description: "OpenShell provider name", + required: false, + }), + }; + static flags = { + help: Flags.help({ char: "h" }), + yes: Flags.boolean({ char: "y", description: "Skip the confirmation prompt" }), + }; + + public async run(): Promise { + const { args, flags } = await this.parse(CredentialsResetCommand); + const key = args.provider; + + if (!key || key.startsWith("-")) { + console.error(` Usage: ${CLI_NAME} credentials reset [--yes]`); + console.error(` PROVIDER is an OpenShell provider name. Run '${CLI_NAME} credentials list' first.`); + process.exit(1); + } + + if (isBridgeProviderName(key)) { + console.error(` '${key}' is a per-sandbox messaging bridge, not a credential.`); + console.error( + ` Use \`${CLI_NAME} channels remove \` to retire`, + ); + console.error(" the integration (it tears down the bridge provider and rebuilds the sandbox),"); + console.error(` or \`${CLI_NAME} channels stop <…>\` to pause it without clearing tokens.`); + process.exit(1); + } + + if (!flags.yes) { + const answer = (await askPrompt(` Remove provider '${key}' from the OpenShell gateway? [y/N]: `)) + .trim() + .toLowerCase(); + if (answer !== "y" && answer !== "yes") { + this.log(" Cancelled."); + return; + } + } + + await recoverGatewayOrExit("reach"); + + const result = runOpenshellProviderCommand(["provider", "delete", key], { + ignoreError: true, + stdio: ["ignore", "pipe", "pipe"], + timeout: OPENSHELL_OPERATION_TIMEOUT_MS, + }); + if (result.status === 0) { + this.log(` Removed provider '${key}' from the OpenShell gateway.`); + this.log(` Re-run '${CLI_NAME} onboard' to enter a new value.`); + return; + } + + console.error(` Could not remove provider '${key}'.`); + if (/^[A-Z][A-Z0-9_]+$/.test(key)) { + console.error(""); + console.error(` '${key}' looks like a credential env variable name.`); + console.error(" As of this release, 'credentials reset' takes an OpenShell"); + console.error(` provider name. Run '${CLI_NAME} credentials list' to see the`); + console.error(" registered providers, then retry with one of those names."); + } + const stderr = String(result.stderr || "").trim(); + if (stderr) console.error(` ${stderr}`); + process.exit(1); + } +} diff --git a/src/lib/debug-cli-command.ts b/src/lib/commands/debug.ts similarity index 84% rename from src/lib/debug-cli-command.ts rename to src/lib/commands/debug.ts index f7d606877d..6a1a7f6771 100644 --- a/src/lib/debug-cli-command.ts +++ b/src/lib/commands/debug.ts @@ -3,17 +3,17 @@ import { Command, Flags } from "@oclif/core"; -import { CLI_NAME } from "./branding"; -import { runDebug } from "./debug"; -import type { DebugOptions } from "./debug"; -import type { RunDebugCommandDeps } from "./debug-command"; -import { runDebugCommandWithOptions } from "./debug-command"; -import type { CaptureOpenshellResult } from "./openshell"; -import { captureOpenshellCommand } from "./openshell"; -import { OPENSHELL_PROBE_TIMEOUT_MS } from "./openshell-timeouts"; -import * as registry from "./registry"; -import { resolveOpenshell } from "./resolve-openshell"; -import { parseLiveSandboxNames } from "./runtime-recovery"; +import { CLI_NAME } from "../branding"; +import { runDebug } from "../debug"; +import type { DebugOptions } from "../debug"; +import type { RunDebugCommandDeps } from "../debug-command"; +import { runDebugCommandWithOptions } from "../debug-command"; +import type { CaptureOpenshellResult } from "../openshell"; +import { captureOpenshellCommand } from "../openshell"; +import { OPENSHELL_PROBE_TIMEOUT_MS } from "../openshell-timeouts"; +import * as registry from "../registry"; +import { resolveOpenshell } from "../resolve-openshell"; +import { parseLiveSandboxNames } from "../runtime-recovery"; const useColor = !process.env.NO_COLOR && !!process.stderr.isTTY; const B = useColor ? "\x1b[1m" : ""; diff --git a/src/lib/deploy-cli-command.ts b/src/lib/commands/deploy.ts similarity index 94% rename from src/lib/deploy-cli-command.ts rename to src/lib/commands/deploy.ts index 854f365084..31d460f81e 100644 --- a/src/lib/deploy-cli-command.ts +++ b/src/lib/commands/deploy.ts @@ -3,7 +3,7 @@ import { Args, Command, Flags } from "@oclif/core"; -import { runDeployAction } from "./global-cli-actions"; +import { runDeployAction } from "../global-cli-actions"; export default class DeployCliCommand extends Command { static id = "deploy"; diff --git a/src/lib/commands/deprecated/start.ts b/src/lib/commands/deprecated/start.ts new file mode 100644 index 0000000000..cf93d20906 --- /dev/null +++ b/src/lib/commands/deprecated/start.ts @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Command, Flags } from "@oclif/core"; + +import { CLI_NAME } from "../../branding"; +import { startAll } from "../../services"; +import { runStartCommand } from "../../services-command"; +import { serviceDeps } from "../tunnel/common"; + +export default class DeprecatedStartCommand extends Command { + static id = "start"; + static strict = true; + static summary = "Deprecated alias for 'tunnel start'"; + static description = "Deprecated alias for tunnel start."; + static usage = ["start"]; + static examples = ["<%= config.bin %> start"]; + static flags = { + help: Flags.help({ char: "h" }), + }; + + public async run(): Promise { + await this.parse(DeprecatedStartCommand); + this.logToStderr( + ` Deprecated: '${CLI_NAME} start' is now '${CLI_NAME} tunnel start'. See '${CLI_NAME} help'.`, + ); + await runStartCommand({ ...serviceDeps(), startAll }); + } +} diff --git a/src/lib/commands/deprecated/stop.ts b/src/lib/commands/deprecated/stop.ts new file mode 100644 index 0000000000..61cbe01e94 --- /dev/null +++ b/src/lib/commands/deprecated/stop.ts @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Command, Flags } from "@oclif/core"; + +import { CLI_NAME } from "../../branding"; +import { stopAll } from "../../services"; +import { runStopCommand } from "../../services-command"; +import { serviceDeps } from "../tunnel/common"; + +export default class DeprecatedStopCommand extends Command { + static id = "stop"; + static strict = true; + static summary = "Deprecated alias for 'tunnel stop'"; + static description = "Deprecated alias for tunnel stop."; + static usage = ["stop"]; + static examples = ["<%= config.bin %> stop"]; + static flags = { + help: Flags.help({ char: "h" }), + }; + + public async run(): Promise { + await this.parse(DeprecatedStopCommand); + this.logToStderr( + ` Deprecated: '${CLI_NAME} stop' is now '${CLI_NAME} tunnel stop'. See '${CLI_NAME} help'.`, + ); + runStopCommand({ ...serviceDeps(), stopAll }); + } +} diff --git a/src/lib/gateway-token-cli-command.ts b/src/lib/commands/gateway-token.ts similarity index 94% rename from src/lib/gateway-token-cli-command.ts rename to src/lib/commands/gateway-token.ts index 901fb5aa5b..28e3f6076a 100644 --- a/src/lib/gateway-token-cli-command.ts +++ b/src/lib/commands/gateway-token.ts @@ -3,7 +3,7 @@ import { Args, Command, Flags } from "@oclif/core"; -import { runGatewayTokenCommand } from "./gateway-token-command"; +import { runGatewayTokenCommand } from "../gateway-token-command"; type GatewayTokenRuntimeBridge = { fetchGatewayAuthTokenFromSandbox: (sandboxName: string) => string | null; @@ -11,7 +11,7 @@ type GatewayTokenRuntimeBridge = { /* v8 ignore next -- source tests inject this bridge; CLI subprocess tests cover the real onboard module. */ let runtimeBridgeFactory = (): GatewayTokenRuntimeBridge => { - const onboard = require("./onboard") as GatewayTokenRuntimeBridge; + const onboard = require("../onboard") as GatewayTokenRuntimeBridge; return { fetchGatewayAuthTokenFromSandbox: onboard.fetchGatewayAuthTokenFromSandbox }; }; diff --git a/src/lib/global-oclif-command-adapters.test.ts b/src/lib/commands/global-oclif-command-adapters.test.ts similarity index 86% rename from src/lib/global-oclif-command-adapters.test.ts rename to src/lib/commands/global-oclif-command-adapters.test.ts index 45dc44f70c..7ae8255b66 100644 --- a/src/lib/global-oclif-command-adapters.test.ts +++ b/src/lib/commands/global-oclif-command-adapters.test.ts @@ -18,22 +18,22 @@ const mocks = vi.hoisted(() => ({ showStatusCommand: vi.fn(), })); -vi.mock("./inventory-commands", () => ({ +vi.mock("../inventory-commands", () => ({ getSandboxInventory: mocks.getSandboxInventory, getStatusReport: mocks.getStatusReport, renderSandboxInventoryText: mocks.renderSandboxInventoryText, showStatusCommand: mocks.showStatusCommand, })); -vi.mock("./list-command-deps", () => ({ +vi.mock("../list-command-deps", () => ({ buildListCommandDeps: mocks.buildListCommandDeps, })); -vi.mock("./status-command-deps", () => ({ +vi.mock("../status-command-deps", () => ({ buildStatusCommandDeps: mocks.buildStatusCommandDeps, })); -vi.mock("./global-cli-actions", () => ({ +vi.mock("../global-cli-actions", () => ({ runBackupAllAction: mocks.runBackupAllAction, runGarbageCollectImagesAction: mocks.runGarbageCollectImagesAction, runOnboardAction: mocks.runOnboardAction, @@ -42,14 +42,14 @@ vi.mock("./global-cli-actions", () => ({ runUpgradeSandboxesAction: mocks.runUpgradeSandboxesAction, })); -import ListCommand from "./list-command"; -import { - BackupAllCommand, - GarbageCollectImagesCommand, - UpgradeSandboxesCommand, -} from "./maintenance-cli-commands"; -import { OnboardCliCommand, SetupCliCommand, SetupSparkCliCommand } from "./onboard-cli-commands"; -import StatusCommand from "./status-command"; +import ListCommand from "./list"; +import BackupAllCommand from "./maintenance/backup-all"; +import GarbageCollectImagesCommand from "./maintenance/gc"; +import UpgradeSandboxesCommand from "./maintenance/upgrade-sandboxes"; +import OnboardCliCommand from "./onboard"; +import SetupCliCommand from "./setup"; +import SetupSparkCliCommand from "./setup-spark"; +import StatusCommand from "./status"; const rootDir = process.cwd(); diff --git a/src/lib/commands/index.ts b/src/lib/commands/index.ts new file mode 100644 index 0000000000..a1878f268c --- /dev/null +++ b/src/lib/commands/index.ts @@ -0,0 +1,108 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import BackupAllCommand from "./maintenance/backup-all"; +import ChannelsAddCommand from "./sandbox/channels/add"; +import ChannelsListCommand from "./sandbox/channels/list"; +import ChannelsRemoveCommand from "./sandbox/channels/remove"; +import ChannelsStartCommand from "./sandbox/channels/start"; +import ChannelsStopCommand from "./sandbox/channels/stop"; +import ConnectCliCommand from "./sandbox/connect"; +import CredentialsCommand from "./credentials"; +import CredentialsListCommand from "./credentials/list"; +import CredentialsResetCommand from "./credentials/reset"; +import DebugCliCommand from "./debug"; +import DeprecatedStartCommand from "./deprecated/start"; +import DeprecatedStopCommand from "./deprecated/stop"; +import DeployCliCommand from "./deploy"; +import DestroyCliCommand from "./sandbox/destroy"; +import GarbageCollectImagesCommand from "./maintenance/gc"; +import GatewayTokenCliCommand from "./gateway-token"; +import ListCommand from "./list"; +import OnboardCliCommand from "./onboard"; +import PolicyAddCommand from "./sandbox/policy/add"; +import PolicyListCommand from "./sandbox/policy/list"; +import PolicyRemoveCommand from "./sandbox/policy/remove"; +import RecoverCliCommand from "../recover-cli-command"; +import RebuildCliCommand from "./sandbox/rebuild"; +import RootHelpCommand from "./root/help"; +import SandboxConfigGetCommand from "./sandbox/config/get"; +import SandboxConfigSetCommand from "../sandbox-config-set-cli-command"; +import SandboxDoctorCliCommand from "./sandbox/doctor"; +import SandboxLogsCommand from "./sandbox/logs"; +import SandboxStatusCommand from "./sandbox/status"; +import SetupCliCommand from "./setup"; +import SetupSparkCliCommand from "./setup-spark"; +import ShareCommand from "./sandbox/share"; +import ShareMountCommand from "./sandbox/share/mount"; +import ShareStatusCommand from "./sandbox/share/status"; +import ShareUnmountCommand from "./sandbox/share/unmount"; +import ShieldsDownCommand from "./sandbox/shields/down"; +import ShieldsStatusCommand from "./sandbox/shields/status"; +import ShieldsUpCommand from "./sandbox/shields/up"; +import SkillCliCommand from "./sandbox/skill"; +import SkillInstallCliCommand from "./sandbox/skill/install"; +import SnapshotCommand from "./sandbox/snapshot"; +import SnapshotCreateCommand from "./sandbox/snapshot/create"; +import SnapshotListCommand from "./sandbox/snapshot/list"; +import SnapshotRestoreCommand from "./sandbox/snapshot/restore"; +import StatusCommand from "./status"; +import TunnelStartCommand from "./tunnel/start"; +import TunnelStopCommand from "./tunnel/stop"; +import UninstallCliCommand from "./uninstall"; +import UpgradeSandboxesCommand from "./maintenance/upgrade-sandboxes"; +import VersionCommand from "./root/version"; + +export default { + "backup-all": BackupAllCommand, + credentials: CredentialsCommand, + "credentials:list": CredentialsListCommand, + "credentials:reset": CredentialsResetCommand, + debug: DebugCliCommand, + deploy: DeployCliCommand, + list: ListCommand, + onboard: OnboardCliCommand, + "root:help": RootHelpCommand, + "root:version": VersionCommand, + "sandbox:channels:add": ChannelsAddCommand, + "sandbox:channels:list": ChannelsListCommand, + "sandbox:channels:remove": ChannelsRemoveCommand, + "sandbox:channels:start": ChannelsStartCommand, + "sandbox:channels:stop": ChannelsStopCommand, + "sandbox:config:get": SandboxConfigGetCommand, + "sandbox:config:set": SandboxConfigSetCommand, + "sandbox:connect": ConnectCliCommand, + "sandbox:destroy": DestroyCliCommand, + "sandbox:doctor": SandboxDoctorCliCommand, + "sandbox:gateway:token": GatewayTokenCliCommand, + "sandbox:logs": SandboxLogsCommand, + "sandbox:policy:add": PolicyAddCommand, + "sandbox:policy:list": PolicyListCommand, + "sandbox:policy:remove": PolicyRemoveCommand, + "sandbox:rebuild": RebuildCliCommand, + "sandbox:recover": RecoverCliCommand, + "sandbox:share": ShareCommand, + "sandbox:share:mount": ShareMountCommand, + "sandbox:share:status": ShareStatusCommand, + "sandbox:share:unmount": ShareUnmountCommand, + "sandbox:shields:down": ShieldsDownCommand, + "sandbox:shields:status": ShieldsStatusCommand, + "sandbox:shields:up": ShieldsUpCommand, + "sandbox:skill": SkillCliCommand, + "sandbox:skill:install": SkillInstallCliCommand, + "sandbox:snapshot": SnapshotCommand, + "sandbox:snapshot:create": SnapshotCreateCommand, + "sandbox:snapshot:list": SnapshotListCommand, + "sandbox:snapshot:restore": SnapshotRestoreCommand, + "sandbox:status": SandboxStatusCommand, + setup: SetupCliCommand, + "setup-spark": SetupSparkCliCommand, + status: StatusCommand, + start: DeprecatedStartCommand, + stop: DeprecatedStopCommand, + "tunnel:start": TunnelStartCommand, + "tunnel:stop": TunnelStopCommand, + gc: GarbageCollectImagesCommand, + uninstall: UninstallCliCommand, + "upgrade-sandboxes": UpgradeSandboxesCommand, +}; diff --git a/src/lib/list-command.ts b/src/lib/commands/list.ts similarity index 87% rename from src/lib/list-command.ts rename to src/lib/commands/list.ts index ba8a654492..bd136beb1f 100644 --- a/src/lib/list-command.ts +++ b/src/lib/commands/list.ts @@ -1,9 +1,9 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { getSandboxInventory, renderSandboxInventoryText } from "./inventory-commands"; -import { NemoClawCommand } from "./cli/nemoclaw-oclif-command"; -import { buildListCommandDeps } from "./list-command-deps"; +import { getSandboxInventory, renderSandboxInventoryText } from "../inventory-commands"; +import { NemoClawCommand } from "../cli/nemoclaw-oclif-command"; +import { buildListCommandDeps } from "../list-command-deps"; export default class ListCommand extends NemoClawCommand { static id = "list"; diff --git a/src/lib/commands/maintenance/backup-all.ts b/src/lib/commands/maintenance/backup-all.ts new file mode 100644 index 0000000000..e1ac85d944 --- /dev/null +++ b/src/lib/commands/maintenance/backup-all.ts @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { runBackupAllAction } from "../../global-cli-actions"; +import { NemoClawCommand } from "../../cli/nemoclaw-oclif-command"; + +export default class BackupAllCommand extends NemoClawCommand { + static id = "backup-all"; + static strict = true; + static summary = "Back up all sandbox state before upgrade"; + static description = "Back up registered, running sandbox state before upgrading."; + static usage = ["backup-all"]; + static examples = ["<%= config.bin %> backup-all"]; + static flags = {}; + + public async run(): Promise { + await this.parse(BackupAllCommand); + runBackupAllAction(); + } +} diff --git a/src/lib/commands/maintenance/gc.ts b/src/lib/commands/maintenance/gc.ts new file mode 100644 index 0000000000..5af768a3e4 --- /dev/null +++ b/src/lib/commands/maintenance/gc.ts @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Flags } from "@oclif/core"; + +import { runGarbageCollectImagesAction } from "../../global-cli-actions"; +import { NemoClawCommand } from "../../cli/nemoclaw-oclif-command"; + +export default class GarbageCollectImagesCommand extends NemoClawCommand { + static id = "gc"; + static strict = true; + static summary = "Remove orphaned sandbox Docker images"; + static description = "Remove sandbox Docker images that are not referenced by registered sandboxes."; + static usage = ["gc [--dry-run] [--yes|-y|--force]"]; + static examples = ["<%= config.bin %> gc --dry-run", "<%= config.bin %> gc --yes"]; + static flags = { + "dry-run": Flags.boolean({ description: "Show images that would be removed without deleting" }), + yes: Flags.boolean({ char: "y", description: "Skip the confirmation prompt" }), + force: Flags.boolean({ description: "Skip the confirmation prompt" }), + }; + + public async run(): Promise { + const { flags } = await this.parse(GarbageCollectImagesCommand); + await runGarbageCollectImagesAction({ + dryRun: flags["dry-run"] === true, + force: flags.force === true, + yes: flags.yes === true, + }); + } +} diff --git a/src/lib/commands/maintenance/upgrade-sandboxes.ts b/src/lib/commands/maintenance/upgrade-sandboxes.ts new file mode 100644 index 0000000000..094ddf7f2f --- /dev/null +++ b/src/lib/commands/maintenance/upgrade-sandboxes.ts @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Flags } from "@oclif/core"; + +import { runUpgradeSandboxesAction } from "../../global-cli-actions"; +import { NemoClawCommand } from "../../cli/nemoclaw-oclif-command"; + +export default class UpgradeSandboxesCommand extends NemoClawCommand { + static id = "upgrade-sandboxes"; + static strict = true; + static summary = "Detect and rebuild stale sandboxes"; + static description = "Detect stale sandboxes and optionally rebuild them."; + static usage = ["upgrade-sandboxes [--check] [--auto] [--yes|-y]"]; + static examples = [ + "<%= config.bin %> upgrade-sandboxes --check", + "<%= config.bin %> upgrade-sandboxes --auto --yes", + ]; + static flags = { + check: Flags.boolean({ description: "Only check whether sandboxes need upgrading" }), + auto: Flags.boolean({ description: "Automatically rebuild running stale sandboxes" }), + yes: Flags.boolean({ char: "y", description: "Skip confirmation prompts" }), + }; + + public async run(): Promise { + const { flags } = await this.parse(UpgradeSandboxesCommand); + await runUpgradeSandboxesAction({ + auto: flags.auto === true, + check: flags.check === true, + yes: flags.yes === true, + }); + } +} diff --git a/src/lib/onboard-cli-commands.test.ts b/src/lib/commands/onboard.test.ts similarity index 82% rename from src/lib/onboard-cli-commands.test.ts rename to src/lib/commands/onboard.test.ts index 94c02a1554..29eda79606 100644 --- a/src/lib/onboard-cli-commands.test.ts +++ b/src/lib/commands/onboard.test.ts @@ -3,10 +3,10 @@ import { describe, expect, it, vi } from "vitest"; -import { OnboardCliCommand } from "./onboard-cli-commands"; -import { runOnboardAction } from "./global-cli-actions"; +import { runOnboardAction } from "../global-cli-actions"; +import OnboardCliCommand from "./onboard"; -vi.mock("./global-cli-actions", () => ({ +vi.mock("../global-cli-actions", () => ({ runOnboardAction: vi.fn().mockResolvedValue(undefined), runSetupAction: vi.fn().mockResolvedValue(undefined), runSetupSparkAction: vi.fn().mockResolvedValue(undefined), diff --git a/src/lib/commands/onboard.ts b/src/lib/commands/onboard.ts new file mode 100644 index 0000000000..090614bce7 --- /dev/null +++ b/src/lib/commands/onboard.ts @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Command } from "@oclif/core"; + +import { runOnboardAction } from "../global-cli-actions"; +import { + buildOnboardFlags, + onboardExamples, + type OnboardFlags, + onboardUsage, + toLegacyOnboardArgs, +} from "./onboard/common"; + +export default class OnboardCliCommand extends Command { + static id = "onboard"; + static strict = true; + static summary = "Configure inference endpoint and credentials"; + static description = "Configure inference, credentials, and sandbox settings."; + static usage = onboardUsage; + static examples = onboardExamples; + static flags = buildOnboardFlags(); + + public async run(): Promise { + const { flags } = await this.parse(OnboardCliCommand); + await runOnboardAction(toLegacyOnboardArgs(flags as OnboardFlags)); + } +} diff --git a/src/lib/onboard-cli-commands.ts b/src/lib/commands/onboard/common.ts similarity index 54% rename from src/lib/onboard-cli-commands.ts rename to src/lib/commands/onboard/common.ts index bc0d8ab809..0f065431dc 100644 --- a/src/lib/onboard-cli-commands.ts +++ b/src/lib/commands/onboard/common.ts @@ -1,18 +1,17 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { Command, Flags } from "@oclif/core"; +import { Flags } from "@oclif/core"; -import { runOnboardAction, runSetupAction, runSetupSparkAction } from "./global-cli-actions"; -import { NOTICE_ACCEPT_FLAG } from "./usage-notice"; +import { NOTICE_ACCEPT_FLAG } from "../../usage-notice"; const acceptFlagName = NOTICE_ACCEPT_FLAG.replace(/^--/, ""); -const onboardUsage = [ +export const onboardUsage = [ `onboard [--non-interactive] [--resume | --fresh] [--recreate-sandbox] [--from ] [--name ] [--agent ] [--control-ui-port ] [--yes | -y] [${NOTICE_ACCEPT_FLAG}]`, ]; -const onboardExamples = [ +export const onboardExamples = [ "<%= config.bin %> onboard", "<%= config.bin %> onboard --name alpha", "<%= config.bin %> onboard --resume", @@ -21,7 +20,7 @@ const onboardExamples = [ `<%= config.bin %> onboard --non-interactive --name alpha ${NOTICE_ACCEPT_FLAG}`, ]; -type OnboardFlags = { +export type OnboardFlags = { "non-interactive"?: boolean; resume?: boolean; fresh?: boolean; @@ -34,7 +33,7 @@ type OnboardFlags = { [acceptFlagName]?: boolean; }; -function buildOnboardFlags(): Record { +export function buildOnboardFlags(): Record { return { help: Flags.help({ char: "h" }), "non-interactive": Flags.boolean({ description: "Run without interactive prompts" }), @@ -63,7 +62,7 @@ function buildOnboardFlags(): Record { } as Record; } -function toLegacyOnboardArgs(flags: OnboardFlags): string[] { +export function toLegacyOnboardArgs(flags: OnboardFlags): string[] { const args: string[] = []; if (flags["non-interactive"]) args.push("--non-interactive"); if (flags.resume) args.push("--resume"); @@ -79,56 +78,3 @@ function toLegacyOnboardArgs(flags: OnboardFlags): string[] { if (flags[acceptFlagName]) args.push(NOTICE_ACCEPT_FLAG); return args; } - -export class OnboardCliCommand extends Command { - static id = "onboard"; - static strict = true; - static summary = "Configure inference endpoint and credentials"; - static description = "Configure inference, credentials, and sandbox settings."; - static usage = onboardUsage; - static examples = onboardExamples; - static flags = buildOnboardFlags(); - - public async run(): Promise { - const { flags } = await this.parse(OnboardCliCommand); - await runOnboardAction(toLegacyOnboardArgs(flags as OnboardFlags)); - } -} - -export class SetupCliCommand extends Command { - static id = "setup"; - 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 { - 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 = 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 { - 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/src/lib/commands/root/help.ts b/src/lib/commands/root/help.ts new file mode 100644 index 0000000000..7200337fe6 --- /dev/null +++ b/src/lib/commands/root/help.ts @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Command } from "@oclif/core"; + +import { showRootHelp } from "../../global-cli-actions"; + +export default class RootHelpCommand extends Command { + static id = "root:help"; + static hidden = true; + static strict = false; + static summary = "Show help"; + + public async run(): Promise { + this.parsed = true; + showRootHelp(); + } +} diff --git a/src/lib/help-version-cli-commands.ts b/src/lib/commands/root/version.ts similarity index 51% rename from src/lib/help-version-cli-commands.ts rename to src/lib/commands/root/version.ts index 7b65966abb..7d098b80b9 100644 --- a/src/lib/help-version-cli-commands.ts +++ b/src/lib/commands/root/version.ts @@ -3,21 +3,9 @@ import { Command } from "@oclif/core"; -import { showRootHelp, showVersion } from "./global-cli-actions"; +import { showVersion } from "../../global-cli-actions"; -export class RootHelpCommand extends Command { - static id = "root:help"; - static hidden = true; - static strict = false; - static summary = "Show help"; - - public async run(): Promise { - this.parsed = true; - showRootHelp(); - } -} - -export class VersionCommand extends Command { +export default class VersionCommand extends Command { static id = "root:version"; static hidden = true; static strict = true; diff --git a/src/lib/commands/sandbox/channels/add.ts b/src/lib/commands/sandbox/channels/add.ts new file mode 100644 index 0000000000..863f53c67b --- /dev/null +++ b/src/lib/commands/sandbox/channels/add.ts @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Command } from "@oclif/core"; + +import { + buildChannelArgs, + channelMutationArgs, + channelMutationFlags, + getChannelsRuntimeBridge, +} from "./common"; + +export default class ChannelsAddCommand extends Command { + static id = "sandbox:channels:add"; + static strict = true; + static summary = "Save messaging channel credentials and rebuild"; + static description = "Store credentials for a messaging channel and queue a sandbox rebuild."; + static usage = [" channels add [--dry-run]"]; + static examples = ["<%= config.bin %> alpha channels add telegram"]; + static args = channelMutationArgs; + static flags = channelMutationFlags; + + public async run(): Promise { + const { args, flags } = await this.parse(ChannelsAddCommand); + await getChannelsRuntimeBridge().sandboxChannelsAdd( + args.sandboxName, + buildChannelArgs(args.channel, flags), + ); + } +} diff --git a/src/lib/commands/sandbox/channels/common.ts b/src/lib/commands/sandbox/channels/common.ts new file mode 100644 index 0000000000..e2e3dd2329 --- /dev/null +++ b/src/lib/commands/sandbox/channels/common.ts @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Args, Flags } from "@oclif/core"; + +type ChannelsRuntimeBridge = { + sandboxChannelsAdd: (sandboxName: string, args?: string[]) => Promise; + sandboxChannelsRemove: (sandboxName: string, args?: string[]) => Promise; + sandboxChannelsStart: (sandboxName: string, args?: string[]) => Promise; + sandboxChannelsStop: (sandboxName: string, args?: string[]) => Promise; +}; + +let runtimeBridgeFactory = (): ChannelsRuntimeBridge => { + const actions = require("../../../policy-channel-actions") as { + addSandboxChannel: ChannelsRuntimeBridge["sandboxChannelsAdd"]; + removeSandboxChannel: ChannelsRuntimeBridge["sandboxChannelsRemove"]; + startSandboxChannel: ChannelsRuntimeBridge["sandboxChannelsStart"]; + stopSandboxChannel: ChannelsRuntimeBridge["sandboxChannelsStop"]; + }; + return { + sandboxChannelsAdd: actions.addSandboxChannel, + sandboxChannelsRemove: actions.removeSandboxChannel, + sandboxChannelsStart: actions.startSandboxChannel, + sandboxChannelsStop: actions.stopSandboxChannel, + }; +}; + +export function setChannelsRuntimeBridgeFactoryForTest( + factory: () => ChannelsRuntimeBridge, +): void { + runtimeBridgeFactory = factory; +} + +export function getChannelsRuntimeBridge(): ChannelsRuntimeBridge { + return runtimeBridgeFactory(); +} + +const sandboxNameArg = Args.string({ name: "sandbox", description: "Sandbox name", required: true }); +const channelArg = Args.string({ name: "channel", description: "Messaging channel", required: true }); + +export function buildChannelArgs( + channel: string | undefined, + flags: { "dry-run"?: boolean }, +): string[] { + const args: string[] = []; + if (channel) args.push(channel); + if (flags["dry-run"]) args.push("--dry-run"); + return args; +} + +export const channelMutationArgs = { + sandboxName: sandboxNameArg, + channel: channelArg, +}; + +export const channelMutationFlags = { + help: Flags.help({ char: "h" }), + "dry-run": Flags.boolean({ description: "Preview the change without applying it" }), +}; diff --git a/src/lib/commands/sandbox/channels/list.ts b/src/lib/commands/sandbox/channels/list.ts new file mode 100644 index 0000000000..acafea2c22 --- /dev/null +++ b/src/lib/commands/sandbox/channels/list.ts @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Command, Flags } from "@oclif/core"; + +import { listSandboxChannels } from "../../../policy-channel-actions"; +import { sandboxNameArg } from "../common"; + +export default class SandboxChannelsListCommand extends Command { + static id = "sandbox:channels:list"; + static strict = true; + 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, + }; + static flags = { + help: Flags.help({ char: "h" }), + }; + + public async run(): Promise { + const { args } = await this.parse(SandboxChannelsListCommand); + listSandboxChannels(args.sandboxName); + } +} diff --git a/src/lib/channels-mutate-cli-commands.test.ts b/src/lib/commands/sandbox/channels/mutate.test.ts similarity index 90% rename from src/lib/channels-mutate-cli-commands.test.ts rename to src/lib/commands/sandbox/channels/mutate.test.ts index bb7ec65a7f..7b36bdd475 100644 --- a/src/lib/channels-mutate-cli-commands.test.ts +++ b/src/lib/commands/sandbox/channels/mutate.test.ts @@ -3,13 +3,11 @@ import { describe, expect, it, vi } from "vitest"; -import { - ChannelsAddCommand, - ChannelsRemoveCommand, - ChannelsStartCommand, - ChannelsStopCommand, - setChannelsRuntimeBridgeFactoryForTest, -} from "./channels-mutate-cli-commands"; +import ChannelsAddCommand from "./add"; +import { setChannelsRuntimeBridgeFactoryForTest } from "./common"; +import ChannelsRemoveCommand from "./remove"; +import ChannelsStartCommand from "./start"; +import ChannelsStopCommand from "./stop"; const rootDir = process.cwd(); diff --git a/src/lib/commands/sandbox/channels/remove.ts b/src/lib/commands/sandbox/channels/remove.ts new file mode 100644 index 0000000000..8d464a3a92 --- /dev/null +++ b/src/lib/commands/sandbox/channels/remove.ts @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Command } from "@oclif/core"; + +import { + buildChannelArgs, + channelMutationArgs, + channelMutationFlags, + getChannelsRuntimeBridge, +} from "./common"; + +export default class ChannelsRemoveCommand extends Command { + static id = "sandbox:channels:remove"; + static strict = true; + static summary = "Clear messaging channel credentials and rebuild"; + static description = "Remove credentials for a messaging channel and queue a sandbox rebuild."; + static usage = [" channels remove [--dry-run]"]; + static examples = ["<%= config.bin %> alpha channels remove slack --dry-run"]; + static args = channelMutationArgs; + static flags = channelMutationFlags; + + public async run(): Promise { + const { args, flags } = await this.parse(ChannelsRemoveCommand); + await getChannelsRuntimeBridge().sandboxChannelsRemove( + args.sandboxName, + buildChannelArgs(args.channel, flags), + ); + } +} diff --git a/src/lib/commands/sandbox/channels/start.ts b/src/lib/commands/sandbox/channels/start.ts new file mode 100644 index 0000000000..ac20928878 --- /dev/null +++ b/src/lib/commands/sandbox/channels/start.ts @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Command } from "@oclif/core"; + +import { + buildChannelArgs, + channelMutationArgs, + channelMutationFlags, + getChannelsRuntimeBridge, +} from "./common"; + +export default class ChannelsStartCommand extends Command { + static id = "sandbox:channels:start"; + static strict = true; + static summary = "Re-enable a stopped messaging channel"; + static description = "Re-enable a previously stopped messaging channel."; + static usage = [" channels start [--dry-run]"]; + static examples = ["<%= config.bin %> alpha channels start discord"]; + static args = channelMutationArgs; + static flags = channelMutationFlags; + + public async run(): Promise { + const { args, flags } = await this.parse(ChannelsStartCommand); + await getChannelsRuntimeBridge().sandboxChannelsStart( + args.sandboxName, + buildChannelArgs(args.channel, flags), + ); + } +} diff --git a/src/lib/commands/sandbox/channels/stop.ts b/src/lib/commands/sandbox/channels/stop.ts new file mode 100644 index 0000000000..1ea7af8f5a --- /dev/null +++ b/src/lib/commands/sandbox/channels/stop.ts @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Command } from "@oclif/core"; + +import { + buildChannelArgs, + channelMutationArgs, + channelMutationFlags, + getChannelsRuntimeBridge, +} from "./common"; + +export default class ChannelsStopCommand extends Command { + static id = "sandbox:channels:stop"; + static strict = true; + static summary = "Disable channel without wiping credentials"; + static description = "Disable a messaging channel while keeping credentials in the gateway."; + static usage = [" channels stop [--dry-run]"]; + static examples = ["<%= config.bin %> alpha channels stop discord"]; + static args = channelMutationArgs; + static flags = channelMutationFlags; + + public async run(): Promise { + const { args, flags } = await this.parse(ChannelsStopCommand); + await getChannelsRuntimeBridge().sandboxChannelsStop( + args.sandboxName, + buildChannelArgs(args.channel, flags), + ); + } +} diff --git a/src/lib/commands/sandbox/common.ts b/src/lib/commands/sandbox/common.ts new file mode 100644 index 0000000000..0385b2760c --- /dev/null +++ b/src/lib/commands/sandbox/common.ts @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Args } from "@oclif/core"; + +export const sandboxNameArg = Args.string({ + name: "sandbox", + description: "Sandbox name", + required: true, +}); diff --git a/src/lib/commands/sandbox/config/get.ts b/src/lib/commands/sandbox/config/get.ts new file mode 100644 index 0000000000..4fd62466d6 --- /dev/null +++ b/src/lib/commands/sandbox/config/get.ts @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Command, Flags } from "@oclif/core"; + +import { CLI_NAME } from "../../../branding"; +import * as sandboxConfig from "../../../sandbox-config"; +import { sandboxNameArg } from "../common"; + +export default class SandboxConfigGetCommand extends Command { + static id = "sandbox:config:get"; + static strict = true; + 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", + options: ["json", "yaml"], + }), + }; + + public async run(): Promise { + const { args, flags } = await this.parse(SandboxConfigGetCommand); + sandboxConfig.configGet(args.sandboxName, { + key: flags.key ?? null, + format: flags.format ?? "json", + }); + } +} + +export function printConfigUsageAndExit(): never { + console.error(` Usage: ${CLI_NAME} config get [--key dotpath] [--format json|yaml]`); + process.exit(1); +} diff --git a/src/lib/connect-cli-command.ts b/src/lib/commands/sandbox/connect.ts similarity index 92% rename from src/lib/connect-cli-command.ts rename to src/lib/commands/sandbox/connect.ts index 8aed111e33..6a349a936a 100644 --- a/src/lib/connect-cli-command.ts +++ b/src/lib/commands/sandbox/connect.ts @@ -3,8 +3,8 @@ import { Args, Command, Flags } from "@oclif/core"; -import { CLI_NAME } from "./branding"; -import { connectSandbox } from "./sandbox-runtime-actions"; +import { CLI_NAME } from "../../branding"; +import { connectSandbox } from "../../sandbox-runtime-actions"; export default class ConnectCliCommand extends Command { static id = "sandbox:connect"; diff --git a/src/lib/destroy-cli-command.ts b/src/lib/commands/sandbox/destroy.ts similarity index 89% rename from src/lib/destroy-cli-command.ts rename to src/lib/commands/sandbox/destroy.ts index b0bbfae8de..dbaf97dc31 100644 --- a/src/lib/destroy-cli-command.ts +++ b/src/lib/commands/sandbox/destroy.ts @@ -3,8 +3,8 @@ import { Args, Flags } from "@oclif/core"; -import { NemoClawCommand } from "./cli/nemoclaw-oclif-command"; -import { destroySandbox } from "./sandbox-runtime-actions"; +import { NemoClawCommand } from "../../cli/nemoclaw-oclif-command"; +import { destroySandbox } from "../../sandbox-runtime-actions"; export default class DestroyCliCommand extends NemoClawCommand { static id = "sandbox:destroy"; diff --git a/src/lib/sandbox-doctor-cli-command.ts b/src/lib/commands/sandbox/doctor.ts similarity index 94% rename from src/lib/sandbox-doctor-cli-command.ts rename to src/lib/commands/sandbox/doctor.ts index e300ecf9fc..a32a56b771 100644 --- a/src/lib/sandbox-doctor-cli-command.ts +++ b/src/lib/commands/sandbox/doctor.ts @@ -3,7 +3,7 @@ import { Args, Command, Flags } from "@oclif/core"; -import { runSandboxDoctor } from "./sandbox-doctor-action"; +import { runSandboxDoctor } from "../../sandbox-doctor-action"; export default class SandboxDoctorCliCommand extends Command { static id = "sandbox:doctor"; diff --git a/src/lib/sandbox-logs-cli-command.test.ts b/src/lib/commands/sandbox/logs.test.ts similarity index 93% rename from src/lib/sandbox-logs-cli-command.test.ts rename to src/lib/commands/sandbox/logs.test.ts index a87afcbea3..7cd7d3e2da 100644 --- a/src/lib/sandbox-logs-cli-command.test.ts +++ b/src/lib/commands/sandbox/logs.test.ts @@ -3,9 +3,7 @@ import { describe, expect, it, vi } from "vitest"; -import SandboxLogsCommand, { - setSandboxLogsRuntimeBridgeFactoryForTest, -} from "./sandbox-logs-cli-command"; +import SandboxLogsCommand, { setSandboxLogsRuntimeBridgeFactoryForTest } from "./logs"; const rootDir = process.cwd(); diff --git a/src/lib/sandbox-logs-cli-command.ts b/src/lib/commands/sandbox/logs.ts similarity index 88% rename from src/lib/sandbox-logs-cli-command.ts rename to src/lib/commands/sandbox/logs.ts index 520ad2905e..fb3c179e91 100644 --- a/src/lib/sandbox-logs-cli-command.ts +++ b/src/lib/commands/sandbox/logs.ts @@ -3,10 +3,10 @@ import { Args, Command, Flags } from "@oclif/core"; -import { logsSinceDurationFlag } from "./duration-flags"; -import type { SandboxLogsOptions } from "./sandbox-logs-options"; -import { DEFAULT_SANDBOX_LOG_LINES } from "./sandbox-logs-options"; -import { showSandboxLogs } from "./sandbox-runtime-actions"; +import { logsSinceDurationFlag } from "../../duration-flags"; +import type { SandboxLogsOptions } from "../../sandbox-logs-options"; +import { DEFAULT_SANDBOX_LOG_LINES } from "../../sandbox-logs-options"; +import { showSandboxLogs } from "../../sandbox-runtime-actions"; type SandboxLogsRuntimeBridge = { sandboxLogs: (sandboxName: string, options: SandboxLogsOptions) => void; diff --git a/src/lib/sandbox-oclif-command-adapters.test.ts b/src/lib/commands/sandbox/oclif-command-adapters.test.ts similarity index 81% rename from src/lib/sandbox-oclif-command-adapters.test.ts rename to src/lib/commands/sandbox/oclif-command-adapters.test.ts index 30115ee3f6..4787ee7793 100644 --- a/src/lib/sandbox-oclif-command-adapters.test.ts +++ b/src/lib/commands/sandbox/oclif-command-adapters.test.ts @@ -17,43 +17,43 @@ const mocks = vi.hoisted(() => ({ showSandboxStatus: vi.fn().mockResolvedValue(undefined), })); -vi.mock("./sandbox-runtime-actions", () => ({ +vi.mock("../../sandbox-runtime-actions", () => ({ connectSandbox: mocks.connectSandbox, destroySandbox: mocks.destroySandbox, rebuildSandbox: mocks.rebuildSandbox, showSandboxStatus: mocks.showSandboxStatus, })); -vi.mock("./policy-channel-actions", () => ({ +vi.mock("../../policy-channel-actions", () => ({ listSandboxChannels: mocks.listSandboxChannels, listSandboxPolicies: mocks.listSandboxPolicies, })); -vi.mock("./sandbox-config", () => ({ +vi.mock("../../sandbox-config", () => ({ configGet: mocks.configGet, })); -vi.mock("./sandbox-doctor-action", () => ({ +vi.mock("../../sandbox-doctor-action", () => ({ runSandboxDoctor: mocks.runSandboxDoctor, })); -vi.mock("./shields", () => ({ +vi.mock("../../shields", () => ({ shieldsDown: mocks.shieldsDown, shieldsStatus: mocks.shieldsStatus, shieldsUp: mocks.shieldsUp, })); -import ConnectCliCommand from "./connect-cli-command"; -import DestroyCliCommand from "./destroy-cli-command"; -import RebuildCliCommand from "./rebuild-cli-command"; -import SandboxDoctorCliCommand from "./sandbox-doctor-cli-command"; -import { - SandboxChannelsListCommand, - SandboxConfigGetCommand, - SandboxPolicyListCommand, - SandboxStatusCommand, -} from "./sandbox-inspection-cli-command"; -import { ShieldsDownCommand, ShieldsStatusCommand, ShieldsUpCommand } from "./shields-cli-commands"; +import ConnectCliCommand from "./connect"; +import SandboxConfigGetCommand from "./config/get"; +import DestroyCliCommand from "./destroy"; +import SandboxDoctorCliCommand from "./doctor"; +import SandboxChannelsListCommand from "./channels/list"; +import SandboxPolicyListCommand from "./policy/list"; +import RebuildCliCommand from "./rebuild"; +import SandboxStatusCommand from "./status"; +import ShieldsDownCommand from "./shields/down"; +import ShieldsStatusCommand from "./shields/status"; +import ShieldsUpCommand from "./shields/up"; const rootDir = process.cwd(); diff --git a/src/lib/commands/sandbox/policy/add.ts b/src/lib/commands/sandbox/policy/add.ts new file mode 100644 index 0000000000..ac9ee92acd --- /dev/null +++ b/src/lib/commands/sandbox/policy/add.ts @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Command, Flags } from "@oclif/core"; + +import { + appendCommonPolicyFlags, + getPolicyRuntimeBridge, + policyMutationArgs, + policyMutationFlags, +} from "./common"; + +export default class PolicyAddCommand extends Command { + static id = "sandbox:policy:add"; + static strict = true; + static summary = "Add a network or filesystem policy preset"; + static description = "Add a built-in or custom policy preset to a sandbox."; + static usage = [ + " policy-add [preset] [--yes|-y] [--dry-run] [--from-file ] [--from-dir ]", + ]; + static examples = [ + "<%= config.bin %> alpha policy-add slack --yes", + "<%= config.bin %> alpha policy-add --from-file ./policy.yaml --dry-run", + "<%= config.bin %> alpha policy-add --from-dir ./policies --yes", + ]; + static args = policyMutationArgs; + static flags = { + ...policyMutationFlags, + "from-file": Flags.string({ + description: "Load one custom preset YAML file", + exclusive: ["from-dir"], + }), + "from-dir": Flags.string({ + description: "Load all custom preset YAML files in a directory", + exclusive: ["from-file"], + }), + }; + + public async run(): Promise { + const { args, flags } = await this.parse(PolicyAddCommand); + const legacyArgs: string[] = []; + if (args.preset) legacyArgs.push(args.preset); + appendCommonPolicyFlags(legacyArgs, flags); + if (flags["from-file"]) legacyArgs.push("--from-file", flags["from-file"]); + if (flags["from-dir"]) legacyArgs.push("--from-dir", flags["from-dir"]); + await getPolicyRuntimeBridge().sandboxPolicyAdd(args.sandboxName, legacyArgs); + } +} diff --git a/src/lib/commands/sandbox/policy/common.ts b/src/lib/commands/sandbox/policy/common.ts new file mode 100644 index 0000000000..a5cd7b358f --- /dev/null +++ b/src/lib/commands/sandbox/policy/common.ts @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Args, Flags } from "@oclif/core"; + +type PolicyRuntimeBridge = { + sandboxPolicyAdd: (sandboxName: string, args?: string[]) => Promise; + sandboxPolicyRemove: (sandboxName: string, args?: string[]) => Promise; +}; + +let runtimeBridgeFactory = (): PolicyRuntimeBridge => { + const actions = require("../../../policy-channel-actions") as { + addSandboxPolicy: PolicyRuntimeBridge["sandboxPolicyAdd"]; + removeSandboxPolicy: PolicyRuntimeBridge["sandboxPolicyRemove"]; + }; + return { + sandboxPolicyAdd: actions.addSandboxPolicy, + sandboxPolicyRemove: actions.removeSandboxPolicy, + }; +}; + +export function setPolicyRuntimeBridgeFactoryForTest(factory: () => PolicyRuntimeBridge): void { + runtimeBridgeFactory = factory; +} + +export function getPolicyRuntimeBridge(): PolicyRuntimeBridge { + return runtimeBridgeFactory(); +} + +const sandboxNameArg = Args.string({ + name: "sandbox", + description: "Sandbox name", + required: true, +}); +const presetArg = Args.string({ + name: "preset", + description: "Policy preset name", + required: false, +}); + +export function appendCommonPolicyFlags( + args: string[], + flags: { yes?: boolean; force?: boolean; "dry-run"?: boolean }, +): void { + if (flags.yes) args.push("--yes"); + if (flags.force) args.push("--force"); + if (flags["dry-run"]) args.push("--dry-run"); +} + +export const policyMutationArgs = { sandboxName: sandboxNameArg, preset: presetArg }; + +export const policyMutationFlags = { + help: Flags.help({ char: "h" }), + yes: Flags.boolean({ char: "y", description: "Skip the confirmation prompt" }), + force: Flags.boolean({ description: "Skip the confirmation prompt" }), + "dry-run": Flags.boolean({ description: "Preview without applying" }), +}; diff --git a/src/lib/commands/sandbox/policy/list.ts b/src/lib/commands/sandbox/policy/list.ts new file mode 100644 index 0000000000..ae740eebb2 --- /dev/null +++ b/src/lib/commands/sandbox/policy/list.ts @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Command, Flags } from "@oclif/core"; + +import { listSandboxPolicies } from "../../../policy-channel-actions"; +import { sandboxNameArg } from "../common"; + +export default class SandboxPolicyListCommand extends Command { + static id = "sandbox:policy:list"; + static strict = true; + 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, + }; + static flags = { + help: Flags.help({ char: "h" }), + }; + + public async run(): Promise { + const { args } = await this.parse(SandboxPolicyListCommand); + listSandboxPolicies(args.sandboxName); + } +} diff --git a/src/lib/policy-mutate-cli-commands.test.ts b/src/lib/commands/sandbox/policy/mutate.test.ts similarity index 94% rename from src/lib/policy-mutate-cli-commands.test.ts rename to src/lib/commands/sandbox/policy/mutate.test.ts index c49d6d77a4..eca4ef9290 100644 --- a/src/lib/policy-mutate-cli-commands.test.ts +++ b/src/lib/commands/sandbox/policy/mutate.test.ts @@ -3,11 +3,9 @@ import { describe, expect, it, vi } from "vitest"; -import { - PolicyAddCommand, - PolicyRemoveCommand, - setPolicyRuntimeBridgeFactoryForTest, -} from "./policy-mutate-cli-commands"; +import PolicyAddCommand from "./add"; +import { setPolicyRuntimeBridgeFactoryForTest } from "./common"; +import PolicyRemoveCommand from "./remove"; const rootDir = process.cwd(); diff --git a/src/lib/commands/sandbox/policy/remove.ts b/src/lib/commands/sandbox/policy/remove.ts new file mode 100644 index 0000000000..63b0fe08a9 --- /dev/null +++ b/src/lib/commands/sandbox/policy/remove.ts @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Command } from "@oclif/core"; + +import { + appendCommonPolicyFlags, + getPolicyRuntimeBridge, + policyMutationArgs, + policyMutationFlags, +} from "./common"; + +export default class PolicyRemoveCommand extends Command { + static id = "sandbox:policy:remove"; + static strict = true; + static summary = "Remove an applied policy preset"; + static description = "Remove a built-in or custom policy preset from a sandbox."; + static usage = [" policy-remove [preset] [--yes|-y] [--dry-run]"]; + static examples = [ + "<%= config.bin %> alpha policy-remove slack --yes", + "<%= config.bin %> alpha policy-remove slack --dry-run", + ]; + static args = policyMutationArgs; + static flags = policyMutationFlags; + + public async run(): Promise { + const { args, flags } = await this.parse(PolicyRemoveCommand); + const legacyArgs: string[] = []; + if (args.preset) legacyArgs.push(args.preset); + appendCommonPolicyFlags(legacyArgs, flags); + await getPolicyRuntimeBridge().sandboxPolicyRemove(args.sandboxName, legacyArgs); + } +} diff --git a/src/lib/rebuild-cli-command.ts b/src/lib/commands/sandbox/rebuild.ts similarity index 90% rename from src/lib/rebuild-cli-command.ts rename to src/lib/commands/sandbox/rebuild.ts index 7e1908034f..afe90d8a7c 100644 --- a/src/lib/rebuild-cli-command.ts +++ b/src/lib/commands/sandbox/rebuild.ts @@ -3,8 +3,8 @@ import { Args, Flags } from "@oclif/core"; -import { NemoClawCommand } from "./cli/nemoclaw-oclif-command"; -import { rebuildSandbox } from "./sandbox-runtime-actions"; +import { NemoClawCommand } from "../../cli/nemoclaw-oclif-command"; +import { rebuildSandbox } from "../../sandbox-runtime-actions"; export default class RebuildCliCommand extends NemoClawCommand { static id = "sandbox:rebuild"; diff --git a/src/lib/share-cli-commands.test.ts b/src/lib/commands/sandbox/share.test.ts similarity index 88% rename from src/lib/share-cli-commands.test.ts rename to src/lib/commands/sandbox/share.test.ts index 1ef4eba36c..d04e821f5b 100644 --- a/src/lib/share-cli-commands.test.ts +++ b/src/lib/commands/sandbox/share.test.ts @@ -12,18 +12,17 @@ const mocks = vi.hoisted(() => ({ runShareUnmount: vi.fn(), })); -vi.mock("./share-command", () => ({ +vi.mock("../../share-command", () => ({ printShareUsageAndExit: mocks.printShareUsageAndExit, runShareMount: mocks.runShareMount, runShareStatus: mocks.runShareStatus, runShareUnmount: mocks.runShareUnmount, })); -import ShareCommand, { - ShareMountCommand, - ShareStatusCommand, - ShareUnmountCommand, -} from "./share-cli-commands"; +import ShareCommand from "./share"; +import ShareMountCommand from "./share/mount"; +import ShareStatusCommand from "./share/status"; +import ShareUnmountCommand from "./share/unmount"; const rootDir = process.cwd(); diff --git a/src/lib/commands/sandbox/share.ts b/src/lib/commands/sandbox/share.ts new file mode 100644 index 0000000000..5cdbcfaeab --- /dev/null +++ b/src/lib/commands/sandbox/share.ts @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Command, Flags } from "@oclif/core"; + +import { printShareUsageAndExit } from "../../share-command"; +import { sandboxNameArg } from "./common"; + +export default class ShareCommand extends Command { + static id = "sandbox:share"; + static strict = true; + static summary = "Mount/unmount sandbox filesystem on the host via SSHFS"; + static description = "Share files between host and sandbox using SSHFS over OpenShell's SSH proxy."; + static usage = [" share "]; + static examples = [ + "<%= config.bin %> alpha share mount", + "<%= config.bin %> alpha share unmount", + "<%= config.bin %> alpha share status", + ]; + static args = { + sandboxName: sandboxNameArg, + }; + static flags = { + help: Flags.help({ char: "h" }), + }; + + public async run(): Promise { + await this.parse(ShareCommand); + printShareUsageAndExit(1); + } +} diff --git a/src/lib/commands/sandbox/share/mount.ts b/src/lib/commands/sandbox/share/mount.ts new file mode 100644 index 0000000000..8590bd43ae --- /dev/null +++ b/src/lib/commands/sandbox/share/mount.ts @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Args, Command, Flags } from "@oclif/core"; + +import { runShareMount } from "../../../share-command"; +import { sandboxNameArg } from "../common"; + +export default class ShareMountCommand extends Command { + static id = "sandbox:share:mount"; + static strict = true; + static summary = "Mount sandbox filesystem on the host"; + static description = "Mount a sandbox path on the host using SSHFS over OpenShell's SSH proxy."; + static usage = [" share mount [sandbox-path] [local-mount-point]"]; + static examples = [ + "<%= config.bin %> alpha share mount", + "<%= config.bin %> alpha share mount /workspace ~/mnt/alpha", + ]; + static args = { + sandboxName: sandboxNameArg, + sandboxPath: Args.string({ + name: "sandbox-path", + description: "Path inside the sandbox to mount", + required: false, + }), + localMountPoint: Args.string({ + name: "local-mount-point", + description: "Host path for the SSHFS mount", + required: false, + }), + }; + static flags = { + help: Flags.help({ char: "h" }), + }; + + public async run(): Promise { + const { args } = await this.parse(ShareMountCommand); + await runShareMount({ + sandboxName: args.sandboxName, + remotePath: args.sandboxPath, + localMount: args.localMountPoint, + }); + } +} diff --git a/src/lib/commands/sandbox/share/status.ts b/src/lib/commands/sandbox/share/status.ts new file mode 100644 index 0000000000..29dd0785b2 --- /dev/null +++ b/src/lib/commands/sandbox/share/status.ts @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Args, Command, Flags } from "@oclif/core"; + +import { runShareStatus } from "../../../share-command"; +import { sandboxNameArg } from "../common"; + +export default class ShareStatusCommand extends Command { + static id = "sandbox:share:status"; + static strict = true; + static summary = "Show sandbox share mount status"; + static description = "Check whether a sandbox filesystem share is currently mounted on the host."; + static usage = [" share status [local-mount-point]"]; + static examples = [ + "<%= config.bin %> alpha share status", + "<%= config.bin %> alpha share status ~/mnt/alpha", + ]; + static args = { + sandboxName: sandboxNameArg, + localMountPoint: Args.string({ + name: "local-mount-point", + description: "Host mount path to check", + required: false, + }), + }; + static flags = { + help: Flags.help({ char: "h" }), + }; + + public async run(): Promise { + const { args } = await this.parse(ShareStatusCommand); + runShareStatus({ sandboxName: args.sandboxName, localMount: args.localMountPoint }); + } +} diff --git a/src/lib/commands/sandbox/share/unmount.ts b/src/lib/commands/sandbox/share/unmount.ts new file mode 100644 index 0000000000..e6c768f5da --- /dev/null +++ b/src/lib/commands/sandbox/share/unmount.ts @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Args, Command, Flags } from "@oclif/core"; + +import { runShareUnmount } from "../../../share-command"; +import { sandboxNameArg } from "../common"; + +export default class ShareUnmountCommand extends Command { + static id = "sandbox:share:unmount"; + static strict = true; + static summary = "Unmount a shared sandbox filesystem"; + static description = "Unmount a previously mounted sandbox filesystem from the host."; + static usage = [" share unmount [local-mount-point]"]; + static examples = [ + "<%= config.bin %> alpha share unmount", + "<%= config.bin %> alpha share unmount ~/mnt/alpha", + ]; + static args = { + sandboxName: sandboxNameArg, + localMountPoint: Args.string({ + name: "local-mount-point", + description: "Host mount path to unmount", + required: false, + }), + }; + static flags = { + help: Flags.help({ char: "h" }), + }; + + public async run(): Promise { + const { args } = await this.parse(ShareUnmountCommand); + runShareUnmount({ sandboxName: args.sandboxName, localMount: args.localMountPoint }); + } +} diff --git a/src/lib/commands/sandbox/shields/down.ts b/src/lib/commands/sandbox/shields/down.ts new file mode 100644 index 0000000000..d813a83477 --- /dev/null +++ b/src/lib/commands/sandbox/shields/down.ts @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Command, Flags } from "@oclif/core"; + +import { shieldsTimeoutDurationFlag } from "../../../duration-flags"; +import * as shields from "../../../shields"; +import { sandboxNameArg } from "../common"; + +export default class ShieldsDownCommand extends Command { + static id = "sandbox:shields:down"; + static hidden = true; + static strict = true; + static summary = "Lower sandbox security shields"; + static description = "Temporarily lower sandbox shields."; + static usage = [" shields down [--timeout 5m] [--reason ] [--policy permissive]"]; + static args = { sandboxName: sandboxNameArg }; + static flags = { + help: Flags.help({ char: "h" }), + timeout: shieldsTimeoutDurationFlag({ description: "Duration before shields are restored" }), + reason: Flags.string({ description: "Reason for lowering shields" }), + policy: Flags.string({ description: "Policy to apply while shields are down" }), + }; + + public async run(): Promise { + const { args, flags } = await this.parse(ShieldsDownCommand); + shields.shieldsDown(args.sandboxName, { + timeout: flags.timeout ?? null, + reason: flags.reason ?? null, + policy: flags.policy ?? "permissive", + }); + } +} diff --git a/src/lib/commands/sandbox/shields/status.ts b/src/lib/commands/sandbox/shields/status.ts new file mode 100644 index 0000000000..f431d0b070 --- /dev/null +++ b/src/lib/commands/sandbox/shields/status.ts @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Command, Flags } from "@oclif/core"; + +import * as shields from "../../../shields"; +import { sandboxNameArg } from "../common"; + +export default class ShieldsStatusCommand extends Command { + static id = "sandbox:shields:status"; + static hidden = true; + static strict = true; + static summary = "Show current shields state"; + static description = "Show current sandbox shields state."; + static usage = [" shields status"]; + static args = { sandboxName: sandboxNameArg }; + static flags = { + help: Flags.help({ char: "h" }), + }; + + public async run(): Promise { + const { args } = await this.parse(ShieldsStatusCommand); + shields.shieldsStatus(args.sandboxName); + } +} diff --git a/src/lib/commands/sandbox/shields/up.ts b/src/lib/commands/sandbox/shields/up.ts new file mode 100644 index 0000000000..4706c760a2 --- /dev/null +++ b/src/lib/commands/sandbox/shields/up.ts @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Command, Flags } from "@oclif/core"; + +import * as shields from "../../../shields"; +import { sandboxNameArg } from "../common"; + +export default class ShieldsUpCommand extends Command { + static id = "sandbox:shields:up"; + static hidden = true; + static strict = true; + static summary = "Raise sandbox security shields"; + static description = "Restore sandbox shields from the saved snapshot."; + static usage = [" shields up"]; + static args = { sandboxName: sandboxNameArg }; + static flags = { + help: Flags.help({ char: "h" }), + }; + + public async run(): Promise { + const { args } = await this.parse(ShieldsUpCommand); + shields.shieldsUp(args.sandboxName); + } +} diff --git a/src/lib/skill-install-cli-command.test.ts b/src/lib/commands/sandbox/skill.test.ts similarity index 88% rename from src/lib/skill-install-cli-command.test.ts rename to src/lib/commands/sandbox/skill.test.ts index e1468d8f18..bade989d18 100644 --- a/src/lib/skill-install-cli-command.test.ts +++ b/src/lib/commands/sandbox/skill.test.ts @@ -3,9 +3,8 @@ import { describe, expect, it, vi } from "vitest"; -import SkillInstallCliCommand, { - setSkillInstallRuntimeBridgeFactoryForTest, -} from "./skill-install-cli-command"; +import { setSkillInstallRuntimeBridgeFactoryForTest } from "./skill/common"; +import SkillInstallCliCommand from "./skill/install"; const rootDir = process.cwd(); diff --git a/src/lib/commands/sandbox/skill.ts b/src/lib/commands/sandbox/skill.ts new file mode 100644 index 0000000000..7ec82a673d --- /dev/null +++ b/src/lib/commands/sandbox/skill.ts @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Command } from "@oclif/core"; + +import { getSkillInstallRuntimeBridge } from "./skill/common"; + +export default class SkillCliCommand extends Command { + static id = "sandbox:skill"; + static strict = false; + static summary = "Show skill command usage"; + static description = "Show skill install usage or report unknown skill subcommands."; + static usage = [" skill install "]; + static examples = ["<%= config.bin %> alpha skill install ./my-skill"]; + + public async run(): Promise { + const [sandboxName, ...actionArgs] = this.argv; + if (!sandboxName || sandboxName.trim() === "") { + this.error("Missing required sandboxName for skill.", { exit: 2 }); + } + await getSkillInstallRuntimeBridge().sandboxSkillInstall(sandboxName, actionArgs); + } +} diff --git a/src/lib/commands/sandbox/skill/common.ts b/src/lib/commands/sandbox/skill/common.ts new file mode 100644 index 0000000000..b2ca1df5e7 --- /dev/null +++ b/src/lib/commands/sandbox/skill/common.ts @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { installSandboxSkill } from "../../../sandbox-runtime-actions"; + +let runtimeBridgeFactory = () => ({ sandboxSkillInstall: installSandboxSkill }); + +export function setSkillInstallRuntimeBridgeFactoryForTest( + factory: () => { sandboxSkillInstall: (sandboxName: string, args?: string[]) => Promise }, +): void { + runtimeBridgeFactory = factory; +} + +export function getSkillInstallRuntimeBridge() { + return runtimeBridgeFactory(); +} diff --git a/src/lib/commands/sandbox/skill/install.ts b/src/lib/commands/sandbox/skill/install.ts new file mode 100644 index 0000000000..79ff770d65 --- /dev/null +++ b/src/lib/commands/sandbox/skill/install.ts @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Args, Command, Flags } from "@oclif/core"; + +import { getSkillInstallRuntimeBridge } from "./common"; + +export default class SkillInstallCliCommand extends Command { + static id = "sandbox:skill:install"; + static strict = true; + static summary = "Deploy a skill directory to the sandbox"; + static description = "Validate a local SKILL.md directory and upload it to a running sandbox."; + static usage = [" skill install "]; + static examples = [ + "<%= config.bin %> alpha skill install ./my-skill", + "<%= config.bin %> alpha skill install ./my-skill/SKILL.md", + ]; + static args = { + sandboxName: Args.string({ + name: "sandbox", + description: "Sandbox name", + required: true, + }), + skillPath: Args.string({ + name: "path", + description: "Skill directory or direct path to SKILL.md", + required: true, + }), + }; + static flags = { + help: Flags.help({ char: "h" }), + }; + + public async run(): Promise { + const { args } = await this.parse(SkillInstallCliCommand); + await getSkillInstallRuntimeBridge().sandboxSkillInstall(args.sandboxName, [ + "install", + args.skillPath, + ]); + } +} diff --git a/src/lib/snapshot-cli-commands.test.ts b/src/lib/commands/sandbox/snapshot.test.ts similarity index 88% rename from src/lib/snapshot-cli-commands.test.ts rename to src/lib/commands/sandbox/snapshot.test.ts index ceb1485fe4..841edb15f9 100644 --- a/src/lib/snapshot-cli-commands.test.ts +++ b/src/lib/commands/sandbox/snapshot.test.ts @@ -3,13 +3,11 @@ import { describe, expect, it, vi } from "vitest"; -import { - setSnapshotRuntimeBridgeFactoryForTest, - SnapshotCommand, - SnapshotCreateCommand, - SnapshotListCommand, - SnapshotRestoreCommand, -} from "./snapshot-cli-commands"; +import SnapshotCommand from "./snapshot"; +import { setSnapshotRuntimeBridgeFactoryForTest } from "./snapshot/common"; +import SnapshotCreateCommand from "./snapshot/create"; +import SnapshotListCommand from "./snapshot/list"; +import SnapshotRestoreCommand from "./snapshot/restore"; const rootDir = process.cwd(); diff --git a/src/lib/commands/sandbox/snapshot.ts b/src/lib/commands/sandbox/snapshot.ts new file mode 100644 index 0000000000..9ea7798117 --- /dev/null +++ b/src/lib/commands/sandbox/snapshot.ts @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Command } from "@oclif/core"; + +import { getSnapshotRuntimeBridge, sandboxNameArg } from "./snapshot/common"; + +export default class SnapshotCommand extends Command { + static id = "sandbox:snapshot"; + static strict = true; + static summary = "Show snapshot usage"; + static description = "Show snapshot usage for create, list, and restore subcommands."; + static usage = [" snapshot "]; + static examples = [ + "<%= config.bin %> alpha snapshot create", + "<%= config.bin %> alpha snapshot list", + "<%= config.bin %> alpha snapshot restore", + ]; + static args = { + sandboxName: sandboxNameArg, + }; + + public async run(): Promise { + const { args } = await this.parse(SnapshotCommand); + await getSnapshotRuntimeBridge().sandboxSnapshot(args.sandboxName, []); + } +} diff --git a/src/lib/commands/sandbox/snapshot/common.ts b/src/lib/commands/sandbox/snapshot/common.ts new file mode 100644 index 0000000000..a7443a5084 --- /dev/null +++ b/src/lib/commands/sandbox/snapshot/common.ts @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Args } from "@oclif/core"; + +import { runSandboxSnapshot } from "../../../sandbox-runtime-actions"; + +let runtimeBridgeFactory = () => ({ sandboxSnapshot: runSandboxSnapshot }); + +export function setSnapshotRuntimeBridgeFactoryForTest( + factory: () => { sandboxSnapshot: (sandboxName: string, args: string[]) => Promise }, +): void { + runtimeBridgeFactory = factory; +} + +export function getSnapshotRuntimeBridge() { + return runtimeBridgeFactory(); +} + +export const sandboxNameArg = Args.string({ + name: "sandbox", + description: "Sandbox name", + required: true, +}); diff --git a/src/lib/commands/sandbox/snapshot/create.ts b/src/lib/commands/sandbox/snapshot/create.ts new file mode 100644 index 0000000000..52687f8736 --- /dev/null +++ b/src/lib/commands/sandbox/snapshot/create.ts @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Command, Flags } from "@oclif/core"; + +import { getSnapshotRuntimeBridge, sandboxNameArg } from "./common"; + +export default class SnapshotCreateCommand extends Command { + static id = "sandbox:snapshot:create"; + static strict = true; + static summary = "Create a snapshot of sandbox state"; + static description = "Create an auto-versioned snapshot of sandbox workspace state."; + static usage = [" snapshot create [--name