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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 15 additions & 66 deletions bin/nemoclaw.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const {
stripAnsi,
versionGte,
} = require("../dist/lib/openshell");
const { listSandboxesCommand, showStatusCommand } = require("../dist/lib/inventory-commands");
const { executeDeploy } = require("../dist/lib/deploy");

// ── Global commands ──────────────────────────────────────────────
Expand Down Expand Up @@ -909,76 +910,24 @@ function uninstall(args) {
}

function showStatus() {
// Show sandbox registry
const { sandboxes, defaultSandbox } = registry.listSandboxes();
if (sandboxes.length > 0) {
const live = parseGatewayInference(
captureOpenshell(["inference", "get"], { ignoreError: true }).output,
);
console.log("");
console.log(" Sandboxes:");
for (const sb of sandboxes) {
const def = sb.name === defaultSandbox ? " *" : "";
const model = (live && live.model) || sb.model;
console.log(` ${sb.name}${def}${model ? ` (${model})` : ""}`);
}
console.log("");
}

// Show service status
const { showStatus: showServiceStatus } = require("./lib/services");
showServiceStatus({ sandboxName: defaultSandbox || undefined });
showStatusCommand({
listSandboxes: () => registry.listSandboxes(),
getLiveInference: () =>
parseGatewayInference(captureOpenshell(["inference", "get"], { ignoreError: true }).output),
showServiceStatus,
log: console.log,
});
}

async function listSandboxes() {
const recovery = await recoverRegistryEntries();
const { sandboxes, defaultSandbox } = recovery;
if (sandboxes.length === 0) {
console.log("");
const session = onboardSession.loadSession();
if (session?.sandboxName) {
console.log(
` No sandboxes registered locally, but the last onboarded sandbox was '${session.sandboxName}'.`,
);
console.log(
" Retry `nemoclaw <name> connect` or `nemoclaw <name> status` once the gateway/runtime is healthy.",
);
} else {
console.log(" No sandboxes registered. Run `nemoclaw onboard` to get started.");
}
console.log("");
return;
}

// Query live gateway inference once; prefer it over stale registry values.
const live = parseGatewayInference(
captureOpenshell(["inference", "get"], { ignoreError: true }).output,
);

console.log("");
if (recovery.recoveredFromSession) {
console.log(" Recovered sandbox inventory from the last onboard session.");
console.log("");
}
if (recovery.recoveredFromGateway > 0) {
console.log(
` Recovered ${recovery.recoveredFromGateway} sandbox entr${recovery.recoveredFromGateway === 1 ? "y" : "ies"} from the live OpenShell gateway.`,
);
console.log("");
}
console.log(" Sandboxes:");
for (const sb of sandboxes) {
const def = sb.name === defaultSandbox ? " *" : "";
const model = (live && live.model) || sb.model || "unknown";
const provider = (live && live.provider) || sb.provider || "unknown";
const gpu = sb.gpuEnabled ? "GPU" : "CPU";
const presets = sb.policies && sb.policies.length > 0 ? sb.policies.join(", ") : "none";
console.log(` ${sb.name}${def}`);
console.log(` model: ${model} provider: ${provider} ${gpu} policies: ${presets}`);
}
console.log("");
console.log(" * = default sandbox");
console.log("");
await listSandboxesCommand({
recoverRegistryEntries: () => recoverRegistryEntries(),
getLiveInference: () =>
parseGatewayInference(captureOpenshell(["inference", "get"], { ignoreError: true }).output),
loadLastSession: () => onboardSession.loadSession(),
log: console.log,
});
}

// ── Sandbox-scoped actions ───────────────────────────────────────
Expand Down
75 changes: 75 additions & 0 deletions src/lib/inventory-commands.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, expect, it, vi } from "vitest";

import { listSandboxesCommand, showStatusCommand } from "../../dist/lib/inventory-commands";

