fix(cli): improve bootstrap onboard UX and fix command references
Always pass --skip-ui and --accept-risk to openclaw onboard so the wizard never prompts for TUI/Web UI selection — bootstrap manages the web UI lifecycle itself. Add a post-onboard spinner to eliminate the silent gap while config-set calls, gateway probing, and web runtime startup run. Fix remediation messages to use `npx denchclaw`.
This commit is contained in:
parent
912e7711bb
commit
78e04e20b8
@ -290,11 +290,11 @@ if [[ "$SKIP_NPX_SMOKE" != true ]]; then
|
||||
npx --yes "${PACKAGE_NAME}@${VERSION}" --version
|
||||
verify_npx_command "$VERSION" "npx dench (via dench package)" \
|
||||
npx --yes "${ALIAS_PACKAGE_NAME}@${VERSION}" --version
|
||||
verify_npx_invocation "npx dench update --help" \
|
||||
verify_npx_invocation "npx denchclaw update --help" \
|
||||
npx --yes "${ALIAS_PACKAGE_NAME}@${VERSION}" update --help
|
||||
verify_npx_invocation "npx dench start --help" \
|
||||
verify_npx_invocation "npx denchclaw start --help" \
|
||||
npx --yes "${ALIAS_PACKAGE_NAME}@${VERSION}" start --help
|
||||
verify_npx_invocation "npx dench stop --help" \
|
||||
verify_npx_invocation "npx denchclaw stop --help" \
|
||||
npx --yes "${ALIAS_PACKAGE_NAME}@${VERSION}" stop --help
|
||||
fi
|
||||
|
||||
|
||||
@ -1122,14 +1122,14 @@ function remediationForGatewayFailure(
|
||||
if (normalized.includes("address already in use") || normalized.includes("eaddrinuse")) {
|
||||
return `Port ${port} is busy. The bootstrap will auto-assign an available port, or you can explicitly specify one with \`--gateway-port <port>\`.`;
|
||||
}
|
||||
return `Run \`openclaw --profile ${profile} doctor --fix\` and retry \`denchclaw bootstrap --profile ${profile} --force-onboard\`.`;
|
||||
return `Run \`openclaw --profile ${profile} doctor --fix\` and retry \`npx denchclaw bootstrap\`.`;
|
||||
}
|
||||
|
||||
function remediationForWebUiFailure(port: number): string {
|
||||
return [
|
||||
`Web UI did not respond on ${port}.`,
|
||||
`Run \`dench update --web-port ${port}\` to refresh the managed web runtime.`,
|
||||
`If the port is stuck, run \`dench stop --web-port ${port}\` first.`,
|
||||
`Run \`npx denchclaw update --web-port ${port}\` to refresh the managed web runtime.`,
|
||||
`If the port is stuck, run \`npx denchclaw stop --web-port ${port}\` first.`,
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
@ -1478,25 +1478,26 @@ export async function bootstrapCommand(
|
||||
}
|
||||
|
||||
const bootstrapStartTime = Date.now();
|
||||
track("cli_bootstrap_started", { version: VERSION });
|
||||
|
||||
if (!opts.json) {
|
||||
const telemetryCfg = readTelemetryConfig();
|
||||
if (!telemetryCfg.noticeShown) {
|
||||
runtime.log(
|
||||
theme.muted(
|
||||
"DenchClaw collects anonymous telemetry to improve the product.\n" +
|
||||
"Dench collects anonymous telemetry to improve the product.\n" +
|
||||
"No personal data is ever collected. Disable anytime:\n" +
|
||||
" denchclaw telemetry disable\n" +
|
||||
" npx denchclaw telemetry disable\n" +
|
||||
" DENCHCLAW_TELEMETRY_DISABLED=1\n" +
|
||||
" DO_NOT_TRACK=1\n" +
|
||||
"Learn more: https://github.com/openclaw/openclaw/blob/main/TELEMETRY.md\n",
|
||||
"Learn more: https://github.com/DenchHQ/DenchClaw/blob/main/TELEMETRY.md\n",
|
||||
),
|
||||
);
|
||||
markNoticeShown();
|
||||
}
|
||||
}
|
||||
|
||||
track("cli_bootstrap_started", { version: VERSION });
|
||||
|
||||
const installResult = await ensureOpenClawCliAvailable({
|
||||
stateDir,
|
||||
showProgress: !opts.json,
|
||||
@ -1578,11 +1579,11 @@ export async function bootstrapCommand(
|
||||
onboardArgv.push("--reset");
|
||||
}
|
||||
if (nonInteractive) {
|
||||
onboardArgv.push("--non-interactive", "--accept-risk");
|
||||
}
|
||||
if (opts.noOpen) {
|
||||
onboardArgv.push("--skip-ui");
|
||||
onboardArgv.push("--non-interactive");
|
||||
}
|
||||
|
||||
onboardArgv.push("--accept-risk", "--skip-ui");
|
||||
|
||||
if (nonInteractive) {
|
||||
await runOpenClawOrThrow({
|
||||
openclawCommand,
|
||||
@ -1605,21 +1606,29 @@ export async function bootstrapCommand(
|
||||
packageRoot,
|
||||
});
|
||||
|
||||
const postOnboardSpinner = !opts.json ? spinner() : null;
|
||||
postOnboardSpinner?.start("Finalizing configuration…");
|
||||
|
||||
// Ensure gateway.mode=local so the gateway never drifts to remote mode.
|
||||
// Keep this post-onboard so we normalize any wizard defaults.
|
||||
await ensureGatewayModeLocal(openclawCommand, profile);
|
||||
postOnboardSpinner?.message("Configuring gateway port…");
|
||||
// Persist the assigned port so all runtime clients (including web) resolve
|
||||
// the same gateway target on subsequent requests.
|
||||
await ensureGatewayPort(openclawCommand, profile, gatewayPort);
|
||||
postOnboardSpinner?.message("Setting tools profile…");
|
||||
// DenchClaw requires the full tool profile; onboarding defaults can drift to
|
||||
// messaging-only, so enforce this on every bootstrap run.
|
||||
await ensureToolsProfile(openclawCommand, profile);
|
||||
|
||||
postOnboardSpinner?.message("Configuring subagent defaults…");
|
||||
await ensureSubagentDefaults(openclawCommand, profile);
|
||||
|
||||
postOnboardSpinner?.message("Probing gateway health…");
|
||||
let gatewayProbe = await probeGateway(openclawCommand, profile, gatewayPort);
|
||||
let gatewayAutoFix: GatewayAutoFixResult | undefined;
|
||||
if (!gatewayProbe.ok) {
|
||||
postOnboardSpinner?.message("Gateway unreachable, attempting auto-fix…");
|
||||
gatewayAutoFix = await attemptGatewayAutoFix({
|
||||
openclawCommand,
|
||||
profile,
|
||||
@ -1638,6 +1647,7 @@ export async function bootstrapCommand(
|
||||
}
|
||||
const gatewayUrl = `ws://127.0.0.1:${gatewayPort}`;
|
||||
const preferredWebPort = parseOptionalPort(opts.webPort) ?? DEFAULT_WEB_APP_PORT;
|
||||
postOnboardSpinner?.message(`Starting web runtime on port ${preferredWebPort}…`);
|
||||
const webRuntimeStatus = await ensureManagedWebRuntime({
|
||||
stateDir,
|
||||
packageRoot,
|
||||
@ -1645,6 +1655,11 @@ export async function bootstrapCommand(
|
||||
port: preferredWebPort,
|
||||
gatewayPort,
|
||||
});
|
||||
postOnboardSpinner?.stop(
|
||||
webRuntimeStatus.ready
|
||||
? "Post-onboard setup complete."
|
||||
: "Post-onboard setup complete (web runtime unhealthy).",
|
||||
);
|
||||
const webReachable = webRuntimeStatus.ready;
|
||||
const webUrl = `http://localhost:${preferredWebPort}`;
|
||||
const diagnostics = buildBootstrapDiagnostics({
|
||||
|
||||
@ -2,6 +2,7 @@ import type { Command } from "commander";
|
||||
import { getPrimaryCommand } from "../argv.js";
|
||||
import type { ProgramContext } from "./context.js";
|
||||
import { registerBootstrapCommand } from "./register.bootstrap.js";
|
||||
import { registerRestartCommand } from "./register.restart.js";
|
||||
import { registerStartCommand } from "./register.start.js";
|
||||
import { registerStopCommand } from "./register.stop.js";
|
||||
import { registerTelemetryCommand } from "./register.telemetry.js";
|
||||
@ -48,6 +49,13 @@ const CORE_CLI_ENTRIES: CoreCliEntry[] = [
|
||||
registerStartCommand(program);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "restart",
|
||||
description: "Restart Dench managed web runtime",
|
||||
register: ({ program }) => {
|
||||
registerRestartCommand(program);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "telemetry",
|
||||
description: "Manage anonymous telemetry",
|
||||
|
||||
22
src/cli/program/register.restart.ts
Normal file
22
src/cli/program/register.restart.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import type { Command } from "commander";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { runCommandWithRuntime } from "../cli-utils.js";
|
||||
import { restartWebRuntimeCommand } from "../web-runtime-command.js";
|
||||
|
||||
export function registerRestartCommand(program: Command) {
|
||||
program
|
||||
.command("restart")
|
||||
.description("Restart Dench managed web runtime (stop then start)")
|
||||
.option("--profile <name>", "Compatibility flag; non-dench values are ignored with a warning")
|
||||
.option("--web-port <port>", "Web runtime port override")
|
||||
.option("--json", "Output summary as JSON", false)
|
||||
.action(async (opts) => {
|
||||
await runCommandWithRuntime(defaultRuntime, async () => {
|
||||
await restartWebRuntimeCommand({
|
||||
profile: opts.profile as string | undefined,
|
||||
webPort: opts.webPort as string | undefined,
|
||||
json: Boolean(opts.json),
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -75,6 +75,7 @@ vi.mock("./web-runtime.js", () => ({
|
||||
}));
|
||||
|
||||
import {
|
||||
restartWebRuntimeCommand,
|
||||
startWebRuntimeCommand,
|
||||
stopWebRuntimeCommand,
|
||||
updateWebRuntimeCommand,
|
||||
@ -303,7 +304,7 @@ describe("startWebRuntimeCommand", () => {
|
||||
});
|
||||
const runtime = runtimeStub();
|
||||
|
||||
await expect(startWebRuntimeCommand({}, runtime)).rejects.toThrow("dench update");
|
||||
await expect(startWebRuntimeCommand({}, runtime)).rejects.toThrow("npx denchclaw update");
|
||||
expect(spawnMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@ -331,3 +332,66 @@ describe("startWebRuntimeCommand", () => {
|
||||
expect(summary.started).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("restartWebRuntimeCommand", () => {
|
||||
beforeEach(() => {
|
||||
webRuntimeMocks.ensureManagedWebRuntime.mockClear();
|
||||
webRuntimeMocks.stopManagedWebRuntime.mockReset();
|
||||
webRuntimeMocks.stopManagedWebRuntime.mockImplementation(
|
||||
async () =>
|
||||
({
|
||||
port: 3100,
|
||||
stoppedPids: [1234],
|
||||
skippedForeignPids: [],
|
||||
}) as { port: number; stoppedPids: number[]; skippedForeignPids: number[] },
|
||||
);
|
||||
webRuntimeMocks.startManagedWebRuntime.mockReset();
|
||||
webRuntimeMocks.startManagedWebRuntime.mockImplementation(() => ({
|
||||
started: true,
|
||||
pid: 7788,
|
||||
runtimeServerPath: "/tmp/.openclaw-dench/web-runtime/app/server.js",
|
||||
}));
|
||||
webRuntimeMocks.waitForWebRuntime.mockReset();
|
||||
webRuntimeMocks.waitForWebRuntime.mockImplementation(
|
||||
async () =>
|
||||
({ ok: true, reason: "profiles payload shape is valid" }) as {
|
||||
ok: boolean;
|
||||
reason: string;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("stops and restarts managed runtime (same stop+start lifecycle as start command)", async () => {
|
||||
const runtime = runtimeStub();
|
||||
const summary = await restartWebRuntimeCommand(
|
||||
{
|
||||
webPort: "3100",
|
||||
},
|
||||
runtime,
|
||||
);
|
||||
|
||||
expect(webRuntimeMocks.stopManagedWebRuntime).toHaveBeenCalledWith({
|
||||
stateDir: "/tmp/.openclaw-dench",
|
||||
port: 3100,
|
||||
includeLegacyStandalone: true,
|
||||
});
|
||||
expect(webRuntimeMocks.startManagedWebRuntime).toHaveBeenCalledWith({
|
||||
stateDir: "/tmp/.openclaw-dench",
|
||||
port: 3100,
|
||||
gatewayPort: 18789,
|
||||
});
|
||||
expect(webRuntimeMocks.ensureManagedWebRuntime).not.toHaveBeenCalled();
|
||||
expect(summary.started).toBe(true);
|
||||
});
|
||||
|
||||
it("outputs restart heading instead of start (distinct user-facing label)", async () => {
|
||||
const runtime = runtimeStub();
|
||||
await restartWebRuntimeCommand({}, runtime);
|
||||
|
||||
const logCalls = (runtime.log as ReturnType<typeof vi.fn>).mock.calls.map(
|
||||
([msg]: [string]) => msg,
|
||||
);
|
||||
expect(logCalls.some((msg) => typeof msg === "string" && msg.includes("restart"))).toBe(true);
|
||||
expect(logCalls.some((msg) => typeof msg === "string" && /\bstart\b/.test(msg) && !msg.includes("restart"))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@ -171,7 +171,7 @@ async function runOpenClawUpdateWithProgress(openclawCommand: string): Promise<v
|
||||
throw new Error(
|
||||
detail
|
||||
? `OpenClaw update failed.\n${detail}`
|
||||
: "OpenClaw update failed. Fix this before running `dench update` again.",
|
||||
: "OpenClaw update failed. Fix this before running `npx denchclaw update` again.",
|
||||
);
|
||||
}
|
||||
|
||||
@ -399,9 +399,20 @@ export async function stopWebRuntimeCommand(
|
||||
return summary;
|
||||
}
|
||||
|
||||
export type RestartWebRuntimeOptions = StartWebRuntimeOptions;
|
||||
export type RestartWebRuntimeSummary = StartWebRuntimeSummary;
|
||||
|
||||
export async function restartWebRuntimeCommand(
|
||||
opts: RestartWebRuntimeOptions,
|
||||
runtime: RuntimeEnv = defaultRuntime,
|
||||
): Promise<RestartWebRuntimeSummary> {
|
||||
return startWebRuntimeCommand(opts, runtime, "restart");
|
||||
}
|
||||
|
||||
export async function startWebRuntimeCommand(
|
||||
opts: StartWebRuntimeOptions,
|
||||
runtime: RuntimeEnv = defaultRuntime,
|
||||
label: "start" | "restart" = "start",
|
||||
): Promise<StartWebRuntimeSummary> {
|
||||
const appliedProfile = applyCliProfileEnv({ profile: opts.profile });
|
||||
const profile = appliedProfile.effectiveProfile;
|
||||
@ -436,7 +447,7 @@ export async function startWebRuntimeCommand(
|
||||
throw new Error(
|
||||
[
|
||||
`Managed web runtime is missing at ${runtimeServerPath}.`,
|
||||
"Run `dench update` (or `dench bootstrap`) to install/update the web runtime first.",
|
||||
"Run `npx denchclaw update` (or `npx denchclaw`) to install/update the web runtime first.",
|
||||
].join(" "),
|
||||
);
|
||||
}
|
||||
@ -457,7 +468,7 @@ export async function startWebRuntimeCommand(
|
||||
}
|
||||
|
||||
runtime.log("");
|
||||
runtime.log(theme.heading("Dench web start"));
|
||||
runtime.log(theme.heading(`Dench web ${label}`));
|
||||
runtime.log(`Profile: ${profile}`);
|
||||
runtime.log(`Web port: ${selectedPort}`);
|
||||
runtime.log(`Restarted managed web runtime: ${summary.started ? "yes" : "no"}`);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user