diff --git a/src/lib/destroy-cli-command.ts b/src/lib/destroy-cli-command.ts new file mode 100644 index 00000000000..8c9166528fd --- /dev/null +++ b/src/lib/destroy-cli-command.ts @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +/* v8 ignore start -- thin oclif adapter covered through CLI integration tests. */ + +import { Args, Command, Flags } from "@oclif/core"; + +type RuntimeBridge = { + sandboxDestroy: (sandboxName: string, args?: string[]) => Promise; +}; + +function getRuntimeBridge(): RuntimeBridge { + return require("../nemoclaw") as RuntimeBridge; +} + +export default class DestroyCliCommand extends Command { + static id = "sandbox:destroy"; + static strict = true; + static summary = "Stop NIM and delete sandbox"; + static description = "Destroy a sandbox and remove its local registry entry."; + static usage = [" destroy [--yes|--force]"]; + static args = { + sandboxName: Args.string({ name: "sandbox", description: "Sandbox name", required: true }), + }; + static flags = { + help: Flags.help({ char: "h" }), + yes: Flags.boolean({ description: "Skip the confirmation prompt" }), + force: Flags.boolean({ description: "Skip the confirmation prompt" }), + }; + + public async run(): Promise { + const { args, flags } = await this.parse(DestroyCliCommand); + const legacyArgs: string[] = []; + if (flags.yes) legacyArgs.push("--yes"); + if (flags.force) legacyArgs.push("--force"); + await getRuntimeBridge().sandboxDestroy(args.sandboxName, legacyArgs); + } +} diff --git a/src/lib/oclif-commands.ts b/src/lib/oclif-commands.ts index a57c2254c7c..5f21317f3fd 100644 --- a/src/lib/oclif-commands.ts +++ b/src/lib/oclif-commands.ts @@ -13,6 +13,7 @@ import { CredentialsResetCommand, } from "./credentials-cli-command"; import DebugCliCommand from "./debug-cli-command"; +import DestroyCliCommand from "./destroy-cli-command"; import GatewayTokenCliCommand from "./gateway-token-cli-command"; import ListCommand from "./list-command"; import { @@ -62,6 +63,7 @@ export default { "sandbox:channels:start": ChannelsStartCommand, "sandbox:channels:stop": ChannelsStopCommand, "sandbox:config:get": SandboxConfigGetCommand, + "sandbox:destroy": DestroyCliCommand, "sandbox:logs": SandboxLogsCommand, "sandbox:policy-add": PolicyAddCommand, "sandbox:policy-list": SandboxPolicyListCommand, diff --git a/src/nemoclaw.ts b/src/nemoclaw.ts index d7bb6be28ab..2ad60e44ad3 100644 --- a/src/nemoclaw.ts +++ b/src/nemoclaw.ts @@ -646,6 +646,7 @@ exports.garbageCollectImages = garbageCollectImages; exports.recoverNamedGatewayRuntime = recoverNamedGatewayRuntime; exports.recoverRegistryEntries = recoverRegistryEntries; exports.runOpenshell = runOpenshell; +exports.sandboxDestroy = sandboxDestroy; exports.sandboxChannelsAdd = sandboxChannelsAdd; exports.sandboxChannelsList = sandboxChannelsList; exports.sandboxChannelsRemove = sandboxChannelsRemove; @@ -4943,7 +4944,11 @@ const mainPromise = (async () => { await runOclif("sandbox:policy-list", [cmd, ...actionArgs]); break; case "destroy": - await sandboxDestroy(cmd, actionArgs); + if (hasHelpFlag(actionArgs)) { + printSandboxActionUsage("destroy [--yes|--force]"); + break; + } + await runOclif("sandbox:destroy", [cmd, ...actionArgs]); break; case "gateway-token": if (actionArgs.includes("--help") || actionArgs.includes("-h")) { diff --git a/test/cli.test.ts b/test/cli.test.ts index c1f4e87e924..225bd041710 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -1068,6 +1068,11 @@ describe("CLI dispatch", () => { expect(logs.out).toContain(" logs [--follow]"); expect(logs.out).not.toContain("sandbox:logs"); + const destroy = runWithEnv("alpha destroy --help", { HOME: home }); + expect(destroy.code).toBe(0); + expect(destroy.out).toContain(" destroy [--yes|--force]"); + expect(destroy.out).not.toContain("sandbox:destroy"); + for (const action of ["policy-add", "policy-remove", "policy-list"]) { const policy = runWithEnv(`alpha ${action} --help`, { HOME: home }); expect(policy.code).toBe(0);