describe("inventory commands", () => {
it("prints the empty-state onboarding hint when no sandboxes exist", async () => {
const lines: string[] = [];
await listSandboxesCommand({
recoverRegistryEntries: async () => ({ sandboxes: [], defaultSandbox: null }),
getLiveInference: () => null,
loadLastSession: () => ({ sandboxName: "alpha" }),
log: (message = "") => lines.push(message),
});

expect(lines).toContain(
" No sandboxes registered locally, but the last onboarded sandbox was 'alpha'.",
);
});

it("prints recovered sandbox inventory details", async () => {
const lines: string[] = [];
await listSandboxesCommand({
recoverRegistryEntries: async () => ({
sandboxes: [
{
name: "alpha",
model: "nvidia/nemotron-3-super-120b-a12b",
provider: "nvidia-prod",
gpuEnabled: true,
policies: ["pypi"],
},
],
defaultSandbox: "alpha",
recoveredFromSession: true,
recoveredFromGateway: 1,
}),
getLiveInference: () => null,
loadLastSession: () => null,
log: (message = "") => lines.push(message),
});

expect(lines).toContain(" Recovered sandbox inventory from the last onboard session.");
expect(lines).toContain(" Recovered 1 sandbox entry from the live OpenShell gateway.");
expect(lines).toContain(" alpha *");
expect(lines).toContain(
" model: nvidia/nemotron-3-super-120b-a12b provider: nvidia-prod GPU policies: pypi",
);
});

it("prints the top-level status inventory and delegates service status", () => {
const lines: string[] = [];
const showServiceStatus = vi.fn();
showStatusCommand({
listSandboxes: () => ({
sandboxes: [
{
name: "alpha",
model: "nvidia/nemotron-3-super-120b-a12b",
},
],
defaultSandbox: "alpha",
}),
getLiveInference: () => ({ provider: "nvidia-prod", model: "moonshotai/kimi-k2.5" }),
showServiceStatus,
log: (message = "") => lines.push(message),
});

expect(lines).toContain(" Sandboxes:");
expect(lines).toContain(" alpha * (moonshotai/kimi-k2.5)");
expect(showServiceStatus).toHaveBeenCalledWith({ sandboxName: "alpha" });
});
});
103 changes: 103 additions & 0 deletions src/lib/inventory-commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

export interface SandboxEntry {
name: string;
model?: string | null;
provider?: string | null;
gpuEnabled?: boolean;
policies?: string[] | null;
}

export interface RecoveryResult {
sandboxes: SandboxEntry[];
defaultSandbox?: string | null;
recoveredFromSession?: boolean;
recoveredFromGateway?: number;
}

export interface GatewayInference {
provider: string | null;
model: string | null;
}

export interface ListSandboxesCommandDeps {
recoverRegistryEntries: () => Promise<RecoveryResult>;
getLiveInference: () => GatewayInference | null;
loadLastSession: () => { sandboxName?: string | null } | null;
log?: (message?: string) => void;
}

export interface ShowStatusCommandDeps {
listSandboxes: () => { sandboxes: SandboxEntry[]; defaultSandbox?: string | null };
getLiveInference: () => GatewayInference | null;
showServiceStatus: (options: { sandboxName?: string }) => void;
log?: (message?: string) => void;
}

export async function listSandboxesCommand(deps: ListSandboxesCommandDeps): Promise<void> {
const log = deps.log ?? console.log;
const recovery = await deps.recoverRegistryEntries();
const { sandboxes, defaultSandbox } = recovery;

if (sandboxes.length === 0) {
log("");
const session = deps.loadLastSession();
if (session?.sandboxName) {
log(
` No sandboxes registered locally, but the last onboarded sandbox was '${session.sandboxName}'.`,
);
log(
" Retry `nemoclaw <name> connect` or `nemoclaw <name> status` once the gateway/runtime is healthy.",
);
} else {
log(" No sandboxes registered. Run `nemoclaw onboard` to get started.");
}
log("");
return;
}

const live = deps.getLiveInference();

log("");
if (recovery.recoveredFromSession) {
log(" Recovered sandbox inventory from the last onboard session.");
log("");
}
if ((recovery.recoveredFromGateway || 0) > 0) {
const count = recovery.recoveredFromGateway || 0;
log(` Recovered ${count} sandbox entr${count === 1 ? "y" : "ies"} from the live OpenShell gateway.`);
log("");
}
log(" Sandboxes:");
for (const sb of sandboxes) {
const def = sb.name === defaultSandbox ? " *" : "";
const model = (live && live.model) || sb.model || "unknown";
const provider = (live && live.provider) || sb.provider || "unknown";
const gpu = sb.gpuEnabled ? "GPU" : "CPU";
const presets = sb.policies && sb.policies.length > 0 ? sb.policies.join(", ") : "none";
log(` ${sb.name}${def}`);
log(` model: ${model} provider: ${provider} ${gpu} policies: ${presets}`);
}
log("");
log(" * = default sandbox");
log("");
}

export function showStatusCommand(deps: ShowStatusCommandDeps): void {
const log = deps.log ?? console.log;
const { sandboxes, defaultSandbox } = deps.listSandboxes();
if (sandboxes.length > 0) {
const live = deps.getLiveInference();
log("");
log(" Sandboxes:");
for (const sb of sandboxes) {
const def = sb.name === defaultSandbox ? " *" : "";
const model = (live && live.model) || sb.model;
log(` ${sb.name}${def}${model ? ` (${model})` : ""}`);
}
log("");
}

deps.showServiceStatus({ sandboxName: defaultSandbox || undefined });
}
Loading