mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:30:42 +00:00
Add startup progress indicators (#71720)
* Add startup progress indicators * Narrow startup progress scope * Revert startup spinner delay to immediate feedback * Improve install.sh progress feedback for quiet steps * Show progress for installer download phases
This commit is contained in:
@@ -179,11 +179,13 @@ bootstrap_gum_temp() {
|
||||
gum_tmpdir="$(mktemp -d)"
|
||||
TMPFILES+=("$gum_tmpdir")
|
||||
|
||||
ui_info "Preparing spinner support"
|
||||
if ! download_file "${base}/${asset}" "$gum_tmpdir/$asset"; then
|
||||
GUM_REASON="download failed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
ui_info "Verifying spinner support download"
|
||||
if ! download_file "${base}/checksums.txt" "$gum_tmpdir/checksums.txt"; then
|
||||
GUM_REASON="checksum unavailable or failed"
|
||||
return 1
|
||||
@@ -444,6 +446,7 @@ run_quiet_step() {
|
||||
|
||||
local log
|
||||
log="$(mktempfile)"
|
||||
local showed_progress=false
|
||||
|
||||
if [[ -n "$GUM" ]] && gum_is_tty && ! is_shell_function "${1:-}"; then
|
||||
local cmd_quoted=""
|
||||
@@ -453,12 +456,20 @@ run_quiet_step() {
|
||||
if run_with_spinner "$title" bash -c "${cmd_quoted}>${log_quoted} 2>&1"; then
|
||||
return 0
|
||||
fi
|
||||
showed_progress=true
|
||||
else
|
||||
# Keep users informed even when gum spinner cannot run (for example shell functions).
|
||||
ui_info "${title}"
|
||||
showed_progress=true
|
||||
if "$@" >"$log" 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$showed_progress" == "false" ]]; then
|
||||
ui_info "${title}"
|
||||
fi
|
||||
|
||||
ui_error "${title} failed — re-run with --verbose for details"
|
||||
if [[ -s "$log" ]]; then
|
||||
tail -n 80 "$log" >&2 || true
|
||||
@@ -1432,7 +1443,7 @@ install_node() {
|
||||
if command -v apt-get &> /dev/null; then
|
||||
local tmp
|
||||
tmp="$(mktempfile)"
|
||||
download_file "https://deb.nodesource.com/setup_${NODE_DEFAULT_MAJOR}.x" "$tmp"
|
||||
run_quiet_step "Downloading NodeSource setup script" download_file "https://deb.nodesource.com/setup_${NODE_DEFAULT_MAJOR}.x" "$tmp"
|
||||
if is_root; then
|
||||
run_quiet_step "Configuring NodeSource repository" bash "$tmp"
|
||||
run_quiet_step "Installing Node.js" apt-get install -y -qq nodejs
|
||||
@@ -1443,7 +1454,7 @@ install_node() {
|
||||
elif command -v dnf &> /dev/null; then
|
||||
local tmp
|
||||
tmp="$(mktempfile)"
|
||||
download_file "https://rpm.nodesource.com/setup_${NODE_DEFAULT_MAJOR}.x" "$tmp"
|
||||
run_quiet_step "Downloading NodeSource setup script" download_file "https://rpm.nodesource.com/setup_${NODE_DEFAULT_MAJOR}.x" "$tmp"
|
||||
if is_root; then
|
||||
run_quiet_step "Configuring NodeSource repository" bash "$tmp"
|
||||
run_quiet_step "Installing Node.js" dnf install -y -q nodejs
|
||||
@@ -1454,7 +1465,7 @@ install_node() {
|
||||
elif command -v yum &> /dev/null; then
|
||||
local tmp
|
||||
tmp="$(mktempfile)"
|
||||
download_file "https://rpm.nodesource.com/setup_${NODE_DEFAULT_MAJOR}.x" "$tmp"
|
||||
run_quiet_step "Downloading NodeSource setup script" download_file "https://rpm.nodesource.com/setup_${NODE_DEFAULT_MAJOR}.x" "$tmp"
|
||||
if is_root; then
|
||||
run_quiet_step "Configuring NodeSource repository" bash "$tmp"
|
||||
run_quiet_step "Installing Node.js" yum install -y -q nodejs
|
||||
@@ -2265,6 +2276,8 @@ main() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
# bootstrap_gum_temp may perform network downloads before any spinner is available.
|
||||
echo -e "${INFO}Preparing installer interface...${NC}"
|
||||
bootstrap_gum_temp || true
|
||||
print_installer_banner
|
||||
print_gum_status
|
||||
|
||||
@@ -15,6 +15,7 @@ import { enableConsoleCapture } from "../logging.js";
|
||||
import type { PluginManifestCommandAliasRegistry } from "../plugins/manifest-command-aliases.js";
|
||||
import { resolveManifestCommandAliasOwner } from "../plugins/manifest-command-aliases.runtime.js";
|
||||
import { hasMemoryRuntime } from "../plugins/memory-state.js";
|
||||
import { createCliProgress } from "./progress.js";
|
||||
import { maybeWarnAboutDebugProxyCoverage } from "../proxy-capture/coverage.js";
|
||||
import {
|
||||
finalizeDebugProxyCapture,
|
||||
@@ -248,7 +249,25 @@ export async function runCli(argv: string[] = process.argv) {
|
||||
return;
|
||||
}
|
||||
const { runCrestodian } = await import("../crestodian/crestodian.js");
|
||||
await runCrestodian();
|
||||
const progress = createCliProgress({
|
||||
label: "Starting Crestodian…",
|
||||
indeterminate: true,
|
||||
delayMs: 0,
|
||||
fallback: "none",
|
||||
});
|
||||
let progressStopped = false;
|
||||
const stopProgress = () => {
|
||||
if (progressStopped) {
|
||||
return;
|
||||
}
|
||||
progressStopped = true;
|
||||
progress.done();
|
||||
};
|
||||
try {
|
||||
await runCrestodian({ onReady: stopProgress });
|
||||
} finally {
|
||||
stopProgress();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -268,95 +287,116 @@ export async function runCli(argv: string[] = process.argv) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Capture all console output into structured logs while keeping stdout/stderr behavior.
|
||||
enableConsoleCapture();
|
||||
|
||||
const [
|
||||
{ buildProgram },
|
||||
{ runFatalErrorHooks },
|
||||
{ installUnhandledRejectionHandler },
|
||||
{ restoreTerminalState },
|
||||
] = await Promise.all([
|
||||
import("./program.js"),
|
||||
import("../infra/fatal-error-hooks.js"),
|
||||
import("../infra/unhandled-rejections.js"),
|
||||
import("../terminal/restore.js"),
|
||||
]);
|
||||
const program = buildProgram();
|
||||
|
||||
// Global error handlers to prevent silent crashes from unhandled rejections/exceptions.
|
||||
// These log the error and exit gracefully instead of crashing without trace.
|
||||
installUnhandledRejectionHandler();
|
||||
|
||||
process.on("uncaughtException", (error) => {
|
||||
console.error("[openclaw] Uncaught exception:", formatUncaughtError(error));
|
||||
for (const message of runFatalErrorHooks({ reason: "uncaught_exception", error })) {
|
||||
console.error("[openclaw]", message);
|
||||
}
|
||||
restoreTerminalState("uncaught exception", { resumeStdinIfPaused: false });
|
||||
process.exit(1);
|
||||
const startupProgress = createCliProgress({
|
||||
label: "Loading OpenClaw CLI…",
|
||||
indeterminate: true,
|
||||
delayMs: 0,
|
||||
fallback: "none",
|
||||
});
|
||||
|
||||
const parseArgv = rewriteUpdateFlagArgv(normalizedArgv);
|
||||
const invocation = resolveCliArgvInvocation(parseArgv);
|
||||
// Register the primary command (builtin or subcli) so help and command parsing
|
||||
// are correct even with lazy command registration.
|
||||
const { primary } = invocation;
|
||||
if (primary && shouldRegisterPrimaryCommandOnly(parseArgv)) {
|
||||
const { getProgramContext } = await import("./program/program-context.js");
|
||||
const ctx = getProgramContext(program);
|
||||
if (ctx) {
|
||||
const { registerCoreCliByName } = await import("./program/command-registry.js");
|
||||
await registerCoreCliByName(program, ctx, primary, parseArgv);
|
||||
let startupProgressStopped = false;
|
||||
const stopStartupProgress = () => {
|
||||
if (startupProgressStopped) {
|
||||
return;
|
||||
}
|
||||
const { registerSubCliByName } = await import("./program/register.subclis.js");
|
||||
await registerSubCliByName(program, primary);
|
||||
}
|
||||
startupProgressStopped = true;
|
||||
startupProgress.done();
|
||||
};
|
||||
|
||||
const hasBuiltinPrimary =
|
||||
primary !== null &&
|
||||
program.commands.some(
|
||||
(command) => command.name() === primary || command.aliases().includes(primary),
|
||||
);
|
||||
const shouldSkipPluginRegistration = shouldSkipPluginCommandRegistration({
|
||||
argv: parseArgv,
|
||||
primary,
|
||||
hasBuiltinPrimary,
|
||||
});
|
||||
if (!shouldSkipPluginRegistration) {
|
||||
// Register plugin CLI commands before parsing
|
||||
const { registerPluginCliCommandsFromValidatedConfig } = await import("../plugins/cli.js");
|
||||
const config = await registerPluginCliCommandsFromValidatedConfig(
|
||||
program,
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
mode: "lazy",
|
||||
primary,
|
||||
},
|
||||
);
|
||||
if (config) {
|
||||
if (
|
||||
primary &&
|
||||
!program.commands.some(
|
||||
(command) => command.name() === primary || command.aliases().includes(primary),
|
||||
)
|
||||
) {
|
||||
const missingPluginCommandMessage = resolveMissingPluginCommandMessage(primary, config);
|
||||
if (missingPluginCommandMessage) {
|
||||
throw new Error(missingPluginCommandMessage);
|
||||
try {
|
||||
// Capture all console output into structured logs while keeping stdout/stderr behavior.
|
||||
enableConsoleCapture();
|
||||
|
||||
const [
|
||||
{ buildProgram },
|
||||
{ runFatalErrorHooks },
|
||||
{ installUnhandledRejectionHandler },
|
||||
{ restoreTerminalState },
|
||||
] = await Promise.all([
|
||||
import("./program.js"),
|
||||
import("../infra/fatal-error-hooks.js"),
|
||||
import("../infra/unhandled-rejections.js"),
|
||||
import("../terminal/restore.js"),
|
||||
]);
|
||||
const program = buildProgram();
|
||||
|
||||
// Global error handlers to prevent silent crashes from unhandled rejections/exceptions.
|
||||
// These log the error and exit gracefully instead of crashing without trace.
|
||||
installUnhandledRejectionHandler();
|
||||
|
||||
process.on("uncaughtException", (error) => {
|
||||
console.error("[openclaw] Uncaught exception:", formatUncaughtError(error));
|
||||
for (const message of runFatalErrorHooks({ reason: "uncaught_exception", error })) {
|
||||
console.error("[openclaw]", message);
|
||||
}
|
||||
restoreTerminalState("uncaught exception", { resumeStdinIfPaused: false });
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
const parseArgv = rewriteUpdateFlagArgv(normalizedArgv);
|
||||
const invocation = resolveCliArgvInvocation(parseArgv);
|
||||
// Register the primary command (builtin or subcli) so help and command parsing
|
||||
// are correct even with lazy command registration.
|
||||
const { primary } = invocation;
|
||||
if (primary && shouldRegisterPrimaryCommandOnly(parseArgv)) {
|
||||
const { getProgramContext } = await import("./program/program-context.js");
|
||||
const ctx = getProgramContext(program);
|
||||
if (ctx) {
|
||||
const { registerCoreCliByName } = await import("./program/command-registry.js");
|
||||
await registerCoreCliByName(program, ctx, primary, parseArgv);
|
||||
}
|
||||
const { registerSubCliByName } = await import("./program/register.subclis.js");
|
||||
await registerSubCliByName(program, primary);
|
||||
}
|
||||
|
||||
const hasBuiltinPrimary =
|
||||
primary !== null &&
|
||||
program.commands.some(
|
||||
(command) => command.name() === primary || command.aliases().includes(primary),
|
||||
);
|
||||
const shouldSkipPluginRegistration = shouldSkipPluginCommandRegistration({
|
||||
argv: parseArgv,
|
||||
primary,
|
||||
hasBuiltinPrimary,
|
||||
});
|
||||
if (!shouldSkipPluginRegistration) {
|
||||
// Register plugin CLI commands before parsing
|
||||
const { registerPluginCliCommandsFromValidatedConfig } = await import("../plugins/cli.js");
|
||||
const config = await registerPluginCliCommandsFromValidatedConfig(
|
||||
program,
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
mode: "lazy",
|
||||
primary,
|
||||
},
|
||||
);
|
||||
if (config) {
|
||||
if (
|
||||
primary &&
|
||||
!program.commands.some(
|
||||
(command) => command.name() === primary || command.aliases().includes(primary),
|
||||
)
|
||||
) {
|
||||
const missingPluginCommandMessage = resolveMissingPluginCommandMessage(primary, config);
|
||||
if (missingPluginCommandMessage) {
|
||||
throw new Error(missingPluginCommandMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await program.parseAsync(parseArgv);
|
||||
} catch (error) {
|
||||
if (!(error instanceof CommanderError)) {
|
||||
throw error;
|
||||
stopStartupProgress();
|
||||
|
||||
try {
|
||||
await program.parseAsync(parseArgv);
|
||||
} catch (error) {
|
||||
if (!(error instanceof CommanderError)) {
|
||||
throw error;
|
||||
}
|
||||
process.exitCode = error.exitCode;
|
||||
}
|
||||
process.exitCode = error.exitCode;
|
||||
} finally {
|
||||
stopStartupProgress();
|
||||
}
|
||||
} finally {
|
||||
await closeCliMemoryManagers();
|
||||
|
||||
@@ -54,11 +54,13 @@ describe("runCrestodian", () => {
|
||||
vi.stubEnv("OPENCLAW_CONFIG_PATH", path.join(tempDir, "openclaw.json"));
|
||||
const { runtime, lines } = createCrestodianTestRuntime();
|
||||
const runGatewayRestart = vi.fn(async () => {});
|
||||
const onReady = vi.fn();
|
||||
|
||||
await runCrestodian(
|
||||
{
|
||||
message: "the local bridge looks sleepy, poke it",
|
||||
deps: { runGatewayRestart },
|
||||
onReady,
|
||||
planWithAssistant: async () => ({
|
||||
reply: "I can queue a Gateway restart.",
|
||||
command: "restart gateway",
|
||||
@@ -69,6 +71,7 @@ describe("runCrestodian", () => {
|
||||
);
|
||||
|
||||
expect(runGatewayRestart).not.toHaveBeenCalled();
|
||||
expect(onReady).not.toHaveBeenCalled();
|
||||
expect(lines.join("\n")).toContain("[crestodian] planner: openai/gpt-5.5");
|
||||
expect(lines.join("\n")).toContain("[crestodian] interpreted: restart gateway");
|
||||
expect(lines.join("\n")).toContain("Plan: restart the Gateway. Say yes to apply.");
|
||||
@@ -80,16 +83,19 @@ describe("runCrestodian", () => {
|
||||
vi.stubEnv("OPENCLAW_CONFIG_PATH", path.join(tempDir, "openclaw.json"));
|
||||
const { runtime, lines } = createCrestodianTestRuntime();
|
||||
const planner = vi.fn(async () => ({ command: "restart gateway" }));
|
||||
const onReady = vi.fn();
|
||||
|
||||
await runCrestodian(
|
||||
{
|
||||
message: "models",
|
||||
planWithAssistant: planner,
|
||||
onReady,
|
||||
},
|
||||
runtime,
|
||||
);
|
||||
|
||||
expect(planner).not.toHaveBeenCalled();
|
||||
expect(onReady).not.toHaveBeenCalled();
|
||||
expect(lines.join("\n")).toContain("Default model:");
|
||||
});
|
||||
|
||||
@@ -99,12 +105,14 @@ describe("runCrestodian", () => {
|
||||
vi.stubEnv("OPENCLAW_CONFIG_PATH", path.join(tempDir, "openclaw.json"));
|
||||
const { runtime, lines } = createCrestodianTestRuntime();
|
||||
const runInteractiveTui = vi.fn(async () => {});
|
||||
const onReady = vi.fn();
|
||||
|
||||
await runCrestodian(
|
||||
{
|
||||
input: { isTTY: true } as unknown as NodeJS.ReadableStream,
|
||||
output: { isTTY: true } as unknown as NodeJS.WritableStream,
|
||||
runInteractiveTui,
|
||||
onReady,
|
||||
},
|
||||
runtime,
|
||||
);
|
||||
@@ -113,6 +121,7 @@ describe("runCrestodian", () => {
|
||||
expect.objectContaining({ runInteractiveTui }),
|
||||
runtime,
|
||||
);
|
||||
expect(onReady).toHaveBeenCalledTimes(1);
|
||||
expect(lines.join("\n")).not.toContain("Say: status");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { stdin as defaultStdin, stdout as defaultStdout } from "node:process";
|
||||
import { defaultRuntime, writeRuntimeJson, type RuntimeEnv } from "../runtime.js";
|
||||
import { withProgress } from "../cli/progress.js";
|
||||
import type { CrestodianAssistantPlanner } from "./assistant.js";
|
||||
import { resolveCrestodianOperation } from "./dialogue.js";
|
||||
import {
|
||||
@@ -19,6 +20,7 @@ export type RunCrestodianOptions = {
|
||||
yes?: boolean;
|
||||
json?: boolean;
|
||||
interactive?: boolean;
|
||||
onReady?: () => void;
|
||||
deps?: CrestodianCommandDeps;
|
||||
planWithAssistant?: CrestodianAssistantPlanner;
|
||||
input?: NodeJS.ReadableStream;
|
||||
@@ -49,7 +51,15 @@ export async function runCrestodian(
|
||||
}
|
||||
|
||||
if (opts.message?.trim()) {
|
||||
const overview = await loadCrestodianOverview();
|
||||
const overview = await withProgress(
|
||||
{
|
||||
label: "Loading Crestodian overview…",
|
||||
indeterminate: true,
|
||||
delayMs: 0,
|
||||
fallback: "none",
|
||||
},
|
||||
async () => await loadCrestodianOverview(),
|
||||
);
|
||||
runtime.log(formatCrestodianOverview(overview));
|
||||
runtime.log("");
|
||||
await runOneShot(opts.message, runtime, opts);
|
||||
@@ -68,5 +78,6 @@ export async function runCrestodian(
|
||||
|
||||
const runInteractiveTui =
|
||||
opts.runInteractiveTui ?? (await import("./tui-backend.js")).runCrestodianTui;
|
||||
opts.onReady?.();
|
||||
await runInteractiveTui(opts, runtime);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user