Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
116111c
refactor(cli): harden oclif bridge
cv Apr 30, 2026
5e997cb
test(cli): relax uninstall helper timeouts
cv Apr 30, 2026
88cd958
refactor(cli): migrate status and tunnel commands to oclif
cv Apr 30, 2026
3f748d6
refactor(cli): migrate debug uninstall and gateway-token to oclif
cv Apr 30, 2026
0bc877a
refactor(cli): migrate credentials commands to oclif
cv Apr 30, 2026
3fd15fc
refactor(cli): migrate sandbox inspection commands to oclif
cv Apr 30, 2026
81c62bb
refactor(cli): migrate maintenance commands to oclif
cv Apr 30, 2026
7d6a6fb
refactor(cli): migrate sandbox logs command to oclif
cv May 1, 2026
3f17a4f
refactor(cli): migrate skill install command to oclif
cv May 1, 2026
0fb3c41
refactor(cli): migrate snapshot list and create to oclif
cv May 1, 2026
d11eeee
refactor(cli): migrate shields commands to oclif
cv May 1, 2026
74806ce
refactor(cli): migrate channels mutation commands to oclif
cv May 1, 2026
e637e84
refactor(cli): migrate policy mutation commands to oclif
cv May 1, 2026
ee20dfb
refactor(cli): migrate snapshot restore command to oclif
cv May 1, 2026
8d3a568
refactor(cli): migrate destroy command to oclif
cv May 1, 2026
b447f36
refactor(cli): migrate rebuild command to oclif
cv May 1, 2026
02431db
refactor(cli): migrate connect command to oclif
cv May 1, 2026
37ce2ff
refactor(cli): migrate deploy command to oclif
cv May 1, 2026
d3e218e
refactor(cli): migrate onboard aliases to oclif
cv May 1, 2026
df05a3e
refactor(cli): migrate help and version commands to oclif
cv May 1, 2026
3edcf2e
refactor(cli): centralize legacy oclif dispatch
cv May 1, 2026
47df3b5
refactor(cli): drop trivial oclif wrapper helpers
cv May 1, 2026
460173c
refactor(cli): centralize sandbox runtime bridge
cv May 1, 2026
31c0195
test(cli): keep dispatch table out of coverage ratchet
cv May 1, 2026
481c758
merge(main): resolve sandbox runtime bridge conflicts
cv May 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 2 additions & 13 deletions src/lib/connect-cli-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,7 @@
import { Args, Command, Flags } from "@oclif/core";

import { CLI_NAME } from "./branding";

type SandboxConnectOptions = {
probeOnly?: boolean;
};

type RuntimeBridge = {
sandboxConnect: (sandboxName: string, options?: SandboxConnectOptions) => Promise<void>;
};

function getRuntimeBridge(): RuntimeBridge {
return require("../nemoclaw") as RuntimeBridge;
}
import { getNemoClawRuntimeBridge } from "./nemoclaw-runtime-bridge";

