mirror of
https://github.com/NVIDIA/NemoClaw.git
synced 2026-07-03 03:37:16 +00:00
refactor(onboard): finish oclif flag migration (#5916)
<!-- markdownlint-disable MD041 --> ## Summary Finish the onboarding oclif migration by passing parsed flags through a typed command boundary instead of reconstructing argv and parsing it again. This removes the legacy parser and alias-specific action plumbing while preserving domain validation and the public sandbox-first dispatcher. ## Changes - replace the `oclif flags -> string[] -> legacy parser` round trip with typed option resolution - move GPU flag exclusivity and device dependencies into oclif metadata while retaining file, agent-alias, manifest, and notice validation in the onboarding boundary - route `setup` and `setup-spark` through the shared onboard action and oclif's native, alias-aware deprecation support - delete the manual parser, manual usage renderer, and deprecated alias action facade, for 804 net lines removed - replace parser-internal tests with typed resolver, command-boundary, integration, package-contract, and alias-branding coverage - cap every function in the affected production cluster at cognitive complexity 10 ## Type of Change - [x] Code change (feature, bug fix, or refactor) - [ ] Code change with doc updates - [ ] Doc only (prose changes, no code sample modifications) - [ ] Doc only (includes code sample changes) ## Quality Gates <!-- Check all that apply. For any "covered by existing tests", "not applicable", or waiver entry, add a brief justification on the same line or in the Changes section. --> - [x] Tests added or updated for changed behavior - [x] Existing tests cover changed behavior — justification: the full CLI integration suite continues to cover onboarding, deprecated aliases, custom Dockerfiles, resume behavior, and agent-specific launchers - [ ] Tests not applicable — justification: - [ ] Docs updated for user-facing behavior changes - [x] Docs not applicable — justification: public commands, flags, and documented semantics are unchanged; existing docs already state that aliases share onboard flags and GPU device selection requires `--sandbox-gpu` - [x] Sensitive paths changed (security, policy, credentials, preflight, onboarding, inference, runner, sandbox, or messaging) - [x] Sensitive-path review completed or maintainer-approved waiver recorded — reviewer/approval link/justification: reviewed the typed mapping against every legacy parser branch; targeted, coverage-ratchet, and full-matrix verification passed - [ ] Non-success, skipped, or missing CI check accepted by maintainer — check name, approval link, and follow-up issue: ## Verification <!-- Check each item you ran and confirmed. Leave unchecked items you skipped. Doc-only changes do not require npm test unless you ran it. --> - [x] PR description includes the DCO sign-off declaration and every commit appears as `Verified` in GitHub - [x] Git hooks passed during commit and push, or `npx prek run --from-ref main --to-ref HEAD` passes - [x] Targeted tests pass for changed behavior - [x] Full `npm test` passes (broad runtime changes only) - [x] Quality Gates section completed with required justifications or waivers - [x] No secrets, API keys, or credentials committed - [ ] `npm run docs` builds without warnings (doc changes only) - [ ] Doc pages follow the [style guide](https://github.com/NVIDIA/NemoClaw/blob/main/docs/CONTRIBUTING.md) (doc changes only) - [ ] New doc pages include SPDX header and frontmatter (new pages only) --- <!-- DCO sign-off is required in this PR description, and every commit must appear as Verified in GitHub. Run: git config user.name && git config user.email --> Signed-off-by: Carlos Villela <cvillela@nvidia.com> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Onboarding now uses a unified, typed flag flow across commands, improving handling for GPU options, agent selection, and `--from`/`--agents` inputs. * **Bug Fixes** * Strengthened validation for incompatible GPU and sandbox GPU device combinations, improved errors for missing/invalid agent manifests and invalid Dockerfile `--from` paths, and tightened `--control-ui-port` checks. * **Chores** * Updated deprecated-alias messaging and help for `setup` and `setup-spark` to consistently direct to `onboard`. * **Tests** * Updated/expanded onboarding and compatibility test coverage to match the new typed-flag behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
30261d5d1b
commit
047cc89e8e
21 changed files with 583 additions and 1387 deletions
22
biome.json
22
biome.json
|
|
@ -182,6 +182,28 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"includes": [
|
||||
"src/commands/onboard.ts",
|
||||
"src/commands/setup.ts",
|
||||
"src/commands/setup-spark.ts",
|
||||
"src/lib/actions/onboard.ts",
|
||||
"src/lib/onboard/command.ts",
|
||||
"src/lib/onboard/command-support.ts"
|
||||
],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"complexity": {
|
||||
"noExcessiveCognitiveComplexity": {
|
||||
"level": "error",
|
||||
"options": {
|
||||
"maxAllowedComplexity": 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"includes": ["nemoclaw/src/**/*.ts"],
|
||||
"linter": {
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@ const mocks = vi.hoisted(() => ({
|
|||
runInferenceGet: vi.fn(),
|
||||
runInferenceSet: vi.fn(),
|
||||
runOnboardAction: vi.fn(),
|
||||
runSetupAction: vi.fn(),
|
||||
runSetupSparkAction: vi.fn(),
|
||||
runUpgradeSandboxesAction: vi.fn(),
|
||||
showStatusCommand: vi.fn(),
|
||||
}));
|
||||
|
|
@ -39,8 +37,6 @@ vi.mock("../lib/actions/global", () => ({
|
|||
runBackupAllAction: mocks.runBackupAllAction,
|
||||
runGarbageCollectImagesAction: mocks.runGarbageCollectImagesAction,
|
||||
runOnboardAction: mocks.runOnboardAction,
|
||||
runSetupAction: mocks.runSetupAction,
|
||||
runSetupSparkAction: mocks.runSetupSparkAction,
|
||||
runUpgradeSandboxesAction: mocks.runUpgradeSandboxesAction,
|
||||
}));
|
||||
|
||||
|
|
@ -70,16 +66,16 @@ vi.mock("../lib/actions/inference-get", () => ({
|
|||
|
||||
import { InferenceGetError } from "../lib/actions/inference-get";
|
||||
import { InferenceSetError } from "../lib/actions/inference-set";
|
||||
import BackupAllCommand from "./backup-all";
|
||||
import GarbageCollectImagesCommand from "./gc";
|
||||
import InferenceGetCommand from "./inference/get";
|
||||
import InferenceSetCommand from "./inference/set";
|
||||
import ListCommand from "./list";
|
||||
import BackupAllCommand from "./backup-all";
|
||||
import GarbageCollectImagesCommand from "./gc";
|
||||
import UpgradeSandboxesCommand from "./upgrade-sandboxes";
|
||||
import OnboardCliCommand from "./onboard";
|
||||
import SetupCliCommand from "./setup";
|
||||
import SetupSparkCliCommand from "./setup-spark";
|
||||
import StatusCommand from "./status";
|
||||
import UpgradeSandboxesCommand from "./upgrade-sandboxes";
|
||||
|
||||
const rootDir = process.cwd();
|
||||
|
||||
|
|
@ -211,14 +207,20 @@ describe("global oclif command adapters", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("maps onboard-family flags into the compatibility action arguments", async () => {
|
||||
it("maps onboard-family flags directly into the shared typed action", async () => {
|
||||
await OnboardCliCommand.run(["--name", "alpha", "--resume"], rootDir);
|
||||
await SetupCliCommand.run(["--fresh"], rootDir);
|
||||
await SetupCliCommand.run(["--name", "alpha", "--resume"], rootDir);
|
||||
await SetupSparkCliCommand.run(["--control-ui-port", "18080"], rootDir);
|
||||
|
||||
expect(mocks.runOnboardAction).toHaveBeenCalledWith(["--resume", "--name", "alpha"]);
|
||||
expect(mocks.runSetupAction).toHaveBeenCalledWith(["--fresh"]);
|
||||
expect(mocks.runSetupSparkAction).toHaveBeenCalledWith(["--control-ui-port", "18080"]);
|
||||
expect(mocks.runOnboardAction).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({ name: "alpha", resume: true }),
|
||||
);
|
||||
expect(mocks.runOnboardAction.mock.calls[1]).toEqual(mocks.runOnboardAction.mock.calls[0]);
|
||||
expect(mocks.runOnboardAction).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
expect.objectContaining({ "control-ui-port": 18080 }),
|
||||
);
|
||||
});
|
||||
|
||||
it("maps inference set flags into the inference action", async () => {
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ import OnboardCliCommand from "./onboard";
|
|||
|
||||
vi.mock("../lib/actions/global", () => ({
|
||||
runOnboardAction: vi.fn().mockResolvedValue(undefined),
|
||||
runSetupAction: vi.fn().mockResolvedValue(undefined),
|
||||
runSetupSparkAction: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const rootDir = process.cwd();
|
||||
|
|
@ -27,50 +25,68 @@ describe("onboard oclif command", () => {
|
|||
expect(runOnboardAction).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("accepts --yes and forwards it to the legacy onboard action", async () => {
|
||||
it("accepts --yes and forwards typed flags to the onboard action", async () => {
|
||||
await OnboardCliCommand.run(
|
||||
["--non-interactive", "--yes", "--yes-i-accept-third-party-software"],
|
||||
rootDir,
|
||||
);
|
||||
|
||||
expect(runOnboardAction).toHaveBeenCalledWith([
|
||||
"--non-interactive",
|
||||
"--yes",
|
||||
"--yes-i-accept-third-party-software",
|
||||
]);
|
||||
expect(runOnboardAction).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
"non-interactive": true,
|
||||
yes: true,
|
||||
"yes-i-accept-third-party-software": true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts -y as the short form for --yes", async () => {
|
||||
await OnboardCliCommand.run(["--non-interactive", "-y"], rootDir);
|
||||
|
||||
expect(runOnboardAction).toHaveBeenCalledWith(["--non-interactive", "--yes"]);
|
||||
expect(runOnboardAction).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ "non-interactive": true, yes: true }),
|
||||
);
|
||||
});
|
||||
|
||||
it("forwards sandbox GPU flags to legacy onboard parsing", async () => {
|
||||
it("forwards typed sandbox GPU flags", async () => {
|
||||
await OnboardCliCommand.run(
|
||||
["--non-interactive", "--yes", "--sandbox-gpu", "--sandbox-gpu-device", "nvidia.com/gpu=0"],
|
||||
rootDir,
|
||||
);
|
||||
|
||||
expect(runOnboardAction).toHaveBeenCalledWith([
|
||||
"--non-interactive",
|
||||
"--sandbox-gpu",
|
||||
"--sandbox-gpu-device",
|
||||
"nvidia.com/gpu=0",
|
||||
"--yes",
|
||||
]);
|
||||
expect(runOnboardAction).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
"non-interactive": true,
|
||||
"sandbox-gpu": true,
|
||||
"sandbox-gpu-device": "nvidia.com/gpu=0",
|
||||
yes: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("forwards --no-gpu to the legacy onboard action", async () => {
|
||||
it("forwards --no-gpu to the onboard action", async () => {
|
||||
await OnboardCliCommand.run(["--non-interactive", "--no-gpu"], rootDir);
|
||||
|
||||
expect(runOnboardAction).toHaveBeenCalledWith(["--non-interactive", "--no-gpu"]);
|
||||
expect(runOnboardAction).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ "non-interactive": true, "no-gpu": true }),
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects mutually exclusive gpu and no-gpu flags before dispatch", async () => {
|
||||
await expect(OnboardCliCommand.run(["--gpu", "--no-gpu"], rootDir)).rejects.toThrow(
|
||||
/gpu|no-gpu/,
|
||||
);
|
||||
it.each([
|
||||
["--gpu", "--no-gpu"],
|
||||
["--sandbox-gpu", "--no-sandbox-gpu"],
|
||||
["--gpu", "--no-sandbox-gpu"],
|
||||
["--no-gpu", "--sandbox-gpu"],
|
||||
])("rejects incompatible GPU flags %s and %s before dispatch", async (left, right) => {
|
||||
await expect(OnboardCliCommand.run([left, right], rootDir)).rejects.toThrow(/gpu/i);
|
||||
|
||||
expect(runOnboardAction).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects a sandbox GPU device without explicit sandbox GPU mode", async () => {
|
||||
await expect(
|
||||
OnboardCliCommand.run(["--sandbox-gpu-device", "nvidia.com/gpu=0"], rootDir),
|
||||
).rejects.toThrow(/sandbox-gpu/);
|
||||
|
||||
expect(runOnboardAction).not.toHaveBeenCalled();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import {
|
|||
type OnboardFlags,
|
||||
onboardExamples,
|
||||
onboardUsage,
|
||||
toLegacyOnboardArgs,
|
||||
} from "../lib/onboard/command-support";
|
||||
|
||||
export default class OnboardCliCommand extends NemoClawCommand {
|
||||
|
|
@ -22,6 +21,6 @@ export default class OnboardCliCommand extends NemoClawCommand {
|
|||
|
||||
public async run(): Promise<void> {
|
||||
const { flags } = await this.parse(OnboardCliCommand);
|
||||
await runOnboardAction(toLegacyOnboardArgs(flags as OnboardFlags));
|
||||
await runOnboardAction(flags as OnboardFlags);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,26 @@
|
|||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { runOnboardAction } from "../lib/actions/global";
|
||||
import { CLI_NAME } from "../lib/cli/branding";
|
||||
import { NemoClawCommand } from "../lib/cli/nemoclaw-oclif-command";
|
||||
|
||||
import { runSetupSparkAction } from "../lib/actions/global";
|
||||
import {
|
||||
buildOnboardFlags,
|
||||
type OnboardFlags,
|
||||
toLegacyOnboardArgs,
|
||||
} from "../lib/onboard/command-support";
|
||||
import { buildOnboardFlags, type OnboardFlags } from "../lib/onboard/command-support";
|
||||
|
||||
export default class SetupSparkCliCommand extends NemoClawCommand {
|
||||
static id = "setup-spark";
|
||||
static strict = true;
|
||||
static summary = "Deprecated alias for nemoclaw onboard";
|
||||
static summary = "Deprecated alias for onboard";
|
||||
static description = "Deprecated alias for onboard.";
|
||||
static usage = ["setup-spark [flags]"];
|
||||
static examples = ["<%= config.bin %> setup-spark --name alpha"];
|
||||
static state = "deprecated" as const;
|
||||
static deprecationOptions = {
|
||||
message: `Deprecated: '${CLI_NAME} setup-spark' is now '${CLI_NAME} onboard'; current OpenShell releases handle the old DGX Spark cgroup issue. See '${CLI_NAME} help'.`,
|
||||
};
|
||||
static flags = buildOnboardFlags();
|
||||
|
||||
public async run(): Promise<void> {
|
||||
if (this.argv.includes("--help") || this.argv.includes("-h")) {
|
||||
await runSetupSparkAction(["--help"]);
|
||||
return;
|
||||
}
|
||||
const { flags } = await this.parse(SetupSparkCliCommand);
|
||||
await runSetupSparkAction(toLegacyOnboardArgs(flags as OnboardFlags));
|
||||
await runOnboardAction(flags as OnboardFlags);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,26 @@
|
|||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { runOnboardAction } from "../lib/actions/global";
|
||||
import { CLI_NAME } from "../lib/cli/branding";
|
||||
import { NemoClawCommand } from "../lib/cli/nemoclaw-oclif-command";
|
||||
|
||||
import { runSetupAction } from "../lib/actions/global";
|
||||
import {
|
||||
buildOnboardFlags,
|
||||
type OnboardFlags,
|
||||
toLegacyOnboardArgs,
|
||||
} from "../lib/onboard/command-support";
|
||||
import { buildOnboardFlags, type OnboardFlags } from "../lib/onboard/command-support";
|
||||
|
||||
export default class SetupCliCommand extends NemoClawCommand {
|
||||
static id = "setup";
|
||||
static strict = true;
|
||||
static summary = "Deprecated alias for nemoclaw onboard";
|
||||
static summary = "Deprecated alias for onboard";
|
||||
static description = "Deprecated alias for onboard.";
|
||||
static usage = ["setup [flags]"];
|
||||
static examples = ["<%= config.bin %> setup --name alpha"];
|
||||
static state = "deprecated" as const;
|
||||
static deprecationOptions = {
|
||||
message: `Deprecated: '${CLI_NAME} setup' is now '${CLI_NAME} onboard'. See '${CLI_NAME} help'.`,
|
||||
};
|
||||
static flags = buildOnboardFlags();
|
||||
|
||||
public async run(): Promise<void> {
|
||||
if (this.argv.includes("--help") || this.argv.includes("-h")) {
|
||||
await runSetupAction(["--help"]);
|
||||
return;
|
||||
}
|
||||
const { flags } = await this.parse(SetupCliCommand);
|
||||
await runSetupAction(toLegacyOnboardArgs(flags as OnboardFlags));
|
||||
await runOnboardAction(flags as OnboardFlags);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@ const mocks = vi.hoisted(() => ({
|
|||
runDeployAction: vi.fn().mockResolvedValue(undefined),
|
||||
runOnboardAction: vi.fn().mockResolvedValue(undefined),
|
||||
runOpenshell: vi.fn(() => ({ status: 0 })),
|
||||
runSetupAction: vi.fn().mockResolvedValue(undefined),
|
||||
runSetupSparkAction: vi.fn().mockResolvedValue(undefined),
|
||||
version: vi.fn(),
|
||||
}));
|
||||
|
||||
|
|
@ -26,8 +24,6 @@ vi.mock("./maintenance", () => ({
|
|||
}));
|
||||
vi.mock("./onboard", () => ({
|
||||
runOnboardAction: mocks.runOnboardAction,
|
||||
runSetupAction: mocks.runSetupAction,
|
||||
runSetupSparkAction: mocks.runSetupSparkAction,
|
||||
}));
|
||||
vi.mock("../adapters/openshell/runtime", () => ({ runOpenshell: mocks.runOpenshell }));
|
||||
vi.mock("./root-help", () => ({ help: mocks.help, version: mocks.version }));
|
||||
|
|
@ -39,8 +35,6 @@ import {
|
|||
runGarbageCollectImagesAction,
|
||||
runOnboardAction,
|
||||
runOpenshellProviderCommand,
|
||||
runSetupAction,
|
||||
runSetupSparkAction,
|
||||
runUpgradeSandboxesAction,
|
||||
setGlobalCliActionRuntimeHooksForTest,
|
||||
showRootHelp,
|
||||
|
|
@ -54,18 +48,14 @@ describe("global cli action facade", () => {
|
|||
});
|
||||
|
||||
it("forwards onboarding, deploy, maintenance, and help actions", async () => {
|
||||
await runOnboardAction(["--resume"]);
|
||||
await runSetupAction(["--fresh"]);
|
||||
await runSetupSparkAction(["--name", "alpha"]);
|
||||
await runOnboardAction({ resume: true });
|
||||
await runDeployAction("gpu-alpha");
|
||||
await runBackupAllAction();
|
||||
await runGarbageCollectImagesAction({ dryRun: true });
|
||||
showRootHelp();
|
||||
showVersion();
|
||||
|
||||
expect(mocks.runOnboardAction).toHaveBeenCalledWith(["--resume"]);
|
||||
expect(mocks.runSetupAction).toHaveBeenCalledWith(["--fresh"]);
|
||||
expect(mocks.runSetupSparkAction).toHaveBeenCalledWith(["--name", "alpha"]);
|
||||
expect(mocks.runOnboardAction).toHaveBeenCalledWith({ resume: true });
|
||||
expect(mocks.runDeployAction).toHaveBeenCalledWith("gpu-alpha");
|
||||
expect(mocks.backupAll).toHaveBeenCalledWith();
|
||||
expect(mocks.garbageCollectImages).toHaveBeenCalledWith({ dryRun: true });
|
||||
|
|
|
|||
|
|
@ -7,16 +7,13 @@ import {
|
|||
type UpgradeSandboxesOptions,
|
||||
} from "../domain/lifecycle/options";
|
||||
import { recoverNamedGatewayRuntime as recoverNamedGatewayRuntimeAction } from "../gateway-runtime-action";
|
||||
import type { OnboardFlags } from "../onboard/command-support";
|
||||
import { runDeployAction as executeDeployAction } from "./deploy";
|
||||
import {
|
||||
backupAll as executeBackupAllAction,
|
||||
garbageCollectImages as executeGarbageCollectImagesAction,
|
||||
} from "./maintenance";
|
||||
import {
|
||||
runOnboardAction as executeOnboardAction,
|
||||
runSetupAction as executeSetupAction,
|
||||
runSetupSparkAction as executeSetupSparkAction,
|
||||
} from "./onboard";
|
||||
import { runOnboardAction as executeOnboardAction } from "./onboard";
|
||||
import { help, version } from "./root-help";
|
||||
|
||||
type GatewayRecovery = { recovered: boolean };
|
||||
|
|
@ -33,16 +30,8 @@ export function setGlobalCliActionRuntimeHooksForTest(hooks: GlobalCliActionRunt
|
|||
runtimeHooks = hooks;
|
||||
}
|
||||
|
||||
export async function runOnboardAction(args: string[] = []): Promise<void> {
|
||||
await executeOnboardAction(args);
|
||||
}
|
||||
|
||||
export async function runSetupAction(args: string[] = []): Promise<void> {
|
||||
await executeSetupAction(args);
|
||||
}
|
||||
|
||||
export async function runSetupSparkAction(args: string[] = []): Promise<void> {
|
||||
await executeSetupSparkAction(args);
|
||||
export async function runOnboardAction(flags: OnboardFlags): Promise<void> {
|
||||
await executeOnboardAction(flags);
|
||||
}
|
||||
|
||||
export async function runDeployAction(instanceName?: string): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -2,18 +2,16 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { listAgents } from "../agent/defs";
|
||||
import { runDeprecatedOnboardAliasCommand, runOnboardCommand } from "../onboard/legacy-command";
|
||||
import { NOTICE_ACCEPT_ENV, NOTICE_ACCEPT_FLAG } from "../onboard/usage-notice";
|
||||
import { runOnboardCommand } from "../onboard/command";
|
||||
import type { OnboardFlags } from "../onboard/command-support";
|
||||
|
||||
const { onboard: runOnboard } = require("../onboard") as {
|
||||
onboard: (options?: unknown) => Promise<void>;
|
||||
};
|
||||
|
||||
function buildOnboardCommandDeps(args: string[]) {
|
||||
function buildOnboardCommandDeps(flags: OnboardFlags) {
|
||||
return {
|
||||
args,
|
||||
noticeAcceptFlag: NOTICE_ACCEPT_FLAG,
|
||||
noticeAcceptEnv: NOTICE_ACCEPT_ENV,
|
||||
flags,
|
||||
env: process.env,
|
||||
runOnboard,
|
||||
listAgents,
|
||||
|
|
@ -23,20 +21,6 @@ function buildOnboardCommandDeps(args: string[]) {
|
|||
};
|
||||
}
|
||||
|
||||
export async function runOnboardAction(args: string[]): Promise<void> {
|
||||
await runOnboardCommand(buildOnboardCommandDeps(args));
|
||||
}
|
||||
|
||||
export async function runSetupAction(args: string[] = []): Promise<void> {
|
||||
await runDeprecatedOnboardAliasCommand({
|
||||
...buildOnboardCommandDeps(args),
|
||||
kind: "setup",
|
||||
});
|
||||
}
|
||||
|
||||
export async function runSetupSparkAction(args: string[] = []): Promise<void> {
|
||||
await runDeprecatedOnboardAliasCommand({
|
||||
...buildOnboardCommandDeps(args),
|
||||
kind: "setup-spark",
|
||||
});
|
||||
export async function runOnboardAction(flags: OnboardFlags): Promise<void> {
|
||||
await runOnboardCommand(buildOnboardCommandDeps(flags));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ types.ts
|
|||
providers.ts
|
||||
preflight.ts
|
||||
usage-notice.ts
|
||||
legacy-command.ts
|
||||
command.ts
|
||||
```
|
||||
|
||||
Related modules may live outside this folder when their ownership is clearer:
|
||||
|
|
|
|||
108
src/lib/onboard/command-agents.test.ts
Normal file
108
src/lib/onboard/command-agents.test.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
// 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, vi } from "vitest";
|
||||
|
||||
import { resolveOnboardOptions, runOnboardCommand } from "./command";
|
||||
|
||||
function exitWithCode(code: number): never {
|
||||
throw new Error(`exit:${code}`);
|
||||
}
|
||||
|
||||
describe("onboard --agents", () => {
|
||||
it("resolves an existing manifest to an absolute path", () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-onboard-agents-"));
|
||||
const manifestPath = path.join(tmpDir, "agents.yaml");
|
||||
fs.writeFileSync(manifestPath, "agents: []\n");
|
||||
const relativeManifestPath = path.relative(process.cwd(), manifestPath);
|
||||
|
||||
const result = resolveOnboardOptions(
|
||||
{ agents: relativeManifestPath },
|
||||
{ env: {}, exit: exitWithCode },
|
||||
);
|
||||
expect(result.agentsManifest).toBe(path.resolve(relativeManifestPath));
|
||||
});
|
||||
|
||||
it("rejects a missing manifest", () => {
|
||||
const errors: string[] = [];
|
||||
expect(() =>
|
||||
resolveOnboardOptions(
|
||||
{ agents: "/nonexistent/agents.yaml" },
|
||||
{
|
||||
env: {},
|
||||
error: (message = "") => errors.push(message),
|
||||
exit: exitWithCode,
|
||||
},
|
||||
),
|
||||
).toThrow("exit:1");
|
||||
expect(errors.join("\n")).toContain("--agents path not found");
|
||||
});
|
||||
|
||||
it("rejects manifests for non-OpenClaw runtimes before mutating the environment", async () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-onboard-agents-hermes-"));
|
||||
const manifestPath = path.join(tmpDir, "agents.yaml");
|
||||
fs.writeFileSync(manifestPath, "agents: []\n");
|
||||
const errors: string[] = [];
|
||||
vi.stubEnv("NEMOCLAW_EXTRA_AGENTS_JSON", "unchanged");
|
||||
|
||||
try {
|
||||
await expect(
|
||||
runOnboardCommand({
|
||||
flags: { agent: "hermes", agents: manifestPath },
|
||||
env: {},
|
||||
listAgents: () => ["openclaw", "hermes"],
|
||||
error: (message = "") => errors.push(message),
|
||||
exit: exitWithCode,
|
||||
runOnboard: vi.fn(),
|
||||
}),
|
||||
).rejects.toThrow("exit:1");
|
||||
expect(errors.join("\n")).toContain("--agents is OpenClaw-specific");
|
||||
expect(process.env.NEMOCLAW_EXTRA_AGENTS_JSON).toBe("unchanged");
|
||||
} finally {
|
||||
vi.unstubAllEnvs();
|
||||
}
|
||||
});
|
||||
|
||||
it("applies the manifest environment before invoking onboard", async () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-onboard-agents-env-"));
|
||||
const manifestPath = path.join(tmpDir, "agents.yaml");
|
||||
fs.writeFileSync(
|
||||
manifestPath,
|
||||
["agents:", " - id: alpha", " tools:", " allow: [read]", ""].join("\n"),
|
||||
);
|
||||
const previous = process.env.NEMOCLAW_EXTRA_AGENTS_JSON;
|
||||
delete process.env.NEMOCLAW_EXTRA_AGENTS_JSON;
|
||||
const restoreEnvironment =
|
||||
previous === undefined
|
||||
? () => delete process.env.NEMOCLAW_EXTRA_AGENTS_JSON
|
||||
: () => {
|
||||
process.env.NEMOCLAW_EXTRA_AGENTS_JSON = previous;
|
||||
};
|
||||
let observedRaw: string | undefined;
|
||||
const runOnboard = vi.fn(async () => {
|
||||
observedRaw = process.env.NEMOCLAW_EXTRA_AGENTS_JSON;
|
||||
});
|
||||
try {
|
||||
await runOnboardCommand({
|
||||
flags: { agents: manifestPath },
|
||||
env: {},
|
||||
runOnboard,
|
||||
exit: exitWithCode,
|
||||
});
|
||||
expect(observedRaw).toBeDefined();
|
||||
const payload = JSON.parse(observedRaw as string);
|
||||
expect(payload.agents).toHaveLength(1);
|
||||
expect(payload.agents[0]).toMatchObject({
|
||||
id: "alpha",
|
||||
workspace: "/sandbox/.openclaw/workspace-alpha",
|
||||
agentDir: "/sandbox/.openclaw/agents/alpha",
|
||||
});
|
||||
} finally {
|
||||
restoreEnvironment();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -4,9 +4,7 @@
|
|||
import { Flags } from "@oclif/core";
|
||||
|
||||
import { describeAgentFlag } from "./agent-flag-help";
|
||||
import { NOTICE_ACCEPT_FLAG } from "./usage-notice";
|
||||
|
||||
const acceptFlagName = NOTICE_ACCEPT_FLAG.replace(/^--/, "");
|
||||
import { NOTICE_ACCEPT_FLAG, NOTICE_ACCEPT_FLAG_NAME } from "./usage-notice";
|
||||
|
||||
type AgentRegistryReader = () => readonly string[];
|
||||
|
||||
|
|
@ -79,7 +77,7 @@ export type OnboardFlags = {
|
|||
"control-ui-port"?: number;
|
||||
yes?: boolean;
|
||||
"no-ollama-autostart"?: boolean;
|
||||
[acceptFlagName]?: boolean;
|
||||
[NOTICE_ACCEPT_FLAG_NAME]?: boolean;
|
||||
};
|
||||
|
||||
export function buildOnboardFlags(): Record<string, any> {
|
||||
|
|
@ -96,24 +94,27 @@ export function buildOnboardFlags(): Record<string, any> {
|
|||
"recreate-sandbox": Flags.boolean({ description: "Delete and recreate an existing sandbox" }),
|
||||
gpu: Flags.boolean({
|
||||
description: "Require OpenShell GPU passthrough for the gateway and sandbox",
|
||||
exclusive: ["no-gpu"],
|
||||
exclusive: ["no-gpu", "no-sandbox-gpu"],
|
||||
}),
|
||||
"no-gpu": Flags.boolean({
|
||||
description: "Disable GPU passthrough even when an NVIDIA GPU is detected",
|
||||
exclusive: ["gpu"],
|
||||
exclusive: ["gpu", "sandbox-gpu"],
|
||||
}),
|
||||
from: Flags.string({ description: "Path to a Dockerfile to use as the sandbox image source" }),
|
||||
name: Flags.string({ description: "Sandbox name" }),
|
||||
"sandbox-gpu": Flags.boolean({
|
||||
description: "Enable direct NVIDIA GPU access inside the sandbox",
|
||||
exclusive: ["no-gpu", "no-sandbox-gpu"],
|
||||
}),
|
||||
"no-sandbox-gpu": Flags.boolean({
|
||||
description:
|
||||
"Force CPU sandbox behavior (equivalent to NEMOCLAW_SANDBOX_GPU=0; alternative to --no-gpu when Docker Desktop WSL CDI injection fails)",
|
||||
exclusive: ["gpu", "sandbox-gpu"],
|
||||
}),
|
||||
"sandbox-gpu-device": Flags.string({
|
||||
description:
|
||||
"OpenShell GPU device selector to pass to sandbox create; requires --sandbox-gpu",
|
||||
dependsOn: ["sandbox-gpu"],
|
||||
}),
|
||||
agent: Flags.string({ description: agentFlagDescription() }),
|
||||
agents: Flags.string({
|
||||
|
|
@ -133,32 +134,8 @@ export function buildOnboardFlags(): Record<string, any> {
|
|||
description:
|
||||
"Skip the wizard's eager Ollama auto-start during inference-provider selection so onboard surfaces the unreachable-Ollama warning and the default fallback model; later setup steps still expect a reachable Ollama, and on Linux/systemd hosts the loopback-override path may still restart the daemon",
|
||||
}),
|
||||
[acceptFlagName]: Flags.boolean({ description: "Accept the third-party software notice" }),
|
||||
[NOTICE_ACCEPT_FLAG_NAME]: Flags.boolean({
|
||||
description: "Accept the third-party software notice",
|
||||
}),
|
||||
} as Record<string, any>;
|
||||
}
|
||||
|
||||
export function toLegacyOnboardArgs(flags: OnboardFlags): string[] {
|
||||
const args: string[] = [];
|
||||
if (flags["non-interactive"]) args.push("--non-interactive");
|
||||
if (flags.resume) args.push("--resume");
|
||||
if (flags.fresh) args.push("--fresh");
|
||||
if (flags["recreate-sandbox"]) args.push("--recreate-sandbox");
|
||||
if (flags.gpu) args.push("--gpu");
|
||||
if (flags["no-gpu"]) args.push("--no-gpu");
|
||||
if (flags.from !== undefined) args.push("--from", flags.from);
|
||||
if (flags.name !== undefined) args.push("--name", flags.name);
|
||||
if (flags["sandbox-gpu"]) args.push("--sandbox-gpu");
|
||||
if (flags["no-sandbox-gpu"]) args.push("--no-sandbox-gpu");
|
||||
if (flags["sandbox-gpu-device"] !== undefined) {
|
||||
args.push("--sandbox-gpu-device", flags["sandbox-gpu-device"]);
|
||||
}
|
||||
if (flags.agent !== undefined) args.push("--agent", flags.agent);
|
||||
if (flags.agents !== undefined) args.push("--agents", flags.agents);
|
||||
if (flags["control-ui-port"] !== undefined) {
|
||||
args.push("--control-ui-port", String(flags["control-ui-port"]));
|
||||
}
|
||||
if (flags.yes) args.push("--yes");
|
||||
if (flags["no-ollama-autostart"]) args.push("--no-ollama-autostart");
|
||||
if (flags[acceptFlagName]) args.push(NOTICE_ACCEPT_FLAG);
|
||||
return args;
|
||||
}
|
||||
|
|
|
|||
180
src/lib/onboard/command.test.ts
Normal file
180
src/lib/onboard/command.test.ts
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
// 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, vi } from "vitest";
|
||||
|
||||
import { resolveOnboardOptions, runOnboardCommand } from "./command";
|
||||
import type { OnboardFlags } from "./command-support";
|
||||
|
||||
function exitWithCode(code: number): never {
|
||||
throw new Error(`exit:${code}`);
|
||||
}
|
||||
|
||||
function resolve(
|
||||
flags: OnboardFlags,
|
||||
overrides: Partial<Parameters<typeof resolveOnboardOptions>[1]> = {},
|
||||
) {
|
||||
return resolveOnboardOptions(flags, {
|
||||
env: {},
|
||||
error: () => {},
|
||||
exit: exitWithCode,
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
describe("onboard command options", () => {
|
||||
it("maps typed oclif flags to onboarding options", () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-onboard-options-"));
|
||||
const dockerfilePath = path.join(tmpDir, "Custom.Dockerfile");
|
||||
fs.writeFileSync(dockerfilePath, "FROM scratch\n");
|
||||
|
||||
expect(
|
||||
resolve(
|
||||
{
|
||||
"non-interactive": true,
|
||||
resume: true,
|
||||
"recreate-sandbox": true,
|
||||
from: dockerfilePath,
|
||||
name: "second-assistant",
|
||||
"sandbox-gpu": true,
|
||||
"sandbox-gpu-device": "nvidia.com/gpu=0",
|
||||
agent: "dcode",
|
||||
"control-ui-port": 18790,
|
||||
gpu: true,
|
||||
yes: true,
|
||||
"no-ollama-autostart": true,
|
||||
"yes-i-accept-third-party-software": true,
|
||||
},
|
||||
{ listAgents: () => ["openclaw", "hermes", "langchain-deepagents-code"] },
|
||||
),
|
||||
).toEqual({
|
||||
nonInteractive: true,
|
||||
resume: true,
|
||||
fresh: false,
|
||||
recreateSandbox: true,
|
||||
fromDockerfile: dockerfilePath,
|
||||
sandboxName: "second-assistant",
|
||||
sandboxGpu: "enable",
|
||||
sandboxGpuDevice: "nvidia.com/gpu=0",
|
||||
acceptThirdPartySoftware: true,
|
||||
agent: "langchain-deepagents-code",
|
||||
agentsManifest: null,
|
||||
controlUiPort: 18790,
|
||||
gpu: true,
|
||||
noGpu: false,
|
||||
autoYes: true,
|
||||
noOllamaAutostart: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("uses explicit false/null defaults when flags are absent", () => {
|
||||
expect(resolve({})).toEqual({
|
||||
nonInteractive: false,
|
||||
resume: false,
|
||||
fresh: false,
|
||||
recreateSandbox: false,
|
||||
fromDockerfile: null,
|
||||
sandboxName: null,
|
||||
sandboxGpu: null,
|
||||
sandboxGpuDevice: null,
|
||||
acceptThirdPartySoftware: false,
|
||||
agent: null,
|
||||
agentsManifest: null,
|
||||
controlUiPort: null,
|
||||
gpu: false,
|
||||
noGpu: false,
|
||||
autoYes: false,
|
||||
noOllamaAutostart: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts the environment-based third-party notice acknowledgement", () => {
|
||||
expect(
|
||||
resolve({}, { env: { NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" } }).acceptThirdPartySoftware,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("preserves the requested Dockerfile path after validating the resolved file", () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-onboard-from-"));
|
||||
const dockerfilePath = path.join(tmpDir, "Custom.Dockerfile");
|
||||
fs.writeFileSync(dockerfilePath, "FROM scratch\n");
|
||||
const relativeDockerfilePath = path.relative(process.cwd(), dockerfilePath);
|
||||
|
||||
expect(resolve({ from: relativeDockerfilePath }).fromDockerfile).toBe(relativeDockerfilePath);
|
||||
});
|
||||
|
||||
it("rejects missing and non-file Dockerfile paths before onboarding", () => {
|
||||
const errors: string[] = [];
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-onboard-from-errors-"));
|
||||
const deps = { error: (message = "") => errors.push(message) };
|
||||
|
||||
expect(() => resolve({ from: path.join(tmpDir, "missing") }, deps)).toThrow("exit:1");
|
||||
expect(errors.join("\n")).toContain("--from path not found:");
|
||||
|
||||
errors.length = 0;
|
||||
expect(() => resolve({ from: tmpDir }, deps)).toThrow("exit:1");
|
||||
expect(errors.join("\n")).toContain("--from must point to a Dockerfile:");
|
||||
});
|
||||
|
||||
it("canonicalizes known agent aliases", () => {
|
||||
const listAgents = () => ["openclaw", "hermes", "langchain-deepagents-code"];
|
||||
expect(resolve({ agent: "dcode" }, { listAgents }).agent).toBe("langchain-deepagents-code");
|
||||
expect(resolve({ agent: "nemohermes" }, { listAgents }).agent).toBe("hermes");
|
||||
});
|
||||
|
||||
it("rejects unknown agents with the available aliases", () => {
|
||||
const errors: string[] = [];
|
||||
expect(() =>
|
||||
resolve(
|
||||
{ agent: "bogus" },
|
||||
{
|
||||
listAgents: () => ["openclaw", "hermes"],
|
||||
error: (message = "") => errors.push(message),
|
||||
},
|
||||
),
|
||||
).toThrow("exit:1");
|
||||
expect(errors.join("\n")).toContain("Unknown agent 'bogus'");
|
||||
expect(errors.join("\n")).toContain("aliases: nemohermes → hermes");
|
||||
});
|
||||
|
||||
it("runs onboard with resolved options", async () => {
|
||||
const runOnboard = vi.fn(async () => {});
|
||||
await runOnboardCommand({
|
||||
flags: { resume: true },
|
||||
env: {},
|
||||
runOnboard,
|
||||
error: () => {},
|
||||
exit: exitWithCode,
|
||||
});
|
||||
|
||||
expect(runOnboard).toHaveBeenCalledWith(expect.objectContaining({ resume: true }));
|
||||
});
|
||||
|
||||
it("sets the Ollama autostart override before onboarding", async () => {
|
||||
const previous = process.env.NEMOCLAW_OLLAMA_NO_AUTOSTART;
|
||||
delete process.env.NEMOCLAW_OLLAMA_NO_AUTOSTART;
|
||||
const restoreEnvironment =
|
||||
previous === undefined
|
||||
? () => delete process.env.NEMOCLAW_OLLAMA_NO_AUTOSTART
|
||||
: () => {
|
||||
process.env.NEMOCLAW_OLLAMA_NO_AUTOSTART = previous;
|
||||
};
|
||||
let observed: string | undefined;
|
||||
try {
|
||||
await runOnboardCommand({
|
||||
flags: { "no-ollama-autostart": true },
|
||||
env: {},
|
||||
runOnboard: async () => {
|
||||
observed = process.env.NEMOCLAW_OLLAMA_NO_AUTOSTART;
|
||||
},
|
||||
});
|
||||
expect(observed).toBe("1");
|
||||
} finally {
|
||||
restoreEnvironment();
|
||||
}
|
||||
});
|
||||
});
|
||||
134
src/lib/onboard/command.ts
Normal file
134
src/lib/onboard/command.ts
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import { formatAgentAliasSuffix, resolveAgentNameAlias } from "../agent/aliases";
|
||||
import { applyAgentsManifestEnv } from "./agents-manifest";
|
||||
import type { OnboardFlags } from "./command-support";
|
||||
import { isOpenclawAgent } from "./openclaw-otel-policy-presets";
|
||||
import { NOTICE_ACCEPT_ENV, NOTICE_ACCEPT_FLAG_NAME } from "./usage-notice";
|
||||
|
||||
export interface OnboardCommandOptions {
|
||||
nonInteractive: boolean;
|
||||
resume: boolean;
|
||||
fresh: boolean;
|
||||
recreateSandbox: boolean;
|
||||
fromDockerfile: string | null;
|
||||
sandboxName: string | null;
|
||||
sandboxGpu: "enable" | "disable" | null;
|
||||
sandboxGpuDevice: string | null;
|
||||
acceptThirdPartySoftware: boolean;
|
||||
agent: string | null;
|
||||
agentsManifest: string | null;
|
||||
controlUiPort: number | null;
|
||||
gpu: boolean;
|
||||
noGpu: boolean;
|
||||
autoYes: boolean;
|
||||
noOllamaAutostart: boolean;
|
||||
}
|
||||
|
||||
export interface ResolveOnboardOptionsDeps {
|
||||
env: NodeJS.ProcessEnv;
|
||||
listAgents?: () => string[];
|
||||
error?: (message?: string) => void;
|
||||
exit?: (code: number) => never;
|
||||
}
|
||||
|
||||
export interface RunOnboardCommandDeps extends ResolveOnboardOptionsDeps {
|
||||
flags: OnboardFlags;
|
||||
runOnboard: (options: OnboardCommandOptions) => Promise<void>;
|
||||
}
|
||||
|
||||
function fail(deps: ResolveOnboardOptionsDeps, message: string): never {
|
||||
const error = deps.error ?? console.error;
|
||||
const exit = deps.exit ?? ((code: number) => process.exit(code));
|
||||
error(message);
|
||||
return exit(1);
|
||||
}
|
||||
|
||||
function resolveFileOption(
|
||||
flag: "--from" | "--agents",
|
||||
value: string | undefined,
|
||||
deps: ResolveOnboardOptionsDeps,
|
||||
preserveInput: boolean,
|
||||
): string | null {
|
||||
if (value === undefined) return null;
|
||||
const resolved = path.resolve(value);
|
||||
if (!fs.existsSync(resolved)) fail(deps, ` ${flag} path not found: ${resolved}`);
|
||||
if (!fs.statSync(resolved).isFile()) {
|
||||
const expected = flag === "--from" ? "a Dockerfile" : "a file";
|
||||
fail(deps, ` ${flag} must point to ${expected}: ${resolved}`);
|
||||
}
|
||||
return preserveInput ? value : resolved;
|
||||
}
|
||||
|
||||
function resolveAgent(
|
||||
requestedAgent: string | undefined,
|
||||
deps: ResolveOnboardOptionsDeps,
|
||||
): string | null {
|
||||
if (requestedAgent === undefined) return null;
|
||||
const knownAgents = deps.listAgents?.() ?? [];
|
||||
if (knownAgents.length === 0) return requestedAgent;
|
||||
const resolvedAgent = resolveAgentNameAlias(requestedAgent, knownAgents);
|
||||
if (resolvedAgent) return resolvedAgent;
|
||||
return fail(
|
||||
deps,
|
||||
` Unknown agent '${requestedAgent}'. Available: ${knownAgents.join(", ")}${formatAgentAliasSuffix(knownAgents)}`,
|
||||
);
|
||||
}
|
||||
|
||||
function resolveAgentsManifest(
|
||||
requestedManifest: string | undefined,
|
||||
agent: string | null,
|
||||
deps: ResolveOnboardOptionsDeps,
|
||||
): string | null {
|
||||
if (requestedManifest === undefined) return null;
|
||||
if (!isOpenclawAgent(agent)) {
|
||||
fail(
|
||||
deps,
|
||||
` --agents is OpenClaw-specific and cannot be used with --agent ${agent}; the declarative manifest only drives OpenClaw secondary agents.`,
|
||||
);
|
||||
}
|
||||
return resolveFileOption("--agents", requestedManifest, deps, false);
|
||||
}
|
||||
|
||||
function resolveSandboxGpu(flags: OnboardFlags): "enable" | "disable" | null {
|
||||
if (flags["sandbox-gpu"]) return "enable";
|
||||
if (flags["no-sandbox-gpu"]) return "disable";
|
||||
return null;
|
||||
}
|
||||
|
||||
export function resolveOnboardOptions(
|
||||
flags: OnboardFlags,
|
||||
deps: ResolveOnboardOptionsDeps,
|
||||
): OnboardCommandOptions {
|
||||
const agent = resolveAgent(flags.agent, deps);
|
||||
return {
|
||||
nonInteractive: flags["non-interactive"] === true,
|
||||
resume: flags.resume === true,
|
||||
fresh: flags.fresh === true,
|
||||
recreateSandbox: flags["recreate-sandbox"] === true,
|
||||
fromDockerfile: resolveFileOption("--from", flags.from, deps, true),
|
||||
sandboxName: flags.name ?? null,
|
||||
sandboxGpu: resolveSandboxGpu(flags),
|
||||
sandboxGpuDevice: flags["sandbox-gpu-device"] ?? null,
|
||||
acceptThirdPartySoftware:
|
||||
flags[NOTICE_ACCEPT_FLAG_NAME] === true || String(deps.env[NOTICE_ACCEPT_ENV] || "") === "1",
|
||||
agent,
|
||||
agentsManifest: resolveAgentsManifest(flags.agents, agent, deps),
|
||||
controlUiPort: flags["control-ui-port"] ?? null,
|
||||
gpu: flags.gpu === true,
|
||||
noGpu: flags["no-gpu"] === true,
|
||||
autoYes: flags.yes === true,
|
||||
noOllamaAutostart: flags["no-ollama-autostart"] === true,
|
||||
};
|
||||
}
|
||||
|
||||
export async function runOnboardCommand(deps: RunOnboardCommandDeps): Promise<void> {
|
||||
const options = resolveOnboardOptions(deps.flags, deps);
|
||||
if (options.noOllamaAutostart) process.env.NEMOCLAW_OLLAMA_NO_AUTOSTART = "1";
|
||||
if (options.agentsManifest) applyAgentsManifestEnv(options.agentsManifest);
|
||||
await deps.runOnboard(options);
|
||||
}
|
||||
|
|
@ -1,137 +0,0 @@
|
|||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Focused coverage for the `--agents <agents.yaml>` lifecycle on `onboard`:
|
||||
// parse-arg-into-option, missing-path/value rejection, env-var application
|
||||
// before runOnboard is invoked. Split from legacy-command.test.ts so the
|
||||
// hotspot does not grow further.
|
||||
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { parseOnboardArgs, runOnboardCommand } from "./legacy-command";
|
||||
|
||||
function exitWithCode(code: number): never {
|
||||
throw new Error(String(code));
|
||||
}
|
||||
|
||||
function exitWithPrefixedCode(code: number): never {
|
||||
throw new Error(`exit:${code}`);
|
||||
}
|
||||
|
||||
describe("onboard --agents", () => {
|
||||
it("parses --agents <agents.yaml> into agentsManifest", () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-onboard-agents-parse-"));
|
||||
const manifestPath = path.join(tmpDir, "agents.yaml");
|
||||
fs.writeFileSync(manifestPath, "agents: []\n");
|
||||
|
||||
const result = parseOnboardArgs(
|
||||
["--agents", manifestPath],
|
||||
"--yes-i-accept-third-party-software",
|
||||
"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
|
||||
{
|
||||
env: {},
|
||||
error: () => {},
|
||||
exit: exitWithCode,
|
||||
},
|
||||
);
|
||||
expect(result.agentsManifest).toBe(manifestPath);
|
||||
});
|
||||
|
||||
it("rejects --agents when the file is missing", () => {
|
||||
const errors: string[] = [];
|
||||
expect(() =>
|
||||
parseOnboardArgs(
|
||||
["--agents", "/nonexistent/agents.yaml"],
|
||||
"--yes-i-accept-third-party-software",
|
||||
"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
|
||||
{
|
||||
env: {},
|
||||
error: (message = "") => errors.push(message),
|
||||
exit: exitWithPrefixedCode,
|
||||
},
|
||||
),
|
||||
).toThrow("exit:1");
|
||||
expect(errors.join("\n")).toContain("--agents path not found");
|
||||
});
|
||||
|
||||
it("rejects --agents when the value is missing", () => {
|
||||
const errors: string[] = [];
|
||||
expect(() =>
|
||||
parseOnboardArgs(
|
||||
["--agents"],
|
||||
"--yes-i-accept-third-party-software",
|
||||
"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
|
||||
{
|
||||
env: {},
|
||||
error: (message = "") => errors.push(message),
|
||||
exit: exitWithPrefixedCode,
|
||||
},
|
||||
),
|
||||
).toThrow("exit:1");
|
||||
expect(errors.join("\n")).toContain("--agents requires a path to a YAML manifest");
|
||||
});
|
||||
|
||||
it("rejects --agents when --agent is set to a non-OpenClaw runtime", () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-onboard-agents-hermes-"));
|
||||
const manifestPath = path.join(tmpDir, "agents.yaml");
|
||||
fs.writeFileSync(manifestPath, "agents: []\n");
|
||||
const errors: string[] = [];
|
||||
expect(() =>
|
||||
parseOnboardArgs(
|
||||
["--agent", "hermes", "--agents", manifestPath],
|
||||
"--yes-i-accept-third-party-software",
|
||||
"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
|
||||
{
|
||||
env: {},
|
||||
error: (message = "") => errors.push(message),
|
||||
exit: exitWithPrefixedCode,
|
||||
},
|
||||
),
|
||||
).toThrow("exit:1");
|
||||
expect(errors.join("\n")).toContain("--agents is OpenClaw-specific");
|
||||
});
|
||||
|
||||
it("sets NEMOCLAW_EXTRA_AGENTS_JSON before invoking runOnboard when --agents is supplied", async () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-onboard-agents-env-"));
|
||||
const manifestPath = path.join(tmpDir, "agents.yaml");
|
||||
fs.writeFileSync(
|
||||
manifestPath,
|
||||
["agents:", " - id: alpha", " tools:", " allow: [read]", ""].join("\n"),
|
||||
);
|
||||
const previous = process.env.NEMOCLAW_EXTRA_AGENTS_JSON;
|
||||
delete process.env.NEMOCLAW_EXTRA_AGENTS_JSON;
|
||||
let observedRaw: string | undefined;
|
||||
const runOnboard = vi.fn(async () => {
|
||||
observedRaw = process.env.NEMOCLAW_EXTRA_AGENTS_JSON;
|
||||
});
|
||||
try {
|
||||
await runOnboardCommand({
|
||||
args: ["--agents", manifestPath],
|
||||
noticeAcceptFlag: "--yes-i-accept-third-party-software",
|
||||
noticeAcceptEnv: "NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
|
||||
env: {},
|
||||
runOnboard,
|
||||
error: () => {},
|
||||
exit: exitWithCode,
|
||||
});
|
||||
expect(observedRaw).toBeDefined();
|
||||
const payload = JSON.parse(observedRaw as string);
|
||||
expect(payload.agents).toHaveLength(1);
|
||||
expect(payload.agents[0]).toMatchObject({
|
||||
id: "alpha",
|
||||
workspace: "/sandbox/.openclaw/workspace-alpha",
|
||||
agentDir: "/sandbox/.openclaw/agents/alpha",
|
||||
});
|
||||
} finally {
|
||||
if (previous === undefined) {
|
||||
delete process.env.NEMOCLAW_EXTRA_AGENTS_JSON;
|
||||
} else {
|
||||
process.env.NEMOCLAW_EXTRA_AGENTS_JSON = previous;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -1,749 +0,0 @@
|
|||
// 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, vi } from "vitest";
|
||||
|
||||
import {
|
||||
parseOnboardArgs,
|
||||
runDeprecatedOnboardAliasCommand,
|
||||
runOnboardCommand,
|
||||
} from "./legacy-command";
|
||||
|
||||
function exitWithCode(code: number): never {
|
||||
throw new Error(String(code));
|
||||
}
|
||||
|
||||
function exitWithPrefixedCode(code: number): never {
|
||||
throw new Error(`exit:${code}`);
|
||||
}
|
||||
|
||||
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: exitWithCode,
|
||||
},
|
||||
),
|
||||
).toEqual({
|
||||
nonInteractive: true,
|
||||
resume: true,
|
||||
fresh: false,
|
||||
recreateSandbox: false,
|
||||
fromDockerfile: null,
|
||||
sandboxName: null,
|
||||
sandboxGpu: null,
|
||||
sandboxGpuDevice: null,
|
||||
acceptThirdPartySoftware: true,
|
||||
agent: null,
|
||||
agentsManifest: null,
|
||||
controlUiPort: null,
|
||||
gpu: false,
|
||||
noGpu: false,
|
||||
autoYes: false,
|
||||
noOllamaAutostart: false,
|
||||
});
|
||||
});
|
||||
|
||||
it.each<{ flags: string[] }>([
|
||||
{ flags: ["--yes"] },
|
||||
{ flags: ["-y"] },
|
||||
{ flags: ["--yes", "-y"] },
|
||||
])("sets autoYes when invoked with $flags", ({ flags }) => {
|
||||
const result = parseOnboardArgs(
|
||||
flags,
|
||||
"--yes-i-accept-third-party-software",
|
||||
"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
|
||||
{
|
||||
env: {},
|
||||
error: () => {},
|
||||
exit: exitWithCode,
|
||||
},
|
||||
);
|
||||
expect(result.autoYes).toBe(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: exitWithCode,
|
||||
},
|
||||
),
|
||||
).toEqual({
|
||||
nonInteractive: false,
|
||||
resume: false,
|
||||
fresh: false,
|
||||
recreateSandbox: false,
|
||||
fromDockerfile: null,
|
||||
sandboxName: null,
|
||||
sandboxGpu: null,
|
||||
sandboxGpuDevice: null,
|
||||
acceptThirdPartySoftware: true,
|
||||
agent: null,
|
||||
agentsManifest: null,
|
||||
controlUiPort: null,
|
||||
gpu: false,
|
||||
noGpu: false,
|
||||
autoYes: false,
|
||||
noOllamaAutostart: false,
|
||||
});
|
||||
});
|
||||
|
||||
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: exitWithCode,
|
||||
});
|
||||
expect(runOnboard).toHaveBeenCalledWith({
|
||||
nonInteractive: false,
|
||||
resume: true,
|
||||
fresh: false,
|
||||
recreateSandbox: false,
|
||||
fromDockerfile: null,
|
||||
sandboxName: null,
|
||||
sandboxGpu: null,
|
||||
sandboxGpuDevice: null,
|
||||
acceptThirdPartySoftware: false,
|
||||
agent: null,
|
||||
agentsManifest: null,
|
||||
controlUiPort: null,
|
||||
gpu: false,
|
||||
noGpu: false,
|
||||
autoYes: false,
|
||||
noOllamaAutostart: 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: exitWithCode,
|
||||
});
|
||||
expect(runOnboard).not.toHaveBeenCalled();
|
||||
expect(lines.join("\n")).toContain("Usage: nemoclaw onboard");
|
||||
expect(lines.join("\n")).toContain("--from <Dockerfile>");
|
||||
expect(lines.join("\n")).toContain("--name <sandbox>");
|
||||
expect(lines.join("\n")).toContain("Dockerfile's parent directory");
|
||||
expect(lines.join("\n")).toContain("node_modules, .git, .venv, __pycache__");
|
||||
expect(lines.join("\n")).toContain(".env*, .ssh, .aws");
|
||||
expect(lines.join("\n")).toContain("--agent <name>");
|
||||
expect(lines.join("\n")).toContain("--agents <agents.yaml>");
|
||||
expect(lines.join("\n")).toContain("--no-gpu");
|
||||
});
|
||||
|
||||
it("parses --from <Dockerfile>", () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-onboard-from-parse-"));
|
||||
const dockerfilePath = path.join(tmpDir, "Custom.Dockerfile");
|
||||
fs.writeFileSync(dockerfilePath, "FROM scratch\n");
|
||||
|
||||
expect(
|
||||
parseOnboardArgs(
|
||||
["--resume", "--from", dockerfilePath],
|
||||
"--yes-i-accept-third-party-software",
|
||||
"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
|
||||
{
|
||||
env: {},
|
||||
error: () => {},
|
||||
exit: exitWithCode,
|
||||
},
|
||||
),
|
||||
).toEqual({
|
||||
nonInteractive: false,
|
||||
resume: true,
|
||||
fresh: false,
|
||||
recreateSandbox: false,
|
||||
fromDockerfile: dockerfilePath,
|
||||
sandboxName: null,
|
||||
sandboxGpu: null,
|
||||
sandboxGpuDevice: null,
|
||||
acceptThirdPartySoftware: false,
|
||||
agent: null,
|
||||
agentsManifest: null,
|
||||
controlUiPort: null,
|
||||
gpu: false,
|
||||
noGpu: false,
|
||||
autoYes: false,
|
||||
noOllamaAutostart: false,
|
||||
});
|
||||
});
|
||||
|
||||
// --agents <agents.yaml> parsing covered by
|
||||
// src/lib/onboard/legacy-command-agents.test.ts to keep this hotspot from
|
||||
// growing further; that file owns the full --agents lifecycle (parse,
|
||||
// missing-path/value rejection, env-var application before runOnboard).
|
||||
|
||||
it("parses --fresh and surfaces it as fresh=true", () => {
|
||||
expect(
|
||||
parseOnboardArgs(
|
||||
["--fresh"],
|
||||
"--yes-i-accept-third-party-software",
|
||||
"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
|
||||
{
|
||||
env: {},
|
||||
error: () => {},
|
||||
exit: exitWithCode,
|
||||
},
|
||||
),
|
||||
).toEqual({
|
||||
nonInteractive: false,
|
||||
resume: false,
|
||||
fresh: true,
|
||||
recreateSandbox: false,
|
||||
fromDockerfile: null,
|
||||
sandboxName: null,
|
||||
sandboxGpu: null,
|
||||
sandboxGpuDevice: null,
|
||||
acceptThirdPartySoftware: false,
|
||||
agent: null,
|
||||
agentsManifest: null,
|
||||
controlUiPort: null,
|
||||
gpu: false,
|
||||
noGpu: false,
|
||||
autoYes: false,
|
||||
noOllamaAutostart: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects --resume and --fresh together", () => {
|
||||
const errors: string[] = [];
|
||||
expect(() =>
|
||||
parseOnboardArgs(
|
||||
["--resume", "--fresh"],
|
||||
"--yes-i-accept-third-party-software",
|
||||
"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
|
||||
{
|
||||
env: {},
|
||||
error: (message = "") => errors.push(message),
|
||||
exit: exitWithPrefixedCode,
|
||||
},
|
||||
),
|
||||
).toThrow("exit:1");
|
||||
expect(errors.join("\n")).toContain("--resume and --fresh are mutually exclusive");
|
||||
});
|
||||
|
||||
it("parses --name <sandbox>", () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-onboard-name-parse-"));
|
||||
const dockerfilePath = path.join(tmpDir, "Custom.Dockerfile");
|
||||
fs.writeFileSync(dockerfilePath, "FROM scratch\n");
|
||||
|
||||
expect(
|
||||
parseOnboardArgs(
|
||||
["--non-interactive", "--from", dockerfilePath, "--name", "second-assistant"],
|
||||
"--yes-i-accept-third-party-software",
|
||||
"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
|
||||
{
|
||||
env: {},
|
||||
error: () => {},
|
||||
exit: exitWithCode,
|
||||
},
|
||||
),
|
||||
).toEqual({
|
||||
nonInteractive: true,
|
||||
resume: false,
|
||||
fresh: false,
|
||||
recreateSandbox: false,
|
||||
fromDockerfile: dockerfilePath,
|
||||
sandboxName: "second-assistant",
|
||||
sandboxGpu: null,
|
||||
sandboxGpuDevice: null,
|
||||
acceptThirdPartySoftware: false,
|
||||
agent: null,
|
||||
agentsManifest: null,
|
||||
controlUiPort: null,
|
||||
gpu: false,
|
||||
noGpu: false,
|
||||
autoYes: false,
|
||||
noOllamaAutostart: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("exits when --name is missing its sandbox value", () => {
|
||||
const errors: string[] = [];
|
||||
expect(() =>
|
||||
parseOnboardArgs(
|
||||
["--name"],
|
||||
"--yes-i-accept-third-party-software",
|
||||
"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
|
||||
{
|
||||
env: {},
|
||||
error: (message = "") => errors.push(message),
|
||||
exit: exitWithPrefixedCode,
|
||||
},
|
||||
),
|
||||
).toThrow("exit:1");
|
||||
expect(errors.join("\n")).toContain("--name requires a sandbox name");
|
||||
});
|
||||
|
||||
it("exits when --name is followed by another flag instead of a value", () => {
|
||||
const errors: string[] = [];
|
||||
expect(() =>
|
||||
parseOnboardArgs(
|
||||
["--name", "--resume"],
|
||||
"--yes-i-accept-third-party-software",
|
||||
"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
|
||||
{
|
||||
env: {},
|
||||
error: (message = "") => errors.push(message),
|
||||
exit: exitWithPrefixedCode,
|
||||
},
|
||||
),
|
||||
).toThrow("exit:1");
|
||||
expect(errors.join("\n")).toContain("--name requires a sandbox name");
|
||||
});
|
||||
|
||||
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: exitWithPrefixedCode,
|
||||
},
|
||||
),
|
||||
).toThrow("exit:1");
|
||||
});
|
||||
|
||||
it("exits before onboarding when --from points to a missing Dockerfile", async () => {
|
||||
const runOnboard = vi.fn(async () => {});
|
||||
const errors: string[] = [];
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-onboard-from-missing-"));
|
||||
|
||||
await expect(
|
||||
runOnboardCommand({
|
||||
args: ["--from", path.join(tmpDir, "no-such-dockerfile-2589")],
|
||||
noticeAcceptFlag: "--yes-i-accept-third-party-software",
|
||||
noticeAcceptEnv: "NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
|
||||
env: {},
|
||||
runOnboard,
|
||||
error: (message = "") => errors.push(message),
|
||||
exit: exitWithPrefixedCode,
|
||||
}),
|
||||
).rejects.toThrow("exit:1");
|
||||
|
||||
expect(runOnboard).not.toHaveBeenCalled();
|
||||
expect(errors.join("\n")).toContain("--from path not found:");
|
||||
});
|
||||
|
||||
it("exits before onboarding when --from points to a directory", async () => {
|
||||
const runOnboard = vi.fn(async () => {});
|
||||
const errors: string[] = [];
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-onboard-from-dir-"));
|
||||
|
||||
await expect(
|
||||
runOnboardCommand({
|
||||
args: ["--from", tmpDir],
|
||||
noticeAcceptFlag: "--yes-i-accept-third-party-software",
|
||||
noticeAcceptEnv: "NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
|
||||
env: {},
|
||||
runOnboard,
|
||||
error: (message = "") => errors.push(message),
|
||||
exit: exitWithPrefixedCode,
|
||||
}),
|
||||
).rejects.toThrow("exit:1");
|
||||
|
||||
expect(runOnboard).not.toHaveBeenCalled();
|
||||
expect(errors.join("\n")).toContain("--from must point to a Dockerfile:");
|
||||
});
|
||||
|
||||
it("exits with usage on unknown args", () => {
|
||||
const errors: string[] = [];
|
||||
expect(() =>
|
||||
parseOnboardArgs(
|
||||
["--bad-flag"],
|
||||
"--yes-i-accept-third-party-software",
|
||||
"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
|
||||
{
|
||||
env: {},
|
||||
error: (message = "") => errors.push(message),
|
||||
exit: exitWithPrefixedCode,
|
||||
},
|
||||
),
|
||||
).toThrow("exit:1");
|
||||
expect(errors.join("\n")).toContain("Unknown onboard option(s): --bad-flag");
|
||||
expect(errors.join("\n")).toContain("Usage: nemoclaw onboard");
|
||||
});
|
||||
|
||||
it("parses --agent", () => {
|
||||
expect(
|
||||
parseOnboardArgs(
|
||||
["--agent", "openclaw"],
|
||||
"--yes-i-accept-third-party-software",
|
||||
"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
|
||||
{
|
||||
env: {},
|
||||
listAgents: () => ["openclaw", "hermes"],
|
||||
error: () => {},
|
||||
exit: exitWithCode,
|
||||
},
|
||||
),
|
||||
).toEqual({
|
||||
nonInteractive: false,
|
||||
resume: false,
|
||||
fresh: false,
|
||||
recreateSandbox: false,
|
||||
fromDockerfile: null,
|
||||
sandboxName: null,
|
||||
sandboxGpu: null,
|
||||
sandboxGpuDevice: null,
|
||||
acceptThirdPartySoftware: false,
|
||||
agent: "openclaw",
|
||||
agentsManifest: null,
|
||||
controlUiPort: null,
|
||||
gpu: false,
|
||||
noGpu: false,
|
||||
autoYes: false,
|
||||
noOllamaAutostart: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("canonicalizes known --agent aliases", () => {
|
||||
expect(
|
||||
parseOnboardArgs(
|
||||
["--agent", "dcode"],
|
||||
"--yes-i-accept-third-party-software",
|
||||
"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
|
||||
{
|
||||
env: {},
|
||||
listAgents: () => ["openclaw", "hermes", "langchain-deepagents-code"],
|
||||
error: () => {},
|
||||
exit: exitWithCode,
|
||||
},
|
||||
).agent,
|
||||
).toBe("langchain-deepagents-code");
|
||||
|
||||
expect(
|
||||
parseOnboardArgs(
|
||||
["--agent", "nemohermes"],
|
||||
"--yes-i-accept-third-party-software",
|
||||
"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
|
||||
{
|
||||
env: {},
|
||||
listAgents: () => ["openclaw", "hermes", "langchain-deepagents-code"],
|
||||
error: () => {},
|
||||
exit: exitWithCode,
|
||||
},
|
||||
).agent,
|
||||
).toBe("hermes");
|
||||
});
|
||||
|
||||
it("rejects unknown --agent values", () => {
|
||||
const errors: string[] = [];
|
||||
expect(() =>
|
||||
parseOnboardArgs(
|
||||
["--agent", "bogus"],
|
||||
"--yes-i-accept-third-party-software",
|
||||
"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
|
||||
{
|
||||
env: {},
|
||||
listAgents: () => ["openclaw", "hermes"],
|
||||
error: (message = "") => errors.push(message),
|
||||
exit: exitWithPrefixedCode,
|
||||
},
|
||||
),
|
||||
).toThrow("exit:1");
|
||||
expect(errors.join("\n")).toContain("Unknown agent 'bogus'");
|
||||
expect(errors.join("\n")).toContain("aliases: nemohermes → hermes");
|
||||
expect(errors.join("\n")).toContain("Usage: nemoclaw onboard");
|
||||
});
|
||||
|
||||
it("parses --control-ui-port with a valid port", () => {
|
||||
const result = parseOnboardArgs(
|
||||
["--control-ui-port", "18790"],
|
||||
"--yes-i-accept-third-party-software",
|
||||
"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
|
||||
{
|
||||
env: {},
|
||||
error: () => {},
|
||||
exit: ((code: number) => {
|
||||
throw new Error(String(code));
|
||||
}) as never,
|
||||
},
|
||||
);
|
||||
expect(result.controlUiPort).toBe(18790);
|
||||
});
|
||||
|
||||
it("exits when --control-ui-port is missing its value", () => {
|
||||
expect(() =>
|
||||
parseOnboardArgs(
|
||||
["--control-ui-port"],
|
||||
"--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("exits when --control-ui-port value is out of range", () => {
|
||||
const errors: string[] = [];
|
||||
expect(() =>
|
||||
parseOnboardArgs(
|
||||
["--control-ui-port", "80"],
|
||||
"--yes-i-accept-third-party-software",
|
||||
"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
|
||||
{
|
||||
env: {},
|
||||
error: (message = "") => errors.push(message),
|
||||
exit: ((code: number) => {
|
||||
throw new Error(`exit:${code}`);
|
||||
}) as never,
|
||||
},
|
||||
),
|
||||
).toThrow("exit:1");
|
||||
expect(errors.join("\n")).toContain("1024-65535");
|
||||
});
|
||||
|
||||
it("--control-ui-port takes precedence over CHAT_UI_URL env", () => {
|
||||
const result = parseOnboardArgs(
|
||||
["--control-ui-port", "19000"],
|
||||
"--yes-i-accept-third-party-software",
|
||||
"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
|
||||
{
|
||||
env: { CHAT_UI_URL: "http://127.0.0.1:18790" },
|
||||
error: () => {},
|
||||
exit: ((code: number) => {
|
||||
throw new Error(String(code));
|
||||
}) as never,
|
||||
},
|
||||
);
|
||||
expect(result.controlUiPort).toBe(19000);
|
||||
});
|
||||
|
||||
it("parses direct sandbox GPU flags", () => {
|
||||
const result = parseOnboardArgs(
|
||||
["--sandbox-gpu", "--sandbox-gpu-device", "0"],
|
||||
"--yes-i-accept-third-party-software",
|
||||
"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
|
||||
{
|
||||
env: {},
|
||||
error: () => {},
|
||||
exit: exitWithCode,
|
||||
},
|
||||
);
|
||||
expect(result.sandboxGpu).toBe("enable");
|
||||
expect(result.sandboxGpuDevice).toBe("0");
|
||||
});
|
||||
|
||||
it("rejects conflicting sandbox GPU flags", () => {
|
||||
const errors: string[] = [];
|
||||
expect(() =>
|
||||
parseOnboardArgs(
|
||||
["--sandbox-gpu", "--no-sandbox-gpu"],
|
||||
"--yes-i-accept-third-party-software",
|
||||
"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
|
||||
{
|
||||
env: {},
|
||||
error: (message = "") => errors.push(message),
|
||||
exit: exitWithPrefixedCode,
|
||||
},
|
||||
),
|
||||
).toThrow("exit:1");
|
||||
expect(errors.join("\n")).toContain("mutually exclusive");
|
||||
});
|
||||
|
||||
it("--help includes --control-ui-port in usage", async () => {
|
||||
const lines: string[] = [];
|
||||
await runOnboardCommand({
|
||||
args: ["--help"],
|
||||
noticeAcceptFlag: "--yes-i-accept-third-party-software",
|
||||
noticeAcceptEnv: "NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
|
||||
env: {},
|
||||
runOnboard: vi.fn(async () => {}),
|
||||
log: (message = "") => lines.push(message),
|
||||
error: () => {},
|
||||
exit: ((code: number) => {
|
||||
throw new Error(String(code));
|
||||
}) as never,
|
||||
});
|
||||
expect(lines.join("\n")).toContain("--control-ui-port");
|
||||
expect(lines.join("\n")).toContain("--sandbox-gpu");
|
||||
});
|
||||
|
||||
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: exitWithCode,
|
||||
});
|
||||
expect(lines.join("\n")).toContain("setup-spark` is deprecated");
|
||||
expect(lines.join("\n")).toContain("Use `nemoclaw onboard` instead");
|
||||
expect(runOnboard).toHaveBeenCalledTimes(1);
|
||||
expect(runOnboard).toHaveBeenCalledWith({
|
||||
nonInteractive: false,
|
||||
resume: false,
|
||||
fresh: false,
|
||||
recreateSandbox: false,
|
||||
fromDockerfile: null,
|
||||
sandboxName: null,
|
||||
sandboxGpu: null,
|
||||
sandboxGpuDevice: null,
|
||||
acceptThirdPartySoftware: false,
|
||||
agent: null,
|
||||
agentsManifest: null,
|
||||
controlUiPort: null,
|
||||
gpu: false,
|
||||
noGpu: false,
|
||||
autoYes: false,
|
||||
noOllamaAutostart: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("prints the setup deprecation text before delegating", async () => {
|
||||
const lines: string[] = [];
|
||||
const runOnboard = vi.fn(async () => {});
|
||||
await runDeprecatedOnboardAliasCommand({
|
||||
kind: "setup",
|
||||
args: [],
|
||||
noticeAcceptFlag: "--yes-i-accept-third-party-software",
|
||||
noticeAcceptEnv: "NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
|
||||
env: {},
|
||||
runOnboard,
|
||||
log: (message = "") => lines.push(message),
|
||||
error: () => {},
|
||||
exit: exitWithCode,
|
||||
});
|
||||
expect(lines.join("\n")).toContain("`nemoclaw setup` is deprecated");
|
||||
expect(lines.join("\n")).toContain("Use `nemoclaw onboard` instead");
|
||||
expect(runOnboard).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("parses --gpu as explicit GPU passthrough intent", () => {
|
||||
const result = parseOnboardArgs(
|
||||
["--gpu", "--non-interactive"],
|
||||
"--yes-i-accept-third-party-software",
|
||||
"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
|
||||
{
|
||||
env: {},
|
||||
error: () => {},
|
||||
exit: exitWithCode,
|
||||
},
|
||||
);
|
||||
expect(result.gpu).toBe(true);
|
||||
expect(result.noGpu).toBe(false);
|
||||
expect(result.nonInteractive).toBe(true);
|
||||
});
|
||||
|
||||
it("parses --no-gpu as explicit GPU passthrough opt-out", () => {
|
||||
const result = parseOnboardArgs(
|
||||
["--no-gpu", "--non-interactive"],
|
||||
"--yes-i-accept-third-party-software",
|
||||
"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
|
||||
{
|
||||
env: {},
|
||||
error: () => {},
|
||||
exit: exitWithCode,
|
||||
},
|
||||
);
|
||||
expect(result.gpu).toBe(false);
|
||||
expect(result.noGpu).toBe(true);
|
||||
expect(result.nonInteractive).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects --gpu and --no-gpu together", () => {
|
||||
const errors: string[] = [];
|
||||
expect(() =>
|
||||
parseOnboardArgs(
|
||||
["--gpu", "--no-gpu"],
|
||||
"--yes-i-accept-third-party-software",
|
||||
"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
|
||||
{
|
||||
env: {},
|
||||
error: (message = "") => errors.push(message),
|
||||
exit: exitWithPrefixedCode,
|
||||
},
|
||||
),
|
||||
).toThrow("exit:1");
|
||||
expect(errors.join("\n")).toContain("--gpu and --no-gpu are mutually exclusive");
|
||||
});
|
||||
|
||||
it("defaults noOllamaAutostart to false when the flag is absent", () => {
|
||||
const result = parseOnboardArgs(
|
||||
[],
|
||||
"--yes-i-accept-third-party-software",
|
||||
"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
|
||||
{
|
||||
env: {},
|
||||
error: () => {},
|
||||
exit: exitWithCode,
|
||||
},
|
||||
);
|
||||
expect(result.noOllamaAutostart).toBe(false);
|
||||
});
|
||||
|
||||
it("parses --no-ollama-autostart as noOllamaAutostart=true without rejecting it as unknown", () => {
|
||||
const errors: string[] = [];
|
||||
const result = parseOnboardArgs(
|
||||
["--no-ollama-autostart"],
|
||||
"--yes-i-accept-third-party-software",
|
||||
"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
|
||||
{
|
||||
env: {},
|
||||
error: (message = "") => errors.push(message),
|
||||
exit: exitWithPrefixedCode,
|
||||
},
|
||||
);
|
||||
expect(result.noOllamaAutostart).toBe(true);
|
||||
expect(errors.join("\n")).not.toContain("Unknown onboard option(s)");
|
||||
});
|
||||
|
||||
it("--help advertises --no-ollama-autostart in the usage output", async () => {
|
||||
const lines: string[] = [];
|
||||
await runOnboardCommand({
|
||||
args: ["--help"],
|
||||
noticeAcceptFlag: "--yes-i-accept-third-party-software",
|
||||
noticeAcceptEnv: "NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
|
||||
env: {},
|
||||
runOnboard: vi.fn(async () => {}),
|
||||
log: (message = "") => lines.push(message),
|
||||
error: () => {},
|
||||
exit: exitWithCode,
|
||||
});
|
||||
expect(lines.join("\n")).toContain("--no-ollama-autostart");
|
||||
expect(lines.join("\n")).toContain("inference-provider selection");
|
||||
});
|
||||
});
|
||||
|
|
@ -1,328 +0,0 @@
|
|||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import { formatAgentAliasSuffix, resolveAgentNameAlias } from "../agent/aliases";
|
||||
import { CLI_NAME } from "../cli/branding";
|
||||
import { applyAgentsManifestEnv } from "./agents-manifest";
|
||||
import { isOpenclawAgent } from "./openclaw-otel-policy-presets";
|
||||
|
||||
export interface OnboardCommandOptions {
|
||||
nonInteractive: boolean;
|
||||
resume: boolean;
|
||||
fresh: boolean;
|
||||
recreateSandbox: boolean;
|
||||
fromDockerfile: string | null;
|
||||
sandboxName: string | null;
|
||||
sandboxGpu: "enable" | "disable" | null;
|
||||
sandboxGpuDevice: string | null;
|
||||
acceptThirdPartySoftware: boolean;
|
||||
agent: string | null;
|
||||
agentsManifest: string | null;
|
||||
controlUiPort: number | null;
|
||||
gpu: boolean;
|
||||
noGpu: boolean;
|
||||
autoYes: boolean;
|
||||
noOllamaAutostart: boolean;
|
||||
}
|
||||
|
||||
export interface RunOnboardCommandDeps {
|
||||
args: string[];
|
||||
noticeAcceptFlag: string;
|
||||
noticeAcceptEnv: string;
|
||||
env: NodeJS.ProcessEnv;
|
||||
runOnboard: (options: OnboardCommandOptions) => Promise<void>;
|
||||
listAgents?: () => string[];
|
||||
log?: (message?: string) => void;
|
||||
error?: (message?: string) => void;
|
||||
exit?: (code: number) => never;
|
||||
}
|
||||
|
||||
export interface RunDeprecatedOnboardAliasCommandDeps extends RunOnboardCommandDeps {
|
||||
kind: "setup" | "setup-spark";
|
||||
}
|
||||
|
||||
const ONBOARD_BASE_ARGS = [
|
||||
"--non-interactive",
|
||||
"--resume",
|
||||
"--fresh",
|
||||
"--recreate-sandbox",
|
||||
"--gpu",
|
||||
"--no-gpu",
|
||||
"--yes",
|
||||
"-y",
|
||||
"--no-ollama-autostart",
|
||||
];
|
||||
|
||||
function onboardUsageLines(noticeAcceptFlag: string): string[] {
|
||||
const name = CLI_NAME;
|
||||
return [
|
||||
` Usage: ${name} onboard [--non-interactive] [--resume | --fresh] [--recreate-sandbox] [--gpu | --no-gpu] [--from <Dockerfile>] [--name <sandbox>] [--sandbox-gpu | --no-sandbox-gpu] [--sandbox-gpu-device <device>] [--agent <name>] [--agents <agents.yaml>] [--control-ui-port <N>] [--yes | -y] [--no-ollama-autostart] [${noticeAcceptFlag}]`,
|
||||
"",
|
||||
" --from <Dockerfile> uses the Dockerfile's parent directory as the Docker build context.",
|
||||
" --agents <agents.yaml> declares secondary OpenClaw agents, agents.defaults, and main-agent overrides; the YAML is baked into the sandbox image at build time.",
|
||||
" --no-ollama-autostart skips the wizard's eager Ollama auto-start during inference-provider selection so onboard surfaces the unreachable-Ollama warning and the default fallback model; later setup steps still expect a reachable Ollama, and on Linux hosts with a systemd Ollama unit the loopback-override path may still restart the daemon ahead of this gate.",
|
||||
" --gpu enables direct NVIDIA GPU access inside the sandbox; --no-gpu forces CPU sandbox behavior.",
|
||||
" --sandbox-gpu enables direct NVIDIA GPU access inside the sandbox; --no-sandbox-gpu forces CPU sandbox behavior.",
|
||||
" --sandbox-gpu-device passes a specific OpenShell GPU device selector to sandbox create; requires --sandbox-gpu.",
|
||||
" Put files referenced by COPY/ADD next to that Dockerfile, or move the Dockerfile into",
|
||||
" a dedicated build directory to avoid sending unrelated files to Docker.",
|
||||
" Common large directories are skipped: node_modules, .git, .venv, __pycache__.",
|
||||
" Credential-style files and directories such as .env*, .ssh, .aws, .netrc, .npmrc, secrets/, *.pem, and *.key are also skipped.",
|
||||
" Generated output directories such as dist/, build/, and target/ are still included.",
|
||||
"",
|
||||
];
|
||||
}
|
||||
|
||||
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" | "listAgents">,
|
||||
): OnboardCommandOptions {
|
||||
const error = deps.error ?? console.error;
|
||||
const exit = deps.exit ?? ((code: number) => process.exit(code));
|
||||
const parsedArgs = [...args];
|
||||
|
||||
let fromDockerfile: string | null = null;
|
||||
const fromIdx = parsedArgs.indexOf("--from");
|
||||
if (fromIdx !== -1) {
|
||||
const requestedFromDockerfile = parsedArgs[fromIdx + 1];
|
||||
if (!requestedFromDockerfile || requestedFromDockerfile.startsWith("--")) {
|
||||
error(" --from requires a path to a Dockerfile");
|
||||
printOnboardUsage(error, noticeAcceptFlag);
|
||||
exit(1);
|
||||
}
|
||||
const resolvedFromDockerfile = path.resolve(requestedFromDockerfile);
|
||||
if (!fs.existsSync(resolvedFromDockerfile)) {
|
||||
error(` --from path not found: ${resolvedFromDockerfile}`);
|
||||
exit(1);
|
||||
}
|
||||
if (!fs.statSync(resolvedFromDockerfile).isFile()) {
|
||||
error(` --from must point to a Dockerfile: ${resolvedFromDockerfile}`);
|
||||
exit(1);
|
||||
}
|
||||
fromDockerfile = requestedFromDockerfile;
|
||||
parsedArgs.splice(fromIdx, 2);
|
||||
}
|
||||
|
||||
let sandboxName: string | null = null;
|
||||
const nameIdx = parsedArgs.indexOf("--name");
|
||||
if (nameIdx !== -1) {
|
||||
const nameValue = parsedArgs[nameIdx + 1];
|
||||
if (typeof nameValue !== "string" || nameValue.length === 0 || nameValue.startsWith("--")) {
|
||||
error(" --name requires a sandbox name");
|
||||
printOnboardUsage(error, noticeAcceptFlag);
|
||||
exit(1);
|
||||
}
|
||||
sandboxName = nameValue;
|
||||
parsedArgs.splice(nameIdx, 2);
|
||||
}
|
||||
|
||||
let agent: string | null = null;
|
||||
const agentIdx = parsedArgs.indexOf("--agent");
|
||||
if (agentIdx !== -1) {
|
||||
const agentValue = parsedArgs[agentIdx + 1];
|
||||
if (typeof agentValue !== "string" || agentValue.startsWith("--")) {
|
||||
error(" --agent requires a name");
|
||||
printOnboardUsage(error, noticeAcceptFlag);
|
||||
exit(1);
|
||||
}
|
||||
const knownAgents = deps.listAgents?.() ?? [];
|
||||
const resolvedAgent =
|
||||
knownAgents.length > 0 ? resolveAgentNameAlias(agentValue, knownAgents) : agentValue;
|
||||
if (knownAgents.length > 0 && !resolvedAgent) {
|
||||
error(
|
||||
` Unknown agent '${agentValue}'. Available: ${knownAgents.join(", ")}${formatAgentAliasSuffix(knownAgents)}`,
|
||||
);
|
||||
printOnboardUsage(error, noticeAcceptFlag);
|
||||
exit(1);
|
||||
}
|
||||
agent = resolvedAgent;
|
||||
parsedArgs.splice(agentIdx, 2);
|
||||
}
|
||||
|
||||
let agentsManifest: string | null = null;
|
||||
const agentsIdx = parsedArgs.indexOf("--agents");
|
||||
if (agentsIdx !== -1) {
|
||||
const agentsValue = parsedArgs[agentsIdx + 1];
|
||||
if (
|
||||
typeof agentsValue !== "string" ||
|
||||
agentsValue.length === 0 ||
|
||||
agentsValue.startsWith("--")
|
||||
) {
|
||||
error(" --agents requires a path to a YAML manifest");
|
||||
printOnboardUsage(error, noticeAcceptFlag);
|
||||
exit(1);
|
||||
}
|
||||
if (!isOpenclawAgent(agent)) {
|
||||
error(
|
||||
` --agents is OpenClaw-specific and cannot be used with --agent ${agent}; the declarative manifest only drives OpenClaw secondary agents.`,
|
||||
);
|
||||
printOnboardUsage(error, noticeAcceptFlag);
|
||||
exit(1);
|
||||
}
|
||||
const resolved = path.resolve(agentsValue);
|
||||
if (!fs.existsSync(resolved)) {
|
||||
error(` --agents path not found: ${resolved}`);
|
||||
exit(1);
|
||||
}
|
||||
if (!fs.statSync(resolved).isFile()) {
|
||||
error(` --agents must point to a file: ${resolved}`);
|
||||
exit(1);
|
||||
}
|
||||
agentsManifest = resolved;
|
||||
parsedArgs.splice(agentsIdx, 2);
|
||||
}
|
||||
|
||||
let controlUiPort: number | null = null;
|
||||
const portIdx = parsedArgs.indexOf("--control-ui-port");
|
||||
if (portIdx !== -1) {
|
||||
const portValue = parsedArgs[portIdx + 1];
|
||||
if (typeof portValue !== "string" || portValue.startsWith("--")) {
|
||||
error(" --control-ui-port requires a port number");
|
||||
printOnboardUsage(error, noticeAcceptFlag);
|
||||
exit(1);
|
||||
}
|
||||
const parsed = Number(portValue);
|
||||
if (!Number.isInteger(parsed) || parsed < 1024 || parsed > 65535) {
|
||||
error(` --control-ui-port: ${portValue} is not a valid port (1024-65535)`);
|
||||
printOnboardUsage(error, noticeAcceptFlag);
|
||||
exit(1);
|
||||
}
|
||||
controlUiPort = parsed;
|
||||
parsedArgs.splice(portIdx, 2);
|
||||
}
|
||||
|
||||
const sandboxGpuFlag = parsedArgs.includes("--sandbox-gpu");
|
||||
const noSandboxGpuFlag = parsedArgs.includes("--no-sandbox-gpu");
|
||||
if (sandboxGpuFlag && noSandboxGpuFlag) {
|
||||
error(" --sandbox-gpu and --no-sandbox-gpu are mutually exclusive.");
|
||||
printOnboardUsage(error, noticeAcceptFlag);
|
||||
exit(1);
|
||||
}
|
||||
let sandboxGpu: "enable" | "disable" | null = null;
|
||||
if (sandboxGpuFlag) {
|
||||
sandboxGpu = "enable";
|
||||
parsedArgs.splice(parsedArgs.indexOf("--sandbox-gpu"), 1);
|
||||
}
|
||||
if (noSandboxGpuFlag) {
|
||||
sandboxGpu = "disable";
|
||||
parsedArgs.splice(parsedArgs.indexOf("--no-sandbox-gpu"), 1);
|
||||
}
|
||||
|
||||
let sandboxGpuDevice: string | null = null;
|
||||
const sandboxGpuDeviceIdx = parsedArgs.indexOf("--sandbox-gpu-device");
|
||||
if (sandboxGpuDeviceIdx !== -1) {
|
||||
const deviceValue = parsedArgs[sandboxGpuDeviceIdx + 1];
|
||||
if (
|
||||
typeof deviceValue !== "string" ||
|
||||
deviceValue.length === 0 ||
|
||||
deviceValue.startsWith("--")
|
||||
) {
|
||||
error(" --sandbox-gpu-device requires a device selector");
|
||||
printOnboardUsage(error, noticeAcceptFlag);
|
||||
exit(1);
|
||||
}
|
||||
sandboxGpuDevice = deviceValue;
|
||||
parsedArgs.splice(sandboxGpuDeviceIdx, 2);
|
||||
}
|
||||
if (sandboxGpu === "disable" && sandboxGpuDevice) {
|
||||
error(" --sandbox-gpu-device cannot be used with --no-sandbox-gpu.");
|
||||
printOnboardUsage(error, noticeAcceptFlag);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
const allowedArgs = new Set([...ONBOARD_BASE_ARGS, noticeAcceptFlag]);
|
||||
const unknownArgs = parsedArgs.filter((arg) => !allowedArgs.has(arg));
|
||||
if (unknownArgs.length > 0) {
|
||||
error(` Unknown onboard option(s): ${unknownArgs.join(", ")}`);
|
||||
printOnboardUsage(error, noticeAcceptFlag);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
const resume = parsedArgs.includes("--resume");
|
||||
const fresh = parsedArgs.includes("--fresh");
|
||||
if (resume && fresh) {
|
||||
error(" --resume and --fresh are mutually exclusive.");
|
||||
printOnboardUsage(error, noticeAcceptFlag);
|
||||
exit(1);
|
||||
}
|
||||
const gpu = parsedArgs.includes("--gpu");
|
||||
const noGpu = parsedArgs.includes("--no-gpu");
|
||||
if (gpu && noGpu) {
|
||||
error(" --gpu and --no-gpu are mutually exclusive.");
|
||||
printOnboardUsage(error, noticeAcceptFlag);
|
||||
exit(1);
|
||||
}
|
||||
if ((gpu && sandboxGpu === "disable") || (noGpu && sandboxGpu === "enable")) {
|
||||
error(" --gpu/--no-gpu conflict with the sandbox GPU flags.");
|
||||
printOnboardUsage(error, noticeAcceptFlag);
|
||||
exit(1);
|
||||
}
|
||||
if (noGpu && sandboxGpuDevice) {
|
||||
error(" --sandbox-gpu-device cannot be used with --no-gpu.");
|
||||
printOnboardUsage(error, noticeAcceptFlag);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
return {
|
||||
nonInteractive: parsedArgs.includes("--non-interactive"),
|
||||
resume,
|
||||
fresh,
|
||||
recreateSandbox: parsedArgs.includes("--recreate-sandbox"),
|
||||
fromDockerfile,
|
||||
sandboxName,
|
||||
sandboxGpu,
|
||||
sandboxGpuDevice,
|
||||
acceptThirdPartySoftware:
|
||||
parsedArgs.includes(noticeAcceptFlag) || String(deps.env[noticeAcceptEnv] || "") === "1",
|
||||
agent,
|
||||
agentsManifest,
|
||||
controlUiPort,
|
||||
gpu,
|
||||
noGpu,
|
||||
autoYes: parsedArgs.includes("--yes") || parsedArgs.includes("-y"),
|
||||
noOllamaAutostart: parsedArgs.includes("--no-ollama-autostart"),
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
if (options.noOllamaAutostart) process.env.NEMOCLAW_OLLAMA_NO_AUTOSTART = "1";
|
||||
if (options.agentsManifest) {
|
||||
applyAgentsManifestEnv(options.agentsManifest);
|
||||
}
|
||||
await deps.runOnboard(options);
|
||||
}
|
||||
|
||||
export async function runDeprecatedOnboardAliasCommand(
|
||||
deps: RunDeprecatedOnboardAliasCommandDeps,
|
||||
): Promise<void> {
|
||||
const cliName = CLI_NAME;
|
||||
const log = deps.log ?? console.log;
|
||||
log("");
|
||||
if (deps.kind === "setup") {
|
||||
log(` ⚠ \`${cliName} setup\` is deprecated. Use \`${cliName} onboard\` instead.`);
|
||||
} else {
|
||||
log(` ⚠ \`${cliName} setup-spark\` is deprecated.`);
|
||||
log(" Current OpenShell releases handle the old DGX Spark cgroup issue themselves.");
|
||||
log(` Use \`${cliName} onboard\` instead.`);
|
||||
}
|
||||
log("");
|
||||
await runOnboardCommand(deps);
|
||||
}
|
||||
|
|
@ -7,7 +7,8 @@ import path from "node:path";
|
|||
|
||||
import noticeConfig from "../../../bin/lib/usage-notice.json";
|
||||
|
||||
export const NOTICE_ACCEPT_FLAG = "--yes-i-accept-third-party-software";
|
||||
export const NOTICE_ACCEPT_FLAG_NAME = "yes-i-accept-third-party-software";
|
||||
export const NOTICE_ACCEPT_FLAG = `--${NOTICE_ACCEPT_FLAG_NAME}`;
|
||||
export const NOTICE_ACCEPT_ENV = "NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE";
|
||||
const OSC8_OPEN = "\u001B]8;;";
|
||||
const OSC8_CLOSE = "\u001B]8;;\u001B\\";
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { PARSER_EXIT_CODE, run, runWithEnv } from "./helpers";
|
||||
|
||||
|
|
@ -100,43 +100,48 @@ describe("CLI onboard compatibility", () => {
|
|||
expect(r.out).not.toContain("Nonexistent flag: --yes");
|
||||
});
|
||||
|
||||
it("passes onboard sandbox GPU flags to legacy validation", () => {
|
||||
it("lets oclif reject conflicting sandbox GPU flags", () => {
|
||||
const r = run(
|
||||
"onboard --sandbox-gpu --no-sandbox-gpu --non-interactive --yes-i-accept-third-party-software --yes",
|
||||
);
|
||||
expect(r.code).toBe(1);
|
||||
expect(r.out).toContain("--sandbox-gpu and --no-sandbox-gpu are mutually exclusive");
|
||||
expect(r.out).not.toContain("Nonexistent flag: --sandbox-gpu");
|
||||
expect(r.out).not.toContain("Nonexistent flag: --no-sandbox-gpu");
|
||||
expect(r.code).toBe(PARSER_EXIT_CODE);
|
||||
expect(r.out).toContain("--no-sandbox-gpu=true cannot also be provided");
|
||||
expect(r.out).toContain("--sandbox-gpu");
|
||||
});
|
||||
|
||||
it("passes onboard sandbox GPU device flags to legacy validation", () => {
|
||||
it("lets oclif enforce the sandbox GPU device dependency", () => {
|
||||
const r = run(
|
||||
"onboard --sandbox-gpu-device nvidia.com/gpu=0 --no-sandbox-gpu --non-interactive --yes-i-accept-third-party-software --yes",
|
||||
);
|
||||
expect(r.code).toBe(1);
|
||||
expect(r.out).toContain("--sandbox-gpu-device cannot be used with --no-sandbox-gpu");
|
||||
expect(r.out).not.toContain("Nonexistent flag: --sandbox-gpu-device");
|
||||
expect(r.code).toBe(PARSER_EXIT_CODE);
|
||||
expect(r.out).toContain("must be provided when using --sandbox-gpu-device");
|
||||
expect(r.out).toContain("--sandbox-gpu");
|
||||
});
|
||||
|
||||
it("setup --help exits 0 and shows onboard usage", () => {
|
||||
it("lets oclif reject privileged control UI ports", () => {
|
||||
const r = run("onboard --control-ui-port 80");
|
||||
expect(r.code).toBe(PARSER_EXIT_CODE);
|
||||
expect(r.out).toContain("Expected an integer greater than or equal to 1024 but received: 80");
|
||||
});
|
||||
|
||||
it("setup --help exits 0 and shows native deprecated-alias usage", () => {
|
||||
const r = run("setup --help");
|
||||
expect(r.code).toBe(0);
|
||||
expect(r.out.includes("setup` is deprecated")).toBeTruthy();
|
||||
expect(r.out.includes("Usage: nemoclaw onboard")).toBeTruthy();
|
||||
expect(r.out.includes("Unknown onboard option")).toBeFalsy();
|
||||
expect(r.out).toContain("Deprecated: 'nemoclaw setup' is now 'nemoclaw onboard'");
|
||||
expect(r.out).toContain("$ nemoclaw setup [flags]");
|
||||
expect(r.out).not.toContain("Unknown onboard option");
|
||||
});
|
||||
|
||||
it("setup forwards unknown options into onboard parsing", () => {
|
||||
it("setup rejects unknown options through oclif", () => {
|
||||
const r = run("setup --non-interactiv");
|
||||
expect(r.code).toBe(PARSER_EXIT_CODE);
|
||||
expect(r.out).toContain("Nonexistent flag: --non-interactiv");
|
||||
});
|
||||
|
||||
it("setup forwards --resume into onboard parsing", () => {
|
||||
it("setup forwards --resume into the shared onboard action", () => {
|
||||
const r = run("setup --resume --non-interactive --yes-i-accept-third-party-software --yes");
|
||||
expect(r.code).toBe(1);
|
||||
expect(r.out.includes("deprecated")).toBeTruthy();
|
||||
expect(r.out).toContain("Deprecated: 'nemoclaw setup' is now 'nemoclaw onboard'");
|
||||
expect(r.out.includes("No resumable onboarding session was found")).toBeTruthy();
|
||||
});
|
||||
|
||||
|
|
@ -197,13 +202,12 @@ describe("CLI onboard compatibility", () => {
|
|||
expect(r.out.includes("Cannot resume non-interactive onboard")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("setup-spark --help exits 0 and shows onboard usage", () => {
|
||||
it("setup-spark --help exits 0 and shows native deprecated-alias usage", () => {
|
||||
const r = run("setup-spark --help");
|
||||
expect(r.code).toBe(0);
|
||||
expect(r.out.includes("setup-spark` is deprecated")).toBeTruthy();
|
||||
expect(r.out.includes("Use `nemoclaw onboard` instead")).toBeTruthy();
|
||||
expect(r.out.includes("Usage: nemoclaw onboard")).toBeTruthy();
|
||||
expect(r.out.includes("Unknown onboard option")).toBeFalsy();
|
||||
expect(r.out).toContain("Deprecated: 'nemoclaw setup-spark' is now 'nemoclaw onboard'");
|
||||
expect(r.out).toContain("$ nemoclaw setup-spark [flags]");
|
||||
expect(r.out).not.toContain("Unknown onboard option");
|
||||
});
|
||||
|
||||
it("setup-spark is a deprecated compatibility alias for onboard", () => {
|
||||
|
|
@ -211,8 +215,7 @@ describe("CLI onboard compatibility", () => {
|
|||
"setup-spark --resume --non-interactive --yes-i-accept-third-party-software --yes",
|
||||
);
|
||||
expect(r.code).toBe(1);
|
||||
expect(r.out.includes("setup-spark` is deprecated")).toBeTruthy();
|
||||
expect(r.out.includes("Use `nemoclaw onboard` instead")).toBeTruthy();
|
||||
expect(r.out).toContain("Deprecated: 'nemoclaw setup-spark' is now 'nemoclaw onboard'");
|
||||
expect(r.out.includes("No resumable onboarding session was found")).toBeTruthy();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -299,10 +299,16 @@ JSON
|
|||
$mode = "flags";
|
||||
next;
|
||||
}
|
||||
if (/^\s*(ARGUMENTS|DESCRIPTION|EXAMPLES)\s*$/i || /^\s*$/) {
|
||||
if (/^\s*(ARGUMENTS|DESCRIPTION|EXAMPLES)\s*$/i) {
|
||||
$mode = "";
|
||||
next;
|
||||
}
|
||||
if (/^\s*$/) {
|
||||
# Oclif separates flag entries with blank lines, while the custom
|
||||
# one-line Usage format uses one to end its usage synopsis.
|
||||
$mode = "" if $mode eq "usage";
|
||||
next;
|
||||
}
|
||||
emit_flags($_) if $mode;
|
||||
' | LC_ALL=C sort -u
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { execSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { execTimeout } from "./helpers/timeouts";
|
||||
|
||||
|
|
@ -96,6 +96,13 @@ describe("nemohermes alias", () => {
|
|||
expect(out).toContain("NemoHermes");
|
||||
});
|
||||
|
||||
it("brands deprecated setup help with the invoked alias", () => {
|
||||
const { code, out } = runHermes("setup --help");
|
||||
expect(code).toBe(0);
|
||||
expect(out).toContain("Deprecated: 'nemohermes setup' is now 'nemohermes onboard'");
|
||||
expect(out).not.toContain("Deprecated: 'nemoclaw setup'");
|
||||
});
|
||||
|
||||
it("routes nemohermes uninstall as a global command, not a sandbox connect command", () => {
|
||||
const { code, out } = runHermes("uninstall --help");
|
||||
expect(code).toBe(0);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue