diff --git a/src/lib/connect-cli-command.ts b/src/lib/connect-cli-command.ts new file mode 100644 index 00000000000..5dc24fac4f5 --- /dev/null +++ b/src/lib/connect-cli-command.ts @@ -0,0 +1,48 @@ +// 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"; + +import { CLI_NAME } from "./branding"; + +type SandboxConnectOptions = { + probeOnly?: boolean; +}; + +type RuntimeBridge = { + sandboxConnect: (sandboxName: string, options?: SandboxConnectOptions) => Promise; +}; + +function getRuntimeBridge(): RuntimeBridge { + return require("../nemoclaw") as RuntimeBridge; +} + +export default class ConnectCliCommand extends Command { + static id = "sandbox:connect"; + static strict = true; + static summary = "Shell into a running sandbox"; + static description = "Connect to a running sandbox."; + static usage = [" connect [--probe-only]"]; + static args = { + sandboxName: Args.string({ name: "sandbox", description: "Sandbox name", required: true }), + }; + static flags = { + help: Flags.help({ char: "h" }), + "probe-only": Flags.boolean({ description: "Recover and check the sandbox without opening SSH" }), + "dangerously-skip-permissions": Flags.boolean({ hidden: true }), + }; + + public async run(): Promise { + const { args, flags } = await this.parse(ConnectCliCommand); + if (flags["dangerously-skip-permissions"]) { + console.error(" --dangerously-skip-permissions was removed; use shields commands instead."); + console.error(` Usage: ${CLI_NAME} connect [--probe-only]`); + process.exit(1); + } + await getRuntimeBridge().sandboxConnect(args.sandboxName, { + probeOnly: Boolean(flags["probe-only"]), + }); + } +} diff --git a/src/lib/oclif-commands.ts b/src/lib/oclif-commands.ts index 95561971f56..fb08cc9eb6a 100644 --- a/src/lib/oclif-commands.ts +++ b/src/lib/oclif-commands.ts @@ -12,6 +12,7 @@ import { CredentialsListCommand, CredentialsResetCommand, } from "./credentials-cli-command"; +import ConnectCliCommand from "./connect-cli-command"; import DebugCliCommand from "./debug-cli-command"; import DestroyCliCommand from "./destroy-cli-command"; import GatewayTokenCliCommand from "./gateway-token-cli-command"; @@ -64,6 +65,7 @@ export default { "sandbox:channels:start": ChannelsStartCommand, "sandbox:channels:stop": ChannelsStopCommand, "sandbox:config:get": SandboxConfigGetCommand, + "sandbox:connect": ConnectCliCommand, "sandbox:destroy": DestroyCliCommand, "sandbox:logs": SandboxLogsCommand, "sandbox:policy-add": PolicyAddCommand, diff --git a/src/nemoclaw.ts b/src/nemoclaw.ts index e582d7222b4..4d4a3fa2955 100644 --- a/src/nemoclaw.ts +++ b/src/nemoclaw.ts @@ -650,6 +650,7 @@ exports.garbageCollectImages = garbageCollectImages; exports.recoverNamedGatewayRuntime = recoverNamedGatewayRuntime; exports.recoverRegistryEntries = recoverRegistryEntries; exports.runOpenshell = runOpenshell; +exports.sandboxConnect = sandboxConnect; exports.sandboxDestroy = sandboxDestroy; exports.sandboxChannelsAdd = sandboxChannelsAdd; exports.sandboxChannelsList = sandboxChannelsList; @@ -4899,7 +4900,8 @@ const mainPromise = (async () => { switch (action) { case "connect": - await sandboxConnect(cmd, parseSandboxConnectArgs(cmd, actionArgs)); + parseSandboxConnectArgs(cmd, actionArgs); + await runOclif("sandbox:connect", [cmd, ...actionArgs]); break; case "status": if (hasHelpFlag(actionArgs)) { diff --git a/test/cli.test.ts b/test/cli.test.ts index aebe00fc895..c8fb6458ce4 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -1076,6 +1076,11 @@ describe("CLI dispatch", () => { const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-cli-inspection-help-")); writeSandboxRegistry(home); + const connect = runWithEnv("alpha connect --help", { HOME: home }); + expect(connect.code).toBe(0); + expect(connect.out).toContain("Usage: nemoclaw alpha connect"); + expect(connect.out).not.toContain("sandbox:connect"); + const status = runWithEnv("alpha status --help", { HOME: home }); expect(status.code).toBe(0); expect(status.out).toContain(" status");