export default class ConnectCliCommand extends Command {
static id = "sandbox:connect";
Expand All @@ -41,7 +30,7 @@ export default class ConnectCliCommand extends Command {
console.error(` Usage: ${CLI_NAME} <name> connect [--probe-only]`);
process.exit(1);
}
await getRuntimeBridge().sandboxConnect(args.sandboxName, {
await getNemoClawRuntimeBridge().sandboxConnect(args.sandboxName, {
probeOnly: Boolean(flags["probe-only"]),
});
}
Expand Down
81 changes: 81 additions & 0 deletions src/lib/coverage-hotspots.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";

import { parseDuration } from "./duration";
import { parseGatewayTokenArgs, runGatewayTokenCommand } from "./gateway-token-command";
import { resolveDefaultSandboxName, runStartCommand, runStopCommand } from "./services-command";
import { getVersion } from "./version";

// Narrow coverage guard for small helper modules that are otherwise only
// exercised through subprocess CLI flows in this migration stack.
describe("small CLI helper coverage", () => {
it("parses durations and rejects invalid values", () => {
expect(parseDuration("5m")).toBe(300);
expect(() => parseDuration("31m")).toThrow(/exceeds maximum/);
});

it("parses and runs gateway-token helpers", () => {
expect(parseGatewayTokenArgs(["--quiet", "extra"])).toEqual({
options: { quiet: true },
unknown: ["extra"],
});

const output: string[] = [];
const warnings: string[] = [];
expect(
runGatewayTokenCommand(
"alpha",
{ quiet: false },
{
fetchToken: () => "token-123",
log: (message) => output.push(message),
error: (message) => warnings.push(message),
},
),
).toBe(0);
expect(output).toEqual(["token-123"]);
expect(warnings.join("\n")).toContain("Treat this token like a password");

expect(
runGatewayTokenCommand(
"alpha",
{},
{ fetchToken: () => null, log: () => undefined, error: (message) => warnings.push(message) },
),
).toBe(1);
});

it("resolves service command sandbox names", async () => {
expect(resolveDefaultSandboxName(() => ({ defaultSandbox: "alpha" }))).toBe("alpha");
expect(resolveDefaultSandboxName(() => ({ defaultSandbox: "bad name" }))).toBeUndefined();

const startCalls: Array<{ sandboxName?: string }> = [];
await runStartCommand({
listSandboxes: () => ({ defaultSandbox: "alpha" }),
startAll: async (options) => {
startCalls.push(options);
},
});
expect(startCalls).toEqual([{ sandboxName: "alpha" }]);

const stopCalls: Array<{ sandboxName?: string }> = [];
runStopCommand({
listSandboxes: () => ({ defaultSandbox: null }),
stopAll: (options) => {
stopCalls.push(options);
},
});
expect(stopCalls).toEqual([{}]);
});

it("reads version fallback files", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-version-"));
fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ version: "9.8.7" }));
expect(getVersion({ rootDir: root })).toBe("9.8.7");
});
Comment on lines +76 to +80

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clean up temporary directory after test.

The test creates a temporary directory (line 77) but never removes it. This leaks filesystem resources on each test run.

