Skip to content
Open
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
88 changes: 36 additions & 52 deletions bin/nemoclaw.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ const {
} = require("../dist/lib/openshell");
const { listSandboxesCommand, showStatusCommand } = require("../dist/lib/inventory-commands");
const { executeDeploy } = require("../dist/lib/deploy");
const {
runDeprecatedOnboardAliasCommand,
runOnboardCommand,
} = require("../dist/lib/onboard-command");
const { runStartCommand, runStopCommand } = require("../dist/lib/services-command");
const {
buildVersionedUninstallUrl,
Expand Down Expand Up @@ -761,65 +765,45 @@ function exitWithSpawnResult(result) {

async function onboard(args) {
const { onboard: runOnboard } = require("./lib/onboard");

// Extract --from <path> before the unknown-arg validator: it takes a value
// so the set-based check would reject the value token as an unknown flag.
let fromDockerfile = null;
const fromIdx = args.indexOf("--from");
if (fromIdx !== -1) {
fromDockerfile = args[fromIdx + 1];
if (!fromDockerfile || fromDockerfile.startsWith("--")) {
console.error(" --from requires a path to a Dockerfile");
console.error(
` Usage: nemoclaw onboard [--non-interactive] [--resume] [--recreate-sandbox] [--from <Dockerfile>] [${NOTICE_ACCEPT_FLAG}]`,
);
process.exit(1);
}
args = [...args.slice(0, fromIdx), ...args.slice(fromIdx + 2)];
}

const allowedArgs = new Set([
"--non-interactive",
"--resume",
"--recreate-sandbox",
NOTICE_ACCEPT_FLAG,
]);
const unknownArgs = args.filter((arg) => !allowedArgs.has(arg));
if (unknownArgs.length > 0) {
console.error(` Unknown onboard option(s): ${unknownArgs.join(", ")}`);
console.error(
` Usage: nemoclaw onboard [--non-interactive] [--resume] [--recreate-sandbox] [--from <Dockerfile>] [${NOTICE_ACCEPT_FLAG}]`,
);
process.exit(1);
}
const nonInteractive = args.includes("--non-interactive");
const resume = args.includes("--resume");
const recreateSandbox = args.includes("--recreate-sandbox");
const acceptThirdPartySoftware =
args.includes(NOTICE_ACCEPT_FLAG) || String(process.env[NOTICE_ACCEPT_ENV] || "") === "1";
await runOnboard({
nonInteractive,
resume,
recreateSandbox,
fromDockerfile,
acceptThirdPartySoftware,
await runOnboardCommand({
args,
noticeAcceptFlag: NOTICE_ACCEPT_FLAG,
noticeAcceptEnv: NOTICE_ACCEPT_ENV,
env: process.env,
runOnboard,
error: console.error,
exit: (code) => process.exit(code),
});
}

async function setup(args = []) {
console.log("");
console.log(" ⚠ `nemoclaw setup` is deprecated. Use `nemoclaw onboard` instead.");
console.log("");
await onboard(args);
const { onboard: runOnboard } = require("./lib/onboard");
await runDeprecatedOnboardAliasCommand({
kind: "setup",
args,
noticeAcceptFlag: NOTICE_ACCEPT_FLAG,
noticeAcceptEnv: NOTICE_ACCEPT_ENV,
env: process.env,
runOnboard,
log: console.log,
error: console.error,
exit: (code) => process.exit(code),
});
}

async function setupSpark(args = []) {
console.log("");
console.log(" ⚠ `nemoclaw setup-spark` is deprecated.");
console.log(" Current OpenShell releases handle the old DGX Spark cgroup issue themselves.");
console.log(" Use `nemoclaw onboard` instead.");
console.log("");
await onboard(args);
const { onboard: runOnboard } = require("./lib/onboard");
await runDeprecatedOnboardAliasCommand({
kind: "setup-spark",
args,
noticeAcceptFlag: NOTICE_ACCEPT_FLAG,
noticeAcceptEnv: NOTICE_ACCEPT_ENV,
env: process.env,
runOnboard,
log: console.log,
error: console.error,
exit: (code) => process.exit(code),
});
}

async function deploy(instanceName) {
Expand Down
154 changes: 154 additions & 0 deletions src/lib/onboard-command.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// 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 {
parseOnboardArgs,
runDeprecatedOnboardAliasCommand,
runOnboardCommand,
} from "../../dist/lib/onboard-command";

describe("onboard command", () => {
it("parses onboard flags", () => {
expect(
parseOnboardArgs(
["--non-interactive", "--resume", "--yes-i-accept-third-party-software"],
"--yes-i-accept-third-party-software",
"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
{ env: {}, error: () => {}, exit: ((code: number) => { throw new Error(String(code)); }) as never },
),
).toEqual({
nonInteractive: true,
resume: true,
recreateSandbox: false,
fromDockerfile: null,
acceptThirdPartySoftware: true,
});
});

it("accepts the env-based third-party notice acknowledgement", () => {
expect(
parseOnboardArgs(
[],
"--yes-i-accept-third-party-software",
"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
{
env: { NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" },
error: () => {},
exit: ((code: number) => {
throw new Error(String(code));
}) as never,
},
),
).toEqual({
nonInteractive: false,
resume: false,
recreateSandbox: false,
fromDockerfile: null,
acceptThirdPartySoftware: true,
});
});

it("runs onboard with parsed options", async () => {
const runOnboard = vi.fn(async () => {});
await runOnboardCommand({
args: ["--resume"],
noticeAcceptFlag: "--yes-i-accept-third-party-software",
noticeAcceptEnv: "NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
env: {},
runOnboard,
error: () => {},
exit: ((code: number) => {
throw new Error(String(code));
}) as never,
});
expect(runOnboard).toHaveBeenCalledWith({
nonInteractive: false,
resume: true,
recreateSandbox: false,
fromDockerfile: null,
acceptThirdPartySoftware: false,
});
});

it("prints usage and skips onboarding for --help", async () => {
const runOnboard = vi.fn(async () => {});
const lines: string[] = [];
await runOnboardCommand({
args: ["--help"],
noticeAcceptFlag: "--yes-i-accept-third-party-software",
noticeAcceptEnv: "NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
env: {},
runOnboard,
log: (message = "") => lines.push(message),
error: () => {},
exit: ((code: number) => {
throw new Error(String(code));
}) as never,
});
expect(runOnboard).not.toHaveBeenCalled();
expect(lines.join("\n")).toContain("Usage: nemoclaw onboard");
expect(lines.join("\n")).toContain("--from <Dockerfile>");
});

it("parses --from <Dockerfile>", () => {
expect(
parseOnboardArgs(
["--resume", "--from", "/tmp/Custom.Dockerfile"],
"--yes-i-accept-third-party-software",
"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
{
env: {},
error: () => {},
exit: ((code: number) => {
throw new Error(String(code));
}) as never,
},
),
).toEqual({
nonInteractive: false,
resume: true,
recreateSandbox: false,
fromDockerfile: "/tmp/Custom.Dockerfile",
acceptThirdPartySoftware: false,
});
});

it("exits when --from is missing its Dockerfile path", () => {
expect(() =>
parseOnboardArgs(
["--from"],
"--yes-i-accept-third-party-software",
"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
{
env: {},
error: () => {},
exit: ((code: number) => {
throw new Error(`exit:${code}`);
}) as never,
},
),
).toThrow("exit:1");
});

it("prints the setup-spark deprecation text before delegating", async () => {
const lines: string[] = [];
const runOnboard = vi.fn(async () => {});
await runDeprecatedOnboardAliasCommand({
kind: "setup-spark",
args: [],
noticeAcceptFlag: "--yes-i-accept-third-party-software",
noticeAcceptEnv: "NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
env: {},
runOnboard,
log: (message = "") => lines.push(message),
error: () => {},
exit: ((code: number) => {
throw new Error(String(code));
}) as never,
});
expect(lines.join("\n")).toContain("setup-spark` is deprecated");
expect(lines.join("\n")).toContain("Use `nemoclaw onboard` instead");
});
});
111 changes: 111 additions & 0 deletions src/lib/onboard-command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

export interface OnboardCommandOptions {
nonInteractive: boolean;
resume: boolean;
recreateSandbox: boolean;
fromDockerfile: string | null;
acceptThirdPartySoftware: boolean;
}

export interface RunOnboardCommandDeps {
args: string[];
noticeAcceptFlag: string;
noticeAcceptEnv: string;
env: NodeJS.ProcessEnv;
runOnboard: (options: OnboardCommandOptions) => Promise<void>;
log?: (message?: string) => void;
error?: (message?: string) => void;
exit?: (code: number) => never;
}

const ONBOARD_BASE_ARGS = ["--non-interactive", "--resume", "--recreate-sandbox"];

function onboardUsageLines(noticeAcceptFlag: string): string[] {
return [
` Usage: nemoclaw onboard [--non-interactive] [--resume] [--recreate-sandbox] [--from <Dockerfile>] [${noticeAcceptFlag}]`,
"",
" Options:",
" --non-interactive Run without prompts",
" --resume Resume a saved onboarding session",
" --recreate-sandbox Destroy and recreate the sandbox",
" --from <Dockerfile> Build the sandbox image from a Dockerfile",
` ${noticeAcceptFlag} Accept the third-party software notice for non-interactive runs`,
];
}

function printOnboardUsage(writer: (message?: string) => void, noticeAcceptFlag: string): void {
for (const line of onboardUsageLines(noticeAcceptFlag)) {
writer(line);
}
}

export function parseOnboardArgs(
args: string[],
noticeAcceptFlag: string,
noticeAcceptEnv: string,
deps: Pick<RunOnboardCommandDeps, "env" | "error" | "exit">,
): OnboardCommandOptions {
const error = deps.error ?? console.error;
const exit = deps.exit ?? ((code: number) => process.exit(code));
let fromDockerfile: string | null = null;
const fromIdx = args.indexOf("--from");
if (fromIdx !== -1) {
fromDockerfile = args[fromIdx + 1] || null;
if (!fromDockerfile || fromDockerfile.startsWith("--")) {
error(" --from requires a path to a Dockerfile");
printOnboardUsage(error, noticeAcceptFlag);
exit(1);
}
args = [...args.slice(0, fromIdx), ...args.slice(fromIdx + 2)];
}

const allowedArgs = new Set([...ONBOARD_BASE_ARGS, noticeAcceptFlag]);
const unknownArgs = args.filter((arg) => !allowedArgs.has(arg));
if (unknownArgs.length > 0) {
error(` Unknown onboard option(s): ${unknownArgs.join(", ")}`);
printOnboardUsage(error, noticeAcceptFlag);
exit(1);
}

return {
nonInteractive: args.includes("--non-interactive"),
resume: args.includes("--resume"),
recreateSandbox: args.includes("--recreate-sandbox"),
fromDockerfile,
acceptThirdPartySoftware:
args.includes(noticeAcceptFlag) || String(deps.env[noticeAcceptEnv] || "") === "1",
};
}

export async function runOnboardCommand(deps: RunOnboardCommandDeps): Promise<void> {
const log = deps.log ?? console.log;
if (deps.args.includes("--help") || deps.args.includes("-h")) {
printOnboardUsage(log, deps.noticeAcceptFlag);
return;
}

const options = parseOnboardArgs(deps.args, deps.noticeAcceptFlag, deps.noticeAcceptEnv, deps);
await deps.runOnboard(options);
}

export interface RunAliasCommandDeps extends RunOnboardCommandDeps {
kind: "setup" | "setup-spark";
}

export async function runDeprecatedOnboardAliasCommand(
deps: RunAliasCommandDeps,
): Promise<void> {
const log = deps.log ?? console.log;
log("");
if (deps.kind === "setup") {
log(" ⚠ `nemoclaw setup` is deprecated. Use `nemoclaw onboard` instead.");
} else {
log(" ⚠ `nemoclaw setup-spark` is deprecated.");
log(" Current OpenShell releases handle the old DGX Spark cgroup issue themselves.");
log(" Use `nemoclaw onboard` instead.");
}
log("");
await runOnboardCommand(deps);
}
Loading
Loading