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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions src/lib/inventory-commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,39 @@ describe("inventory commands", () => {
);
});

it("prints the dashboard URL for each sandbox when dashboardPort is set", async () => {
const lines: string[] = [];
await listSandboxesCommand({
recoverRegistryEntries: async () => ({
sandboxes: [
{
name: "alpha",
model: "m",
provider: "p",
gpuEnabled: false,
policies: [],
dashboardPort: 18789,
},
{
name: "beta",
model: "m",
provider: "p",
gpuEnabled: false,
policies: [],
dashboardPort: 18790,
},
],
defaultSandbox: "alpha",
}),
getLiveInference: () => null,
loadLastSession: () => null,
log: (message = "") => lines.push(message),
});

expect(lines).toContain(" dashboard: http://127.0.0.1:18789");
expect(lines).toContain(" dashboard: http://127.0.0.1:18790");
});

it("shows stored sandbox inference instead of live gateway inference in list output", async () => {
const lines: string[] = [];
await listSandboxesCommand({
Expand Down Expand Up @@ -88,6 +121,25 @@ describe("inventory commands", () => {
);
});

it("prints the dashboard URL per sandbox in status when dashboardPort is set", () => {
const lines: string[] = [];
showStatusCommand({
listSandboxes: () => ({
sandboxes: [
{ name: "alpha", model: "m", dashboardPort: 18789 },
{ name: "beta", model: "m", dashboardPort: 18790 },
],
defaultSandbox: "alpha",
}),
getLiveInference: () => null,
showServiceStatus: vi.fn(),
log: (message = "") => lines.push(message),
});

expect(lines).toContain(" dashboard: http://127.0.0.1:18789");
expect(lines).toContain(" dashboard: http://127.0.0.1:18790");
});

it("flags messaging bridge as degraded when checkMessagingBridgeHealth reports conflicts", () => {
const lines: string[] = [];
const checkMessagingBridgeHealth = vi.fn().mockReturnValue([
Expand Down
7 changes: 7 additions & 0 deletions src/lib/inventory-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface SandboxEntry {
policies?: string[] | null;
messagingChannels?: string[] | null;
agent?: string | null;
dashboardPort?: number;
}

export interface MessagingBridgeHealth {
Expand Down Expand Up @@ -98,6 +99,9 @@ export async function listSandboxesCommand(deps: ListSandboxesCommandDeps): Prom
const connected = sessionCount !== null && sessionCount > 0 ? " ●" : "";
log(` ${sb.name}${def}${connected}`);
log(` model: ${model} provider: ${provider} ${gpu} policies: ${presets}`);
if (typeof sb.dashboardPort === "number") {
log(` dashboard: http://127.0.0.1:${sb.dashboardPort}`);
}
}
log("");
log(" * = default sandbox");
Expand All @@ -116,6 +120,9 @@ export function showStatusCommand(deps: ShowStatusCommandDeps): void {
const def = isDefault ? " *" : "";
const model = sb.model;
log(` ${sb.name}${def}${model ? ` (${model})` : ""}`);
if (typeof sb.dashboardPort === "number") {
log(` dashboard: http://127.0.0.1:${sb.dashboardPort}`);
}
}
log("");
}
Expand Down
39 changes: 39 additions & 0 deletions src/lib/onboard-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ describe("onboard command", () => {
acceptThirdPartySoftware: true,
agent: null,
dangerouslySkipPermissions: false,
controlUiPort: null,
});
});

Expand All @@ -57,6 +58,7 @@ describe("onboard command", () => {
acceptThirdPartySoftware: true,
agent: null,
dangerouslySkipPermissions: false,
controlUiPort: null,
});
});

Expand All @@ -81,6 +83,7 @@ describe("onboard command", () => {
acceptThirdPartySoftware: false,
agent: null,
dangerouslySkipPermissions: false,
controlUiPort: null,
});
});

Expand Down Expand Up @@ -128,9 +131,43 @@ describe("onboard command", () => {
acceptThirdPartySoftware: false,
agent: null,
dangerouslySkipPermissions: false,
controlUiPort: null,
});
});

it("parses --control-ui-port <n>", () => {
const result = parseOnboardArgs(
["--resume", "--control-ui-port", "18795"],
"--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(18795);
});

it("exits when --control-ui-port is out of range", () => {
expect(() =>
parseOnboardArgs(
["--control-ui-port", "80"],
"--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 --from is missing its Dockerfile path", () => {
expect(() =>
parseOnboardArgs(
Expand Down Expand Up @@ -191,6 +228,7 @@ describe("onboard command", () => {
acceptThirdPartySoftware: false,
agent: "openclaw",
dangerouslySkipPermissions: true,
controlUiPort: null,
});
});

Expand Down Expand Up @@ -242,6 +280,7 @@ describe("onboard command", () => {
acceptThirdPartySoftware: false,
agent: null,
dangerouslySkipPermissions: false,
controlUiPort: null,
});
});

Expand Down
32 changes: 31 additions & 1 deletion src/lib/onboard-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface OnboardCommandOptions {
acceptThirdPartySoftware: boolean;
agent: string | null;
dangerouslySkipPermissions: boolean;
controlUiPort: number | null;
}

export interface RunOnboardCommandDeps {
Expand Down Expand Up @@ -36,7 +37,7 @@ const ONBOARD_BASE_ARGS = [

function onboardUsageLines(noticeAcceptFlag: string): string[] {
return [
` Usage: nemoclaw onboard [--non-interactive] [--resume] [--recreate-sandbox] [--from <Dockerfile>] [--agent <name>] [--dangerously-skip-permissions] [${noticeAcceptFlag}]`,
` Usage: nemoclaw onboard [--non-interactive] [--resume] [--recreate-sandbox] [--from <Dockerfile>] [--agent <name>] [--control-ui-port <n>] [--dangerously-skip-permissions] [${noticeAcceptFlag}]`,
"",
];
}
Expand Down Expand Up @@ -69,6 +70,28 @@ export function parseOnboardArgs(
parsedArgs.splice(fromIdx, 2);
}

let controlUiPort: number | null = null;
const controlUiPortIdx = parsedArgs.indexOf("--control-ui-port");
if (controlUiPortIdx !== -1) {
const raw = parsedArgs[controlUiPortIdx + 1];
if (typeof raw !== "string" || raw.startsWith("--")) {
error(" --control-ui-port requires a port number");
printOnboardUsage(error, noticeAcceptFlag);
exit(1);
}
if (!/^\d+$/.test(raw)) {
error(` --control-ui-port '${raw}' must be an integer between 1024 and 65535`);
exit(1);
}
const parsed = Number(raw);
if (parsed < 1024 || parsed > 65535) {
error(` --control-ui-port '${raw}' must be an integer between 1024 and 65535`);
exit(1);
}
controlUiPort = parsed;
parsedArgs.splice(controlUiPortIdx, 2);
}

let agent: string | null = null;
const agentIdx = parsedArgs.indexOf("--agent");
if (agentIdx !== -1) {
Expand Down Expand Up @@ -105,6 +128,7 @@ export function parseOnboardArgs(
parsedArgs.includes(noticeAcceptFlag) || String(deps.env[noticeAcceptEnv] || "") === "1",
agent,
dangerouslySkipPermissions: parsedArgs.includes("--dangerously-skip-permissions"),
controlUiPort,
};
}

Expand All @@ -116,6 +140,12 @@ export async function runOnboardCommand(deps: RunOnboardCommandDeps): Promise<vo
}

const options = parseOnboardArgs(deps.args, deps.noticeAcceptFlag, deps.noticeAcceptEnv, deps);
// --control-ui-port takes precedence over existing CHAT_UI_URL. The onboard
// flow reads CHAT_UI_URL directly in many places, so setting it here is the
// single seam that makes the flag effective without threading it through.
if (options.controlUiPort !== null) {
process.env.CHAT_UI_URL = `http://127.0.0.1:${options.controlUiPort}`;
}
await deps.runOnboard(options);
}

Expand Down
Loading
Loading