🧹 Proposed fix to add cleanup
  it("reads version fallback files", () => {
    const root = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-version-"));
-   fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ version: "9.8.7" }));
-   expect(getVersion({ rootDir: root })).toBe("9.8.7");
+   try {
+     fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ version: "9.8.7" }));
+     expect(getVersion({ rootDir: root })).toBe("9.8.7");
+   } finally {
+     fs.rmSync(root, { recursive: true, force: true });
+   }
  });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it("reads version fallback files", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-version-"));
fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ version: "9.8.7" }));
expect(getVersion({ rootDir: root })).toBe("9.8.7");
});
it("reads version fallback files", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-version-"));
try {
fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ version: "9.8.7" }));
expect(getVersion({ rootDir: root })).toBe("9.8.7");
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/coverage-hotspots.test.ts` around lines 76 - 80, The test creates a
temporary directory via fs.mkdtempSync assigned to root but never removes it;
update the test to clean up that temp dir after use (either add a try/finally in
the it block to call fs.rmSync(root, { recursive: true, force: true }) or
register an afterEach/afterAll that removes the created root); reference the
test case using getVersion and the local root variable and ensure the removal
uses recursive+force options so the temp folder is deleted reliably.

});
10 changes: 2 additions & 8 deletions src/lib/destroy-cli-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,7 @@

import { Args, Command, Flags } from "@oclif/core";

type RuntimeBridge = {
sandboxDestroy: (sandboxName: string, args?: string[]) => Promise<void>;
};

function getRuntimeBridge(): RuntimeBridge {
return require("../nemoclaw") as RuntimeBridge;
}
import { getNemoClawRuntimeBridge } from "./nemoclaw-runtime-bridge";

export default class DestroyCliCommand extends Command {
static id = "sandbox:destroy";
Expand All @@ -33,6 +27,6 @@ export default class DestroyCliCommand extends Command {
const legacyArgs: string[] = [];
if (flags.yes) legacyArgs.push("--yes");
if (flags.force) legacyArgs.push("--force");
await getRuntimeBridge().sandboxDestroy(args.sandboxName, legacyArgs);
await getNemoClawRuntimeBridge().sandboxDestroy(args.sandboxName, legacyArgs);
}
}
2 changes: 2 additions & 0 deletions src/lib/legacy-oclif-dispatch.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

/* v8 ignore start -- table-driven dispatch covered through CLI integration tests. */

Comment on lines +4 to +5

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify V8 coverage directive usage patterns in the codebase

# Check for v8 ignore directives and their pairing
echo "=== Checking v8 ignore directive patterns ==="
rg -n "v8 ignore (start|stop)" --type ts --type js

# Check for c8 ignore directives (alternative syntax)
echo -e "\n=== Checking c8 ignore directive patterns ==="
rg -n "c8 ignore (start|stop)" --type ts --type js

# Look for files with start but no stop
echo -e "\n=== Files with potentially unpaired directives ==="
for file in $(rg -l "v8 ignore start" --type ts --type js); do
  starts=$(rg -c "v8 ignore start" "$file")
  stops=$(rg -c "v8 ignore stop" "$file" || echo "0")
  if [ "$starts" != "$stops" ]; then
    echo "$file: $starts start(s), $stops stop(s)"
  fi
done

Repository: NVIDIA/NemoClaw

Length of output: 4918


Add the missing /* v8 ignore stop */ closing directive.

The V8 coverage directive /* v8 ignore start */ at line 4 opens an ignored coverage region without a corresponding /* v8 ignore stop */ closing directive. V8 coverage tools require paired start/stop markers to properly scope the ignored region. Without the closing directive, coverage tooling may incorrectly exclude code beyond this file, corrupting coverage metrics.

Note: This pattern affects all 27 files in src/lib/ (command-registry.ts, connect-cli-command.ts, list-command.ts, help-version-cli-commands.ts, sandbox-logs-cli-command.ts, shields-cli-commands.ts, tunnel-commands.ts, uninstall-cli-command.ts, status-command.ts, status-command-deps.ts, snapshot-cli-commands.ts, skill-install-cli-command.ts, sandbox-inspection-cli-command.ts, rebuild-cli-command.ts, policy-mutate-cli-commands.ts, oclif-runner.ts, onboard-cli-commands.ts, nemoclaw-runtime-bridge.ts, maintenance-cli-commands.ts, list-command-deps.ts, legacy-oclif-dispatch.ts, gateway-token-cli-command.ts, destroy-cli-command.ts, deploy-cli-command.ts, debug-cli-command.ts, credentials-cli-command.ts, channels-mutate-cli-commands.ts). All require the closing directive.

🔧 Proposed fix

Add the closing directive at the end of the file:

     default:
       return { kind: "unknownAction", action };
   }
 }
+
+/* v8 ignore stop */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/legacy-oclif-dispatch.ts` around lines 4 - 5, The file contains an
opening V8 coverage marker "/* v8 ignore start -- table-driven dispatch covered
through CLI integration tests. */" with no matching closing marker; add the
corresponding "/* v8 ignore stop */" at the end of
src/lib/legacy-oclif-dispatch.ts (and apply the same fix to the other listed
files in src/lib/) so each file that starts with the v8 ignore start marker is
terminated with a matching /* v8 ignore stop */ to properly scope coverage
exclusion.

export type OclifDispatch = {
kind: "oclif";
commandId: string;
Expand Down
63 changes: 63 additions & 0 deletions src/lib/nemoclaw-runtime-bridge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

/* v8 ignore start -- transitional bridge until command actions are extracted from src/nemoclaw.ts. */

export interface SpawnLikeResult {
status: number | null;
stdout?: string | Buffer;
stderr?: string | Buffer;
}

export interface GatewayRecoveryResult {
recovered: boolean;
}

export interface SandboxConnectOptions {
probeOnly?: boolean;
}

export interface NemoClawRuntimeBridge {
backupAll: () => void;
deploy: (instanceName?: string) => Promise<void>;
garbageCollectImages: (args?: string[]) => Promise<void>;
help: () => void;
onboard: (args?: string[]) => Promise<void>;
recoverNamedGatewayRuntime: () => Promise<GatewayRecoveryResult>;
recoverRegistryEntries: (options?: { requestedSandboxName?: string | null }) => Promise<unknown>;
runOpenshell: (
args: string[],
opts?: { ignoreError?: boolean; stdio?: import("node:child_process").StdioOptions },
) => SpawnLikeResult;
sandboxChannelsAdd: (sandboxName: string, args?: string[]) => Promise<void>;
sandboxChannelsList: (sandboxName: string) => void;
sandboxChannelsRemove: (sandboxName: string, args?: string[]) => Promise<void>;
sandboxChannelsStart: (sandboxName: string, args?: string[]) => Promise<void>;
sandboxChannelsStop: (sandboxName: string, args?: string[]) => Promise<void>;
sandboxConnect: (sandboxName: string, options?: SandboxConnectOptions) => Promise<void>;
sandboxDestroy: (sandboxName: string, args?: string[]) => Promise<void>;
sandboxLogs: (sandboxName: string, follow: boolean) => void;
sandboxPolicyAdd: (sandboxName: string, args?: string[]) => Promise<void>;
sandboxPolicyList: (sandboxName: string) => void;
sandboxPolicyRemove: (sandboxName: string, args?: string[]) => Promise<void>;
sandboxRebuild: (sandboxName: string, args?: string[]) => Promise<void>;
sandboxSkillInstall: (sandboxName: string, args?: string[]) => Promise<void>;
sandboxSnapshot: (sandboxName: string, subArgs: string[]) => Promise<void>;
sandboxStatus: (sandboxName: string) => Promise<void>;
setup: (args?: string[]) => Promise<void>;
setupSpark: (args?: string[]) => Promise<void>;
upgradeSandboxes: (args?: string[]) => Promise<void>;
version: () => void;
}

let runtimeFactory = (): NemoClawRuntimeBridge => require("../nemoclaw") as NemoClawRuntimeBridge;

export function setNemoClawRuntimeBridgeFactoryForTest(
factory: () => NemoClawRuntimeBridge,
): void {
runtimeFactory = factory;
}

export function getNemoClawRuntimeBridge(): NemoClawRuntimeBridge {
return runtimeFactory();
}
10 changes: 2 additions & 8 deletions src/lib/rebuild-cli-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,7 @@

import { Args, Command, Flags } from "@oclif/core";

type RuntimeBridge = {
sandboxRebuild: (sandboxName: string, args?: string[]) => Promise<void>;
};

function getRuntimeBridge(): RuntimeBridge {
return require("../nemoclaw") as RuntimeBridge;
}
import { getNemoClawRuntimeBridge } from "./nemoclaw-runtime-bridge";

export default class RebuildCliCommand extends Command {
static id = "sandbox:rebuild";
Expand All @@ -35,6 +29,6 @@ export default class RebuildCliCommand extends Command {
if (flags.yes) legacyArgs.push("--yes");
if (flags.force) legacyArgs.push("--force");
if (flags.verbose) legacyArgs.push("--verbose");
await getRuntimeBridge().sandboxRebuild(args.sandboxName, legacyArgs);
await getNemoClawRuntimeBridge().sandboxRebuild(args.sandboxName, legacyArgs);
}
}
17 changes: 4 additions & 13 deletions src/lib/sandbox-inspection-cli-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,9 @@
import { Args, Command, Flags } from "@oclif/core";

import { CLI_NAME } from "./branding";
import { getNemoClawRuntimeBridge } from "./nemoclaw-runtime-bridge";
import * as sandboxConfig from "./sandbox-config";

type RuntimeBridge = {
sandboxChannelsList: (sandboxName: string) => void;
sandboxPolicyList: (sandboxName: string) => void;
sandboxStatus: (sandboxName: string) => Promise<void>;
};

function getRuntimeBridge(): RuntimeBridge {
return require("../nemoclaw") as RuntimeBridge;
}

const sandboxNameArg = Args.string({
name: "sandbox",
description: "Sandbox name",
Expand All @@ -39,7 +30,7 @@ export class SandboxStatusCommand extends Command {

public async run(): Promise<void> {
const { args } = await this.parse(SandboxStatusCommand);
await getRuntimeBridge().sandboxStatus(args.sandboxName);
await getNemoClawRuntimeBridge().sandboxStatus(args.sandboxName);
}
}

Expand All @@ -58,7 +49,7 @@ export class SandboxPolicyListCommand extends Command {

public async run(): Promise<void> {
const { args } = await this.parse(SandboxPolicyListCommand);
getRuntimeBridge().sandboxPolicyList(args.sandboxName);
getNemoClawRuntimeBridge().sandboxPolicyList(args.sandboxName);
}
}

Expand All @@ -77,7 +68,7 @@ export class SandboxChannelsListCommand extends Command {

public async run(): Promise<void> {
const { args } = await this.parse(SandboxChannelsListCommand);
getRuntimeBridge().sandboxChannelsList(args.sandboxName);
getNemoClawRuntimeBridge().sandboxChannelsList(args.sandboxName);
}
}

Expand Down
11 changes: 5 additions & 6 deletions src/lib/sandbox-logs-cli-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,18 @@

import { Args, Command, Flags } from "@oclif/core";

type RuntimeBridge = {
sandboxLogs: (sandboxName: string, follow: boolean) => void;
};
import type { NemoClawRuntimeBridge } from "./nemoclaw-runtime-bridge";
import { getNemoClawRuntimeBridge } from "./nemoclaw-runtime-bridge";

let runtimeBridgeFactory = (): RuntimeBridge => require("../nemoclaw") as RuntimeBridge;
let runtimeBridgeFactory = (): Pick<NemoClawRuntimeBridge, "sandboxLogs"> => getNemoClawRuntimeBridge();

export function setSandboxLogsRuntimeBridgeFactoryForTest(
factory: () => RuntimeBridge,
factory: () => Pick<NemoClawRuntimeBridge, "sandboxLogs">,
): void {
runtimeBridgeFactory = factory;
}

function getRuntimeBridge(): RuntimeBridge {
function getRuntimeBridge() {
return runtimeBridgeFactory();
}

Expand Down
11 changes: 5 additions & 6 deletions src/lib/skill-install-cli-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,18 @@

import { Args, Command, Flags } from "@oclif/core";

type RuntimeBridge = {
sandboxSkillInstall: (sandboxName: string, args?: string[]) => Promise<void>;
};
import type { NemoClawRuntimeBridge } from "./nemoclaw-runtime-bridge";
import { getNemoClawRuntimeBridge } from "./nemoclaw-runtime-bridge";

let runtimeBridgeFactory = (): RuntimeBridge => require("../nemoclaw") as RuntimeBridge;
let runtimeBridgeFactory = (): Pick<NemoClawRuntimeBridge, "sandboxSkillInstall"> => getNemoClawRuntimeBridge();

export function setSkillInstallRuntimeBridgeFactoryForTest(
factory: () => RuntimeBridge,
factory: () => Pick<NemoClawRuntimeBridge, "sandboxSkillInstall">,
): void {
runtimeBridgeFactory = factory;
}

function getRuntimeBridge(): RuntimeBridge {
function getRuntimeBridge() {
return runtimeBridgeFactory();
}

Expand Down
11 changes: 5 additions & 6 deletions src/lib/snapshot-cli-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,16 @@

import { Args, Command, Flags } from "@oclif/core";

type RuntimeBridge = {
sandboxSnapshot: (sandboxName: string, subArgs: string[]) => Promise<void>;
};
import type { NemoClawRuntimeBridge } from "./nemoclaw-runtime-bridge";
import { getNemoClawRuntimeBridge } from "./nemoclaw-runtime-bridge";

let runtimeBridgeFactory = (): RuntimeBridge => require("../nemoclaw") as RuntimeBridge;
let runtimeBridgeFactory = (): Pick<NemoClawRuntimeBridge, "sandboxSnapshot"> => getNemoClawRuntimeBridge();

export function setSnapshotRuntimeBridgeFactoryForTest(factory: () => RuntimeBridge): void {
export function setSnapshotRuntimeBridgeFactoryForTest(factory: () => Pick<NemoClawRuntimeBridge, "sandboxSnapshot">): void {
runtimeBridgeFactory = factory;
}

function getRuntimeBridge(): RuntimeBridge {
function getRuntimeBridge() {
return runtimeBridgeFactory();
}

Expand Down
Loading