diff --git a/scripts/deploy.sh b/scripts/deploy.sh index e47be244e96..a5bf55430b1 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -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 diff --git a/src/cli/bootstrap-external.ts b/src/cli/bootstrap-external.ts index 797d25dc9ef..e8ef37d88df 100644 --- a/src/cli/bootstrap-external.ts +++ b/src/cli/bootstrap-external.ts @@ -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 \`.`; } - 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({ diff --git a/src/cli/program/command-registry.ts b/src/cli/program/command-registry.ts index ca84d5d2070..d32975a6210 100644 --- a/src/cli/program/command-registry.ts +++ b/src/cli/program/command-registry.ts @@ -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", diff --git a/src/cli/program/register.restart.ts b/src/cli/program/register.restart.ts new file mode 100644 index 00000000000..85993f71038 --- /dev/null +++ b/src/cli/program/register.restart.ts @@ -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 ", "Compatibility flag; non-dench values are ignored with a warning") + .option("--web-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), + }); + }); + }); +} diff --git a/src/cli/web-runtime-command.test.ts b/src/cli/web-runtime-command.test.ts index d0a58f1506d..498c862ff59 100644 --- a/src/cli/web-runtime-command.test.ts +++ b/src/cli/web-runtime-command.test.ts @@ -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).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); + }); +}); diff --git a/src/cli/web-runtime-command.ts b/src/cli/web-runtime-command.ts index 1844f737626..2d9f6e10db3 100644 --- a/src/cli/web-runtime-command.ts +++ b/src/cli/web-runtime-command.ts @@ -171,7 +171,7 @@ async function runOpenClawUpdateWithProgress(openclawCommand: string): Promise { + return startWebRuntimeCommand(opts, runtime, "restart"); +} + export async function startWebRuntimeCommand( opts: StartWebRuntimeOptions, runtime: RuntimeEnv = defaultRuntime, + label: "start" | "restart" = "start", ): Promise { 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"}`);