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
5 changes: 4 additions & 1 deletion src/lib/global-cli-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ export function runBackupAllAction(): void {
}

export async function runUpgradeSandboxesAction(args: string[] = []): Promise<void> {
await getNemoClawRuntimeBridge().upgradeSandboxes(args);
const { upgradeSandboxes } = require("./upgrade-sandboxes-action") as {
upgradeSandboxes: (args?: string[]) => Promise<void>;
};
await upgradeSandboxes(args);
}

export async function runGarbageCollectImagesAction(args: string[] = []): Promise<void> {
Expand Down
4 changes: 1 addition & 3 deletions src/lib/nemoclaw-runtime-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@

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

export interface NemoClawRuntimeBridge {
upgradeSandboxes: (args?: string[]) => Promise<void>;
}
export interface NemoClawRuntimeBridge {}

let runtimeFactory = (): NemoClawRuntimeBridge => {
const runtimeModule = require("../nemoclaw") as {
Expand Down
1 change: 0 additions & 1 deletion src/lib/sandbox-runtime-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

import type { SandboxConnectOptions } from "./sandbox-connect-action";
import type { SandboxLogsOptions } from "./sandbox-logs-options";
import { getNemoClawRuntimeBridge } from "./nemoclaw-runtime-bridge";

export async function connectSandbox(
sandboxName: string,
Expand Down
125 changes: 125 additions & 0 deletions src/lib/upgrade-sandboxes-action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

/* v8 ignore start -- exercised through CLI subprocess upgrade tests. */

import { CLI_NAME } from "./branding";
import { prompt as askPrompt } from "./credentials";
import { captureOpenshell } from "./openshell-runtime";
import * as registry from "./registry";
import { parseLiveSandboxNames } from "./runtime-recovery";
import { rebuildSandbox } from "./sandbox-rebuild-action";
import * as sandboxVersion from "./sandbox-version";
import { B, D, G, R, YW } from "./terminal-style";

// ── Upgrade sandboxes (#1904) ────────────────────────────────────
// Detect sandboxes running stale agent versions and offer to rebuild them.

export async function upgradeSandboxes(args: string[] = []): Promise<void> {
const checkOnly = args.includes("--check");
const auto = args.includes("--auto");
const skipConfirm = auto || args.includes("--yes");

const sandboxes = registry.listSandboxes().sandboxes;
if (sandboxes.length === 0) {
console.log(" No sandboxes found in the registry.");
return;
}

// Query live sandboxes so we can tell the user which are running
const liveResult = captureOpenshell(["sandbox", "list"], { ignoreError: true });
if (liveResult.status !== 0) {
console.error(" Failed to query running sandboxes from OpenShell.");
console.error(" Ensure OpenShell is running: openshell status");
process.exit(liveResult.status || 1);
}
const liveNames = parseLiveSandboxNames(liveResult.output || "");

// Classify sandboxes as stale, unknown, or current
const stale = [];
const unknown = [];
for (const sb of sandboxes) {
const versionCheck = sandboxVersion.checkAgentVersion(sb.name);
if (versionCheck.isStale) {
stale.push({
name: sb.name,
current: versionCheck.sandboxVersion,
expected: versionCheck.expectedVersion,
running: liveNames.has(sb.name),
});
} else if (versionCheck.detectionMethod === "unavailable") {
unknown.push({
name: sb.name,
expected: versionCheck.expectedVersion,
running: liveNames.has(sb.name),
});
}
}

if (stale.length === 0 && unknown.length === 0) {
console.log(" All sandboxes are up to date.");
return;
}

if (stale.length > 0) {
console.log(`\n ${B}Stale sandboxes:${R}`);
for (const s of stale) {
const status = s.running ? `${G}running${R}` : `${D}stopped${R}`;
console.log(` ${s.name} v${s.current || "?"} → v${s.expected} (${status})`);
}
}
if (unknown.length > 0) {
console.log(`\n ${YW}Unknown version:${R}`);
for (const s of unknown) {
const status = s.running ? `${G}running${R}` : `${D}stopped${R}`;
console.log(` ${s.name} v? → v${s.expected} (${status})`);
}
}
console.log("");

if (checkOnly) {
if (stale.length > 0) console.log(` ${stale.length} sandbox(es) need upgrading.`);
if (unknown.length > 0) {
console.log(
` ${unknown.length} sandbox(es) could not be version-checked; start them and rerun, or rebuild manually.`,
);
}
console.log(` Run \`${CLI_NAME} upgrade-sandboxes\` to rebuild them.`);
return;
}

const rebuildable = stale.filter((s: { running: boolean }) => s.running);
const stopped = stale.filter((s: { running: boolean }) => !s.running);
if (stopped.length > 0) {
console.log(` ${D}Skipping ${stopped.length} stopped sandbox(es) — start them first.${R}`);
}
if (rebuildable.length === 0) {
console.log(" No running stale sandboxes to rebuild.");
return;
}

let rebuilt = 0;
let failed = 0;
for (const s of rebuildable) {
if (!skipConfirm) {
const answer = await askPrompt(` Rebuild '${s.name}'? [y/N]: `);
if (answer.trim().toLowerCase() !== "y" && answer.trim().toLowerCase() !== "yes") {
console.log(` Skipped '${s.name}'.`);
continue;
}
}
try {
await rebuildSandbox(s.name, ["--yes"], { throwOnError: true });
rebuilt++;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
console.error(` ${YW}⚠${R} Failed to rebuild '${s.name}': ${errorMessage}`);
failed++;
}
}

console.log("");
if (rebuilt > 0) console.log(` ${G}✓${R} ${rebuilt} sandbox(es) rebuilt.`);
if (failed > 0) console.log(` ${YW}⚠${R} ${failed} sandbox(es) failed — see errors above.`);
if (failed > 0) process.exit(1);
}
119 changes: 0 additions & 119 deletions src/nemoclaw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ const {
} = require("./lib/docker");
const { resolveOpenshell } = require("./lib/resolve-openshell");
const { hydrateCredentialEnv, isNonInteractive } = require("./lib/onboard");
const { prompt: askPrompt } = require("./lib/credentials");
const registry = require("./lib/registry");
import type { SandboxEntry } from "./lib/registry";
const nim = require("./lib/nim");
Expand All @@ -48,10 +47,8 @@ const { buildStatusCommandDeps } = require("./lib/status-command-deps");
const { help, version } = require("./lib/root-help-action");
const onboardSession = require("./lib/onboard-session");
import type { Session } from "./lib/onboard-session";
const { parseLiveSandboxNames } = require("./lib/runtime-recovery");
const { stripAnsi } = require("./lib/openshell");
const {
captureOpenshell,
getInstalledOpenshellVersionOrNull,
runOpenshell,
} = require("./lib/openshell-runtime");
Expand All @@ -70,11 +67,9 @@ const {
const {
getSandboxDeleteOutcome,
} = require("./lib/sandbox-destroy-action");
const { rebuildSandbox: sandboxRebuild } = require("./lib/sandbox-rebuild-action");
const { runRegisteredOclifCommand } = require("./lib/oclif-runner");
const { isErrnoException }: typeof import("./lib/errno") = require("./lib/errno");
const agentRuntime = require("../bin/lib/agent-runtime");
const sandboxVersion = require("./lib/sandbox-version");
const sandboxState = require("./lib/sandbox-state");
const { parseRestoreArgs } = sandboxState;
const {
Expand Down Expand Up @@ -119,9 +114,6 @@ const DASHBOARD_FORWARD_PORT = String(DASHBOARD_PORT);
const DEFAULT_LOGS_PROBE_TIMEOUT_MS = 5000;
const LOGS_PROBE_TIMEOUT_ENV = "NEMOCLAW_LOGS_PROBE_TIMEOUT_MS";

exports.runtimeBridge = {
upgradeSandboxes,
};
/** Print user-facing guidance when OpenShell is too old to support `openshell logs`. */
function printOldLogsCompatibilityGuidance(installedVersion = null) {
const versionText = installedVersion ? ` (${installedVersion})` : "";
Expand Down Expand Up @@ -150,117 +142,6 @@ function printSandboxActionUsage(action: string): void {
console.log(` Usage: ${CLI_NAME} <name> ${action}`);
}

// ── Rebuild ──────────────────────────────────────────────────────

async function upgradeSandboxes(args: string[] = []): Promise<void> {
const checkOnly = args.includes("--check");
const auto = args.includes("--auto");
const skipConfirm = auto || args.includes("--yes");

const sandboxes = registry.listSandboxes().sandboxes;
if (sandboxes.length === 0) {
console.log(" No sandboxes found in the registry.");
return;
}

// Query live sandboxes so we can tell the user which are running
const liveResult = captureOpenshell(["sandbox", "list"], { ignoreError: true });
if (liveResult.status !== 0) {
console.error(" Failed to query running sandboxes from OpenShell.");
console.error(" Ensure OpenShell is running: openshell status");
process.exit(liveResult.status || 1);
}
const liveNames = parseLiveSandboxNames(liveResult.output || "");

// Classify sandboxes as stale, unknown, or current
const stale = [];
const unknown = [];
for (const sb of sandboxes) {
const versionCheck = sandboxVersion.checkAgentVersion(sb.name);
if (versionCheck.isStale) {
stale.push({
name: sb.name,
current: versionCheck.sandboxVersion,
expected: versionCheck.expectedVersion,
running: liveNames.has(sb.name),
});
} else if (versionCheck.detectionMethod === "unavailable") {
unknown.push({
name: sb.name,
expected: versionCheck.expectedVersion,
running: liveNames.has(sb.name),
});
}
}

if (stale.length === 0 && unknown.length === 0) {
console.log(" All sandboxes are up to date.");
return;
}

if (stale.length > 0) {
console.log(`\n ${B}Stale sandboxes:${R}`);
for (const s of stale) {
const status = s.running ? `${G}running${R}` : `${D}stopped${R}`;
console.log(` ${s.name} v${s.current || "?"} → v${s.expected} (${status})`);
}
}
if (unknown.length > 0) {
console.log(`\n ${YW}Unknown version:${R}`);
for (const s of unknown) {
const status = s.running ? `${G}running${R}` : `${D}stopped${R}`;
console.log(` ${s.name} v? → v${s.expected} (${status})`);
}
}
console.log("");

if (checkOnly) {
if (stale.length > 0) console.log(` ${stale.length} sandbox(es) need upgrading.`);
if (unknown.length > 0) {
console.log(
` ${unknown.length} sandbox(es) could not be version-checked; start them and rerun, or rebuild manually.`,
);
}
console.log(` Run \`${CLI_NAME} upgrade-sandboxes\` to rebuild them.`);
return;
}

const rebuildable = stale.filter((s: { running: boolean }) => s.running);
const stopped = stale.filter((s: { running: boolean }) => !s.running);
if (stopped.length > 0) {
console.log(` ${D}Skipping ${stopped.length} stopped sandbox(es) — start them first.${R}`);
}
if (rebuildable.length === 0) {
console.log(" No running stale sandboxes to rebuild.");
return;
}

let rebuilt = 0;
let failed = 0;
for (const s of rebuildable) {
if (!skipConfirm) {
const answer = await askPrompt(` Rebuild '${s.name}'? [y/N]: `);
if (answer.trim().toLowerCase() !== "y" && answer.trim().toLowerCase() !== "yes") {
console.log(` Skipped '${s.name}'.`);
continue;
}
}
try {
await sandboxRebuild(s.name, ["--yes"], { throwOnError: true });
rebuilt++;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
console.error(` ${YW}\u26a0${R} Failed to rebuild '${s.name}': ${errorMessage}`);
failed++;
}
}

console.log("");
if (rebuilt > 0) console.log(` ${G}\u2713${R} ${rebuilt} sandbox(es) rebuilt.`);
if (failed > 0) console.log(` ${YW}\u26a0${R} ${failed} sandbox(es) failed — see errors above.`);
if (failed > 0) process.exit(1);
}

// ── Pre-upgrade backup ───────────────────────────────────────────

// ── Snapshot ─────────────────────────────────────────────────────
Expand Down
Loading