fix: harden onboarding for non-systemd environments
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
- macOS: avoid clearing Launch at Login during app initialization. (#607) — thanks @wes-davis
|
- macOS: avoid clearing Launch at Login during app initialization. (#607) — thanks @wes-davis
|
||||||
|
- Onboarding: skip systemd checks/daemon installs when systemd user services are unavailable; add onboarding flags to skip flow steps and stabilize Docker E2E. (#573) — thanks @steipete
|
||||||
- macOS: add node bridge heartbeat pings to detect half-open sockets and reconnect cleanly. (#572) — thanks @ngutman
|
- macOS: add node bridge heartbeat pings to detect half-open sockets and reconnect cleanly. (#572) — thanks @ngutman
|
||||||
- Node bridge: harden keepalive + heartbeat handling (TCP keepalive, better disconnects, and keepalive config tests). (#577) — thanks @steipete
|
- Node bridge: harden keepalive + heartbeat handling (TCP keepalive, better disconnects, and keepalive config tests). (#577) — thanks @steipete
|
||||||
- Control UI: improve mobile responsiveness. (#558) — thanks @carlulsoe
|
- Control UI: improve mobile responsiveness. (#558) — thanks @carlulsoe
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ echo " - Gateway token: $CLAWDBOT_GATEWAY_TOKEN"
|
|||||||
echo " - Tailscale exposure: Off"
|
echo " - Tailscale exposure: Off"
|
||||||
echo " - Install Gateway daemon: No"
|
echo " - Install Gateway daemon: No"
|
||||||
echo ""
|
echo ""
|
||||||
docker compose -f "$COMPOSE_FILE" run --rm clawdbot-cli onboard
|
docker compose -f "$COMPOSE_FILE" run --rm clawdbot-cli onboard --no-install-daemon
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "==> Provider setup (optional)"
|
echo "==> Provider setup (optional)"
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ docker run --rm -t "$IMAGE_NAME" bash -lc '
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
trap "" PIPE
|
trap "" PIPE
|
||||||
export TERM=xterm-256color
|
export TERM=xterm-256color
|
||||||
|
ONBOARD_FLAGS="--flow quickstart --auth-choice skip --skip-providers --skip-skills --skip-daemon --skip-ui"
|
||||||
|
|
||||||
# Provide a minimal trash shim to avoid noisy "missing trash" logs in containers.
|
# Provide a minimal trash shim to avoid noisy "missing trash" logs in containers.
|
||||||
export PATH="/tmp/clawdbot-bin:$PATH"
|
export PATH="/tmp/clawdbot-bin:$PATH"
|
||||||
@@ -42,7 +43,7 @@ TRASH
|
|||||||
}
|
}
|
||||||
|
|
||||||
start_gateway() {
|
start_gateway() {
|
||||||
node dist/index.js gateway --port 18789 --bind loopback > /tmp/gateway-e2e.log 2>&1 &
|
node dist/index.js gateway --port 18789 --bind loopback --allow-unconfigured > /tmp/gateway-e2e.log 2>&1 &
|
||||||
GATEWAY_PID="$!"
|
GATEWAY_PID="$!"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,6 +72,7 @@ TRASH
|
|||||||
local command="$3"
|
local command="$3"
|
||||||
local send_fn="$4"
|
local send_fn="$4"
|
||||||
local with_gateway="${5:-false}"
|
local with_gateway="${5:-false}"
|
||||||
|
local validate_fn="${6:-}"
|
||||||
|
|
||||||
echo "== Wizard case: $case_name =="
|
echo "== Wizard case: $case_name =="
|
||||||
export HOME="$home_dir"
|
export HOME="$home_dir"
|
||||||
@@ -78,8 +80,9 @@ TRASH
|
|||||||
|
|
||||||
input_fifo="$(mktemp -u "/tmp/clawdbot-onboard-${case_name}.XXXXXX")"
|
input_fifo="$(mktemp -u "/tmp/clawdbot-onboard-${case_name}.XXXXXX")"
|
||||||
mkfifo "$input_fifo"
|
mkfifo "$input_fifo"
|
||||||
|
local log_path="/tmp/clawdbot-onboard-${case_name}.log"
|
||||||
# Run under script to keep an interactive TTY for clack prompts.
|
# Run under script to keep an interactive TTY for clack prompts.
|
||||||
script -q -c "$command" /dev/null < "$input_fifo" &
|
script -q -c "$command" "$log_path" < "$input_fifo" &
|
||||||
wizard_pid=$!
|
wizard_pid=$!
|
||||||
exec 3> "$input_fifo"
|
exec 3> "$input_fifo"
|
||||||
|
|
||||||
@@ -96,15 +99,19 @@ TRASH
|
|||||||
wait "$wizard_pid"
|
wait "$wizard_pid"
|
||||||
rm -f "$input_fifo"
|
rm -f "$input_fifo"
|
||||||
stop_gateway "$gw_pid"
|
stop_gateway "$gw_pid"
|
||||||
|
if [ -n "$validate_fn" ]; then
|
||||||
|
"$validate_fn" "$log_path"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
run_wizard() {
|
run_wizard() {
|
||||||
local case_name="$1"
|
local case_name="$1"
|
||||||
local home_dir="$2"
|
local home_dir="$2"
|
||||||
local send_fn="$3"
|
local send_fn="$3"
|
||||||
|
local validate_fn="${4:-}"
|
||||||
|
|
||||||
# Default onboarding command wrapper.
|
# Default onboarding command wrapper.
|
||||||
run_wizard_cmd "$case_name" "$home_dir" "node dist/index.js onboard" "$send_fn" true
|
run_wizard_cmd "$case_name" "$home_dir" "node dist/index.js onboard $ONBOARD_FLAGS" "$send_fn" true "$validate_fn"
|
||||||
}
|
}
|
||||||
|
|
||||||
make_home() {
|
make_home() {
|
||||||
@@ -129,23 +136,7 @@ TRASH
|
|||||||
|
|
||||||
send_local_basic() {
|
send_local_basic() {
|
||||||
# Choose local gateway, accept defaults, skip provider/skills/daemon, skip UI.
|
# Choose local gateway, accept defaults, skip provider/skills/daemon, skip UI.
|
||||||
send $'"'"'\r'"'"' 1.0
|
|
||||||
send $'"'"'\r'"'"' 1.0
|
|
||||||
send "" 1.2
|
|
||||||
send $'"'"'\e[B'"'"' 0.6
|
|
||||||
send $'"'"'\e[B'"'"' 0.6
|
|
||||||
send $'"'"'\e[B'"'"' 0.6
|
|
||||||
send $'"'"'\e[B'"'"' 0.6
|
|
||||||
send $'"'"'\e[B'"'"' 0.6
|
|
||||||
send $'"'"'\r'"'"' 0.6
|
|
||||||
send $'"'"'\r'"'"' 0.5
|
send $'"'"'\r'"'"' 0.5
|
||||||
send $'"'"'\r'"'"' 0.5
|
|
||||||
send $'"'"'\r'"'"' 0.5
|
|
||||||
send $'"'"'\r'"'"' 0.5
|
|
||||||
send $'"'"'n\r'"'"' 0.5
|
|
||||||
send $'"'"'n\r'"'"' 0.5
|
|
||||||
send $'"'"'n\r'"'"' 0.5
|
|
||||||
send $'"'"'n\r'"'"' 0.5
|
|
||||||
}
|
}
|
||||||
|
|
||||||
send_reset_config_only() {
|
send_reset_config_only() {
|
||||||
@@ -160,62 +151,31 @@ TRASH
|
|||||||
|
|
||||||
send_providers_flow() {
|
send_providers_flow() {
|
||||||
# Configure providers via configure wizard.
|
# Configure providers via configure wizard.
|
||||||
send "" 0.6
|
send $'"'"'\r'"'"' 1.0
|
||||||
|
send "" 1.5
|
||||||
|
# Provider mode (default Configure providers)
|
||||||
send $'"'"'\r'"'"' 0.8
|
send $'"'"'\r'"'"' 0.8
|
||||||
send "" 1.2
|
send "" 1.0
|
||||||
# Select Providers section only.
|
# Configure chat providers now? -> No
|
||||||
send $'"'"'\e[B'"'"' 0.5
|
send $'"'"'n\r'"'"' 0.6
|
||||||
send $'"'"'\e[B'"'"' 0.5
|
|
||||||
send $'"'"'\e[B'"'"' 0.5
|
|
||||||
send $'"'"'\e[B'"'"' 0.5
|
|
||||||
send $'"'"' '"'"' 0.4
|
|
||||||
send $'"'"'\r'"'"' 0.6
|
|
||||||
# Configure providers now? (default Yes)
|
|
||||||
send $'"'"'\r'"'"' 0.8
|
|
||||||
send "" 0.8
|
|
||||||
# Select Telegram, Discord, Slack.
|
|
||||||
send $'"'"'\e[B'"'"' 0.4
|
|
||||||
send $'"'"' '"'"' 0.4
|
|
||||||
send $'"'"'\e[B'"'"' 0.4
|
|
||||||
send $'"'"' '"'"' 0.4
|
|
||||||
send $'"'"'\e[B'"'"' 0.4
|
|
||||||
send $'"'"' '"'"' 0.4
|
|
||||||
send $'"'"'\r'"'"' 0.6
|
|
||||||
send $'"'"'tg_token\r'"'"' 0.8
|
|
||||||
send $'"'"'discord_token\r'"'"' 0.8
|
|
||||||
send "" 0.6
|
|
||||||
send $'"'"'\r'"'"' 0.6
|
|
||||||
send "" 0.6
|
|
||||||
send $'"'"'slack_bot\r'"'"' 0.8
|
|
||||||
send "" 0.6
|
|
||||||
send $'"'"'slack_app\r'"'"' 0.8
|
|
||||||
}
|
}
|
||||||
|
|
||||||
send_skills_flow() {
|
send_skills_flow() {
|
||||||
# Select skills section and skip optional installs.
|
# Select skills section and skip optional installs.
|
||||||
send "" 0.6
|
send $'"'"'\r'"'"' 1.0
|
||||||
send $'"'"'\r'"'"' 0.6
|
send "" 1.2
|
||||||
send "" 1.0
|
send $'"'"'n\r'"'"' 0.6
|
||||||
send $'"'"'\e[B'"'"' 0.4
|
|
||||||
send $'"'"'\e[B'"'"' 0.4
|
|
||||||
send $'"'"'\e[B'"'"' 0.4
|
|
||||||
send $'"'"'\e[B'"'"' 0.4
|
|
||||||
send $'"'"'\e[B'"'"' 0.4
|
|
||||||
send $'"'"' '"'"' 0.3
|
|
||||||
send $'"'"'\r'"'"' 0.4
|
|
||||||
send $'"'"'n\r'"'"' 0.4
|
|
||||||
send $'"'"'n\r'"'"' 0.4
|
|
||||||
}
|
}
|
||||||
|
|
||||||
run_case_local_basic() {
|
run_case_local_basic() {
|
||||||
local home_dir
|
local home_dir
|
||||||
home_dir="$(make_home local-basic)"
|
home_dir="$(make_home local-basic)"
|
||||||
run_wizard local-basic "$home_dir" send_local_basic
|
run_wizard local-basic "$home_dir" send_local_basic validate_local_basic_log
|
||||||
|
|
||||||
# Assert config + workspace scaffolding.
|
# Assert config + workspace scaffolding.
|
||||||
workspace_dir="$HOME/clawd"
|
workspace_dir="$HOME/clawd"
|
||||||
config_path="$HOME/.clawdbot/clawdbot.json"
|
config_path="$HOME/.clawdbot/clawdbot.json"
|
||||||
sessions_dir="$HOME/.clawdbot/sessions"
|
sessions_dir="$HOME/.clawdbot/agents/main/sessions"
|
||||||
|
|
||||||
assert_file "$config_path"
|
assert_file "$config_path"
|
||||||
assert_dir "$sessions_dir"
|
assert_dir "$sessions_dir"
|
||||||
@@ -383,7 +343,7 @@ NODE
|
|||||||
local home_dir
|
local home_dir
|
||||||
home_dir="$(make_home providers)"
|
home_dir="$(make_home providers)"
|
||||||
# Providers-only configure flow.
|
# Providers-only configure flow.
|
||||||
run_wizard_cmd providers "$home_dir" "node dist/index.js configure" send_providers_flow
|
run_wizard_cmd providers "$home_dir" "node dist/index.js configure --section providers" send_providers_flow
|
||||||
|
|
||||||
config_path="$HOME/.clawdbot/clawdbot.json"
|
config_path="$HOME/.clawdbot/clawdbot.json"
|
||||||
assert_file "$config_path"
|
assert_file "$config_path"
|
||||||
@@ -395,21 +355,22 @@ import JSON5 from "json5";
|
|||||||
const cfg = JSON5.parse(fs.readFileSync(process.env.CONFIG_PATH, "utf-8"));
|
const cfg = JSON5.parse(fs.readFileSync(process.env.CONFIG_PATH, "utf-8"));
|
||||||
const errors = [];
|
const errors = [];
|
||||||
|
|
||||||
if (cfg?.telegram?.botToken !== "tg_token") {
|
if (cfg?.telegram?.botToken) {
|
||||||
errors.push(`telegram.botToken mismatch (got ${cfg?.telegram?.botToken ?? "unset"})`);
|
errors.push(`telegram.botToken should be unset (got ${cfg?.telegram?.botToken})`);
|
||||||
}
|
}
|
||||||
if (cfg?.discord?.token !== "discord_token") {
|
if (cfg?.discord?.token) {
|
||||||
errors.push(`discord.token mismatch (got ${cfg?.discord?.token ?? "unset"})`);
|
errors.push(`discord.token should be unset (got ${cfg?.discord?.token})`);
|
||||||
}
|
}
|
||||||
if (cfg?.slack?.botToken !== "slack_bot") {
|
if (cfg?.slack?.botToken || cfg?.slack?.appToken) {
|
||||||
errors.push(`slack.botToken mismatch (got ${cfg?.slack?.botToken ?? "unset"})`);
|
errors.push(
|
||||||
}
|
`slack tokens should be unset (got bot=${cfg?.slack?.botToken ?? "unset"}, app=${cfg?.slack?.appToken ?? "unset"})`,
|
||||||
if (cfg?.slack?.appToken !== "slack_app") {
|
);
|
||||||
errors.push(`slack.appToken mismatch (got ${cfg?.slack?.appToken ?? "unset"})`);
|
}
|
||||||
}
|
if (cfg?.wizard?.lastRunCommand !== "configure") {
|
||||||
if (cfg?.wizard?.lastRunMode !== "local") {
|
errors.push(
|
||||||
errors.push(`wizard.lastRunMode mismatch (got ${cfg?.wizard?.lastRunMode ?? "unset"})`);
|
`wizard.lastRunCommand mismatch (got ${cfg?.wizard?.lastRunCommand ?? "unset"})`,
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
console.error(errors.join("\n"));
|
console.error(errors.join("\n"));
|
||||||
@@ -433,7 +394,7 @@ NODE
|
|||||||
}
|
}
|
||||||
JSON
|
JSON
|
||||||
|
|
||||||
run_wizard_cmd skills "$home_dir" "node dist/index.js configure" send_skills_flow
|
run_wizard_cmd skills "$home_dir" "node dist/index.js configure --section skills" send_skills_flow
|
||||||
|
|
||||||
config_path="$HOME/.clawdbot/clawdbot.json"
|
config_path="$HOME/.clawdbot/clawdbot.json"
|
||||||
assert_file "$config_path"
|
assert_file "$config_path"
|
||||||
@@ -462,6 +423,20 @@ if (errors.length > 0) {
|
|||||||
NODE
|
NODE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert_log_not_contains() {
|
||||||
|
local file_path="$1"
|
||||||
|
local needle="$2"
|
||||||
|
if grep -q "$needle" "$file_path"; then
|
||||||
|
echo "Unexpected log output: $needle"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_local_basic_log() {
|
||||||
|
local log_path="$1"
|
||||||
|
assert_log_not_contains "$log_path" "systemctl --user unavailable"
|
||||||
|
}
|
||||||
|
|
||||||
run_case_local_basic
|
run_case_local_basic
|
||||||
run_case_remote_non_interactive
|
run_case_remote_non_interactive
|
||||||
run_case_reset
|
run_case_reset
|
||||||
|
|||||||
@@ -241,6 +241,7 @@ export function buildProgram() {
|
|||||||
)
|
)
|
||||||
.option("--workspace <dir>", "Agent workspace directory (default: ~/clawd)")
|
.option("--workspace <dir>", "Agent workspace directory (default: ~/clawd)")
|
||||||
.option("--non-interactive", "Run without prompts", false)
|
.option("--non-interactive", "Run without prompts", false)
|
||||||
|
.option("--flow <flow>", "Wizard flow: quickstart|advanced")
|
||||||
.option("--mode <mode>", "Wizard mode: local|remote")
|
.option("--mode <mode>", "Wizard mode: local|remote")
|
||||||
.option(
|
.option(
|
||||||
"--auth-choice <choice>",
|
"--auth-choice <choice>",
|
||||||
@@ -276,17 +277,30 @@ export function buildProgram() {
|
|||||||
.option("--tailscale <mode>", "Tailscale: off|serve|funnel")
|
.option("--tailscale <mode>", "Tailscale: off|serve|funnel")
|
||||||
.option("--tailscale-reset-on-exit", "Reset tailscale serve/funnel on exit")
|
.option("--tailscale-reset-on-exit", "Reset tailscale serve/funnel on exit")
|
||||||
.option("--install-daemon", "Install gateway daemon")
|
.option("--install-daemon", "Install gateway daemon")
|
||||||
|
.option("--no-install-daemon", "Skip gateway daemon install")
|
||||||
|
.option("--skip-daemon", "Skip gateway daemon install")
|
||||||
.option("--daemon-runtime <runtime>", "Daemon runtime: node|bun")
|
.option("--daemon-runtime <runtime>", "Daemon runtime: node|bun")
|
||||||
|
.option("--skip-providers", "Skip provider setup")
|
||||||
.option("--skip-skills", "Skip skills setup")
|
.option("--skip-skills", "Skip skills setup")
|
||||||
.option("--skip-health", "Skip health check")
|
.option("--skip-health", "Skip health check")
|
||||||
|
.option("--skip-ui", "Skip Control UI/TUI prompts")
|
||||||
.option("--node-manager <name>", "Node manager for skills: npm|pnpm|bun")
|
.option("--node-manager <name>", "Node manager for skills: npm|pnpm|bun")
|
||||||
.option("--json", "Output JSON summary", false)
|
.option("--json", "Output JSON summary", false)
|
||||||
.action(async (opts) => {
|
.action(async (opts, command) => {
|
||||||
try {
|
try {
|
||||||
|
const installDaemon =
|
||||||
|
typeof command?.getOptionValueSource === "function"
|
||||||
|
? command.getOptionValueSource("skipDaemon") === "cli"
|
||||||
|
? false
|
||||||
|
: command.getOptionValueSource("installDaemon") === "cli"
|
||||||
|
? Boolean(opts.installDaemon)
|
||||||
|
: undefined
|
||||||
|
: undefined;
|
||||||
await onboardCommand(
|
await onboardCommand(
|
||||||
{
|
{
|
||||||
workspace: opts.workspace as string | undefined,
|
workspace: opts.workspace as string | undefined,
|
||||||
nonInteractive: Boolean(opts.nonInteractive),
|
nonInteractive: Boolean(opts.nonInteractive),
|
||||||
|
flow: opts.flow as "quickstart" | "advanced" | undefined,
|
||||||
mode: opts.mode as "local" | "remote" | undefined,
|
mode: opts.mode as "local" | "remote" | undefined,
|
||||||
authChoice: opts.authChoice as
|
authChoice: opts.authChoice as
|
||||||
| "oauth"
|
| "oauth"
|
||||||
@@ -332,10 +346,12 @@ export function buildProgram() {
|
|||||||
remoteToken: opts.remoteToken as string | undefined,
|
remoteToken: opts.remoteToken as string | undefined,
|
||||||
tailscale: opts.tailscale as "off" | "serve" | "funnel" | undefined,
|
tailscale: opts.tailscale as "off" | "serve" | "funnel" | undefined,
|
||||||
tailscaleResetOnExit: Boolean(opts.tailscaleResetOnExit),
|
tailscaleResetOnExit: Boolean(opts.tailscaleResetOnExit),
|
||||||
installDaemon: Boolean(opts.installDaemon),
|
installDaemon,
|
||||||
daemonRuntime: opts.daemonRuntime as "node" | "bun" | undefined,
|
daemonRuntime: opts.daemonRuntime as "node" | "bun" | undefined,
|
||||||
|
skipProviders: Boolean(opts.skipProviders),
|
||||||
skipSkills: Boolean(opts.skipSkills),
|
skipSkills: Boolean(opts.skipSkills),
|
||||||
skipHealth: Boolean(opts.skipHealth),
|
skipHealth: Boolean(opts.skipHealth),
|
||||||
|
skipUi: Boolean(opts.skipUi),
|
||||||
nodeManager: opts.nodeManager as "npm" | "pnpm" | "bun" | undefined,
|
nodeManager: opts.nodeManager as "npm" | "pnpm" | "bun" | undefined,
|
||||||
json: Boolean(opts.json),
|
json: Boolean(opts.json),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { resolveGatewayProgramArguments } from "../daemon/program-args.js";
|
|||||||
import { resolvePreferredNodePath } from "../daemon/runtime-paths.js";
|
import { resolvePreferredNodePath } from "../daemon/runtime-paths.js";
|
||||||
import { resolveGatewayService } from "../daemon/service.js";
|
import { resolveGatewayService } from "../daemon/service.js";
|
||||||
import { buildServiceEnvironment } from "../daemon/service-env.js";
|
import { buildServiceEnvironment } from "../daemon/service-env.js";
|
||||||
|
import { isSystemdUserServiceAvailable } from "../daemon/systemd.js";
|
||||||
import { upsertSharedEnvVar } from "../infra/env-file.js";
|
import { upsertSharedEnvVar } from "../infra/env-file.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
import { defaultRuntime } from "../runtime.js";
|
import { defaultRuntime } from "../runtime.js";
|
||||||
@@ -429,41 +430,53 @@ export async function runNonInteractiveOnboarding(
|
|||||||
const daemonRuntimeRaw = opts.daemonRuntime ?? DEFAULT_GATEWAY_DAEMON_RUNTIME;
|
const daemonRuntimeRaw = opts.daemonRuntime ?? DEFAULT_GATEWAY_DAEMON_RUNTIME;
|
||||||
|
|
||||||
if (opts.installDaemon) {
|
if (opts.installDaemon) {
|
||||||
if (!isGatewayDaemonRuntime(daemonRuntimeRaw)) {
|
const systemdAvailable =
|
||||||
runtime.error("Invalid --daemon-runtime (use node or bun)");
|
process.platform === "linux"
|
||||||
runtime.exit(1);
|
? await isSystemdUserServiceAvailable()
|
||||||
return;
|
: true;
|
||||||
}
|
if (process.platform === "linux" && !systemdAvailable) {
|
||||||
const service = resolveGatewayService();
|
runtime.log(
|
||||||
const devMode =
|
"Systemd user services are unavailable; skipping daemon install.",
|
||||||
process.argv[1]?.includes(`${path.sep}src${path.sep}`) &&
|
);
|
||||||
process.argv[1]?.endsWith(".ts");
|
} else {
|
||||||
const nodePath = await resolvePreferredNodePath({
|
if (!isGatewayDaemonRuntime(daemonRuntimeRaw)) {
|
||||||
env: process.env,
|
runtime.error("Invalid --daemon-runtime (use node or bun)");
|
||||||
runtime: daemonRuntimeRaw,
|
runtime.exit(1);
|
||||||
});
|
return;
|
||||||
const { programArguments, workingDirectory } =
|
}
|
||||||
await resolveGatewayProgramArguments({
|
const service = resolveGatewayService();
|
||||||
port,
|
const devMode =
|
||||||
dev: devMode,
|
process.argv[1]?.includes(`${path.sep}src${path.sep}`) &&
|
||||||
|
process.argv[1]?.endsWith(".ts");
|
||||||
|
const nodePath = await resolvePreferredNodePath({
|
||||||
|
env: process.env,
|
||||||
runtime: daemonRuntimeRaw,
|
runtime: daemonRuntimeRaw,
|
||||||
nodePath,
|
|
||||||
});
|
});
|
||||||
const environment = buildServiceEnvironment({
|
const { programArguments, workingDirectory } =
|
||||||
env: process.env,
|
await resolveGatewayProgramArguments({
|
||||||
port,
|
port,
|
||||||
token: gatewayToken,
|
dev: devMode,
|
||||||
launchdLabel:
|
runtime: daemonRuntimeRaw,
|
||||||
process.platform === "darwin" ? GATEWAY_LAUNCH_AGENT_LABEL : undefined,
|
nodePath,
|
||||||
});
|
});
|
||||||
await service.install({
|
const environment = buildServiceEnvironment({
|
||||||
env: process.env,
|
env: process.env,
|
||||||
stdout: process.stdout,
|
port,
|
||||||
programArguments,
|
token: gatewayToken,
|
||||||
workingDirectory,
|
launchdLabel:
|
||||||
environment,
|
process.platform === "darwin"
|
||||||
});
|
? GATEWAY_LAUNCH_AGENT_LABEL
|
||||||
await ensureSystemdUserLingerNonInteractive({ runtime });
|
: undefined,
|
||||||
|
});
|
||||||
|
await service.install({
|
||||||
|
env: process.env,
|
||||||
|
stdout: process.stdout,
|
||||||
|
programArguments,
|
||||||
|
workingDirectory,
|
||||||
|
environment,
|
||||||
|
});
|
||||||
|
await ensureSystemdUserLingerNonInteractive({ runtime });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!opts.skipHealth) {
|
if (!opts.skipHealth) {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export type ProviderChoice = ChatProviderId;
|
|||||||
|
|
||||||
export type OnboardOptions = {
|
export type OnboardOptions = {
|
||||||
mode?: OnboardMode;
|
mode?: OnboardMode;
|
||||||
|
flow?: "quickstart" | "advanced";
|
||||||
workspace?: string;
|
workspace?: string;
|
||||||
nonInteractive?: boolean;
|
nonInteractive?: boolean;
|
||||||
authChoice?: AuthChoice;
|
authChoice?: AuthChoice;
|
||||||
@@ -51,8 +52,10 @@ export type OnboardOptions = {
|
|||||||
tailscaleResetOnExit?: boolean;
|
tailscaleResetOnExit?: boolean;
|
||||||
installDaemon?: boolean;
|
installDaemon?: boolean;
|
||||||
daemonRuntime?: GatewayDaemonRuntime;
|
daemonRuntime?: GatewayDaemonRuntime;
|
||||||
|
skipProviders?: boolean;
|
||||||
skipSkills?: boolean;
|
skipSkills?: boolean;
|
||||||
skipHealth?: boolean;
|
skipHealth?: boolean;
|
||||||
|
skipUi?: boolean;
|
||||||
nodeManager?: NodeManagerChoice;
|
nodeManager?: NodeManagerChoice;
|
||||||
remoteUrl?: string;
|
remoteUrl?: string;
|
||||||
remoteToken?: string;
|
remoteToken?: string;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { note as clackNote } from "@clack/prompts";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
enableSystemdUserLinger,
|
enableSystemdUserLinger,
|
||||||
|
isSystemdUserServiceAvailable,
|
||||||
readSystemdUserLingerStatus,
|
readSystemdUserLingerStatus,
|
||||||
} from "../daemon/systemd.js";
|
} from "../daemon/systemd.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
@@ -32,6 +33,13 @@ export async function ensureSystemdUserLingerInteractive(params: {
|
|||||||
const env = params.env ?? process.env;
|
const env = params.env ?? process.env;
|
||||||
const prompter = params.prompter ?? { note };
|
const prompter = params.prompter ?? { note };
|
||||||
const title = params.title ?? "Systemd";
|
const title = params.title ?? "Systemd";
|
||||||
|
if (!(await isSystemdUserServiceAvailable())) {
|
||||||
|
await prompter.note(
|
||||||
|
"Systemd user services are unavailable. Skipping lingering checks.",
|
||||||
|
title,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const status = await readSystemdUserLingerStatus(env);
|
const status = await readSystemdUserLingerStatus(env);
|
||||||
if (!status) {
|
if (!status) {
|
||||||
await prompter.note(
|
await prompter.note(
|
||||||
@@ -98,6 +106,7 @@ export async function ensureSystemdUserLingerNonInteractive(params: {
|
|||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
if (process.platform !== "linux") return;
|
if (process.platform !== "linux") return;
|
||||||
const env = params.env ?? process.env;
|
const env = params.env ?? process.env;
|
||||||
|
if (!(await isSystemdUserServiceAvailable())) return;
|
||||||
const status = await readSystemdUserLingerStatus(env);
|
const status = await readSystemdUserLingerStatus(env);
|
||||||
if (!status || status.linger === "yes") return;
|
if (!status || status.linger === "yes") return;
|
||||||
|
|
||||||
|
|||||||
35
src/daemon/systemd-availability.test.ts
Normal file
35
src/daemon/systemd-availability.test.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const execFileMock = vi.hoisted(() => vi.fn());
|
||||||
|
|
||||||
|
vi.mock("node:child_process", () => ({
|
||||||
|
execFile: execFileMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { isSystemdUserServiceAvailable } from "./systemd.js";
|
||||||
|
|
||||||
|
describe("systemd availability", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
execFileMock.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns true when systemctl --user succeeds", async () => {
|
||||||
|
execFileMock.mockImplementation((_cmd, _args, _opts, cb) => {
|
||||||
|
cb(null, "", "");
|
||||||
|
});
|
||||||
|
await expect(isSystemdUserServiceAvailable()).resolves.toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false when systemd user bus is unavailable", async () => {
|
||||||
|
execFileMock.mockImplementation((_cmd, _args, _opts, cb) => {
|
||||||
|
const err = new Error("Failed to connect to bus") as Error & {
|
||||||
|
stderr?: string;
|
||||||
|
code?: number;
|
||||||
|
};
|
||||||
|
err.stderr = "Failed to connect to bus";
|
||||||
|
err.code = 1;
|
||||||
|
cb(err, "", "");
|
||||||
|
});
|
||||||
|
await expect(isSystemdUserServiceAvailable()).resolves.toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -337,6 +337,19 @@ async function execSystemctl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function isSystemdUserServiceAvailable(): Promise<boolean> {
|
||||||
|
const res = await execSystemctl(["--user", "status"]);
|
||||||
|
if (res.code === 0) return true;
|
||||||
|
const detail = `${res.stderr} ${res.stdout}`.toLowerCase();
|
||||||
|
if (!detail) return false;
|
||||||
|
if (detail.includes("not found")) return false;
|
||||||
|
if (detail.includes("failed to connect")) return false;
|
||||||
|
if (detail.includes("not been booted")) return false;
|
||||||
|
if (detail.includes("no such file or directory")) return false;
|
||||||
|
if (detail.includes("not supported")) return false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
async function assertSystemdAvailable() {
|
async function assertSystemdAvailable() {
|
||||||
const res = await execSystemctl(["--user", "status"]);
|
const res = await execSystemctl(["--user", "status"]);
|
||||||
if (res.code === 0) return;
|
if (res.code === 0) return;
|
||||||
|
|||||||
120
src/wizard/onboarding.test.ts
Normal file
120
src/wizard/onboarding.test.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
|
import { runOnboardingWizard } from "./onboarding.js";
|
||||||
|
import type { WizardPrompter } from "./prompts.js";
|
||||||
|
|
||||||
|
const setupProviders = vi.hoisted(() => vi.fn(async (cfg) => cfg));
|
||||||
|
const setupSkills = vi.hoisted(() => vi.fn(async (cfg) => cfg));
|
||||||
|
const healthCommand = vi.hoisted(() => vi.fn(async () => {}));
|
||||||
|
const ensureWorkspaceAndSessions = vi.hoisted(() => vi.fn(async () => {}));
|
||||||
|
const writeConfigFile = vi.hoisted(() => vi.fn(async () => {}));
|
||||||
|
const readConfigFileSnapshot = vi.hoisted(() =>
|
||||||
|
vi.fn(async () => ({ exists: false, valid: true, config: {} })),
|
||||||
|
);
|
||||||
|
const ensureSystemdUserLingerInteractive = vi.hoisted(() =>
|
||||||
|
vi.fn(async () => {}),
|
||||||
|
);
|
||||||
|
const isSystemdUserServiceAvailable = vi.hoisted(() => vi.fn(async () => true));
|
||||||
|
const ensureControlUiAssetsBuilt = vi.hoisted(() =>
|
||||||
|
vi.fn(async () => ({ ok: true })),
|
||||||
|
);
|
||||||
|
const runTui = vi.hoisted(() => vi.fn(async () => {}));
|
||||||
|
|
||||||
|
vi.mock("../commands/onboard-providers.js", () => ({
|
||||||
|
setupProviders,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../commands/onboard-skills.js", () => ({
|
||||||
|
setupSkills,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../commands/health.js", () => ({
|
||||||
|
healthCommand,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../config/config.js", async (importActual) => {
|
||||||
|
const actual = await importActual<typeof import("../config/config.js")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
readConfigFileSnapshot,
|
||||||
|
writeConfigFile,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("../commands/onboard-helpers.js", async (importActual) => {
|
||||||
|
const actual =
|
||||||
|
await importActual<typeof import("../commands/onboard-helpers.js")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
ensureWorkspaceAndSessions,
|
||||||
|
detectBrowserOpenSupport: vi.fn(async () => ({ ok: false })),
|
||||||
|
openUrl: vi.fn(async () => true),
|
||||||
|
printWizardHeader: vi.fn(),
|
||||||
|
probeGatewayReachable: vi.fn(async () => ({ ok: true })),
|
||||||
|
resolveControlUiLinks: vi.fn(() => ({
|
||||||
|
httpUrl: "http://127.0.0.1:18789",
|
||||||
|
wsUrl: "ws://127.0.0.1:18789",
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("../commands/systemd-linger.js", () => ({
|
||||||
|
ensureSystemdUserLingerInteractive,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../daemon/systemd.js", () => ({
|
||||||
|
isSystemdUserServiceAvailable,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../infra/control-ui-assets.js", () => ({
|
||||||
|
ensureControlUiAssetsBuilt,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../tui/tui.js", () => ({
|
||||||
|
runTui,
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("runOnboardingWizard", () => {
|
||||||
|
it("skips prompts and setup steps when flags are set", async () => {
|
||||||
|
const select: WizardPrompter["select"] = vi.fn(async () => "quickstart");
|
||||||
|
const multiselect: WizardPrompter["multiselect"] = vi.fn(async () => []);
|
||||||
|
const prompter: WizardPrompter = {
|
||||||
|
intro: vi.fn(async () => {}),
|
||||||
|
outro: vi.fn(async () => {}),
|
||||||
|
note: vi.fn(async () => {}),
|
||||||
|
select,
|
||||||
|
multiselect,
|
||||||
|
text: vi.fn(async () => ""),
|
||||||
|
confirm: vi.fn(async () => false),
|
||||||
|
progress: vi.fn(() => ({ update: vi.fn(), stop: vi.fn() })),
|
||||||
|
};
|
||||||
|
const runtime: RuntimeEnv = {
|
||||||
|
log: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
exit: vi.fn((code: number) => {
|
||||||
|
throw new Error(`exit:${code}`);
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
await runOnboardingWizard(
|
||||||
|
{
|
||||||
|
flow: "quickstart",
|
||||||
|
authChoice: "skip",
|
||||||
|
installDaemon: false,
|
||||||
|
skipProviders: true,
|
||||||
|
skipSkills: true,
|
||||||
|
skipHealth: true,
|
||||||
|
skipUi: true,
|
||||||
|
},
|
||||||
|
runtime,
|
||||||
|
prompter,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(select).not.toHaveBeenCalled();
|
||||||
|
expect(setupProviders).not.toHaveBeenCalled();
|
||||||
|
expect(setupSkills).not.toHaveBeenCalled();
|
||||||
|
expect(healthCommand).not.toHaveBeenCalled();
|
||||||
|
expect(runTui).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -51,6 +51,7 @@ import { resolveGatewayProgramArguments } from "../daemon/program-args.js";
|
|||||||
import { resolvePreferredNodePath } from "../daemon/runtime-paths.js";
|
import { resolvePreferredNodePath } from "../daemon/runtime-paths.js";
|
||||||
import { resolveGatewayService } from "../daemon/service.js";
|
import { resolveGatewayService } from "../daemon/service.js";
|
||||||
import { buildServiceEnvironment } from "../daemon/service-env.js";
|
import { buildServiceEnvironment } from "../daemon/service-env.js";
|
||||||
|
import { isSystemdUserServiceAvailable } from "../daemon/systemd.js";
|
||||||
import { ensureControlUiAssetsBuilt } from "../infra/control-ui-assets.js";
|
import { ensureControlUiAssetsBuilt } from "../infra/control-ui-assets.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
import { defaultRuntime } from "../runtime.js";
|
import { defaultRuntime } from "../runtime.js";
|
||||||
@@ -120,14 +121,26 @@ export async function runOnboardingWizard(
|
|||||||
|
|
||||||
const quickstartHint = "Configure details later via clawdbot configure.";
|
const quickstartHint = "Configure details later via clawdbot configure.";
|
||||||
const advancedHint = "Configure port, network, Tailscale, and auth options.";
|
const advancedHint = "Configure port, network, Tailscale, and auth options.";
|
||||||
let flow = (await prompter.select({
|
const explicitFlow = opts.flow?.trim();
|
||||||
message: "Onboarding mode",
|
if (
|
||||||
options: [
|
explicitFlow &&
|
||||||
{ value: "quickstart", label: "QuickStart", hint: quickstartHint },
|
explicitFlow !== "quickstart" &&
|
||||||
{ value: "advanced", label: "Advanced", hint: advancedHint },
|
explicitFlow !== "advanced"
|
||||||
],
|
) {
|
||||||
initialValue: "quickstart",
|
runtime.error("Invalid --flow (use quickstart or advanced).");
|
||||||
})) as "quickstart" | "advanced";
|
runtime.exit(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let flow =
|
||||||
|
explicitFlow ??
|
||||||
|
((await prompter.select({
|
||||||
|
message: "Onboarding mode",
|
||||||
|
options: [
|
||||||
|
{ value: "quickstart", label: "QuickStart", hint: quickstartHint },
|
||||||
|
{ value: "advanced", label: "Advanced", hint: advancedHint },
|
||||||
|
],
|
||||||
|
initialValue: "quickstart",
|
||||||
|
})) as "quickstart" | "advanced");
|
||||||
|
|
||||||
if (opts.mode === "remote" && flow === "quickstart") {
|
if (opts.mode === "remote" && flow === "quickstart") {
|
||||||
await prompter.note(
|
await prompter.note(
|
||||||
@@ -309,14 +322,16 @@ export async function runOnboardingWizard(
|
|||||||
const authStore = ensureAuthProfileStore(undefined, {
|
const authStore = ensureAuthProfileStore(undefined, {
|
||||||
allowKeychainPrompt: false,
|
allowKeychainPrompt: false,
|
||||||
});
|
});
|
||||||
const authChoice = (await prompter.select({
|
const authChoice =
|
||||||
message: "Model/auth choice",
|
opts.authChoice ??
|
||||||
options: buildAuthChoiceOptions({
|
((await prompter.select({
|
||||||
store: authStore,
|
message: "Model/auth choice",
|
||||||
includeSkip: true,
|
options: buildAuthChoiceOptions({
|
||||||
includeClaudeCliIfMissing: true,
|
store: authStore,
|
||||||
}),
|
includeSkip: true,
|
||||||
})) as AuthChoice;
|
includeClaudeCliIfMissing: true,
|
||||||
|
}),
|
||||||
|
})) as AuthChoice);
|
||||||
|
|
||||||
const authResult = await applyAuthChoice({
|
const authResult = await applyAuthChoice({
|
||||||
authChoice,
|
authChoice,
|
||||||
@@ -501,14 +516,18 @@ export async function runOnboardingWizard(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
nextConfig = await setupProviders(nextConfig, runtime, prompter, {
|
if (opts.skipProviders) {
|
||||||
allowSignalInstall: true,
|
await prompter.note("Skipping provider setup.", "Providers");
|
||||||
forceAllowFromProviders:
|
} else {
|
||||||
flow === "quickstart" ? ["telegram", "whatsapp"] : [],
|
nextConfig = await setupProviders(nextConfig, runtime, prompter, {
|
||||||
skipDmPolicyPrompt: flow === "quickstart",
|
allowSignalInstall: true,
|
||||||
skipConfirm: flow === "quickstart",
|
forceAllowFromProviders:
|
||||||
quickstartDefaults: flow === "quickstart",
|
flow === "quickstart" ? ["telegram", "whatsapp"] : [],
|
||||||
});
|
skipDmPolicyPrompt: flow === "quickstart",
|
||||||
|
skipConfirm: flow === "quickstart",
|
||||||
|
quickstartDefaults: flow === "quickstart",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await writeConfigFile(nextConfig);
|
await writeConfigFile(nextConfig);
|
||||||
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
|
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
|
||||||
@@ -516,28 +535,59 @@ export async function runOnboardingWizard(
|
|||||||
skipBootstrap: Boolean(nextConfig.agents?.defaults?.skipBootstrap),
|
skipBootstrap: Boolean(nextConfig.agents?.defaults?.skipBootstrap),
|
||||||
});
|
});
|
||||||
|
|
||||||
nextConfig = await setupSkills(nextConfig, workspaceDir, runtime, prompter);
|
if (opts.skipSkills) {
|
||||||
|
await prompter.note("Skipping skills setup.", "Skills");
|
||||||
|
} else {
|
||||||
|
nextConfig = await setupSkills(nextConfig, workspaceDir, runtime, prompter);
|
||||||
|
}
|
||||||
nextConfig = applyWizardMetadata(nextConfig, { command: "onboard", mode });
|
nextConfig = applyWizardMetadata(nextConfig, { command: "onboard", mode });
|
||||||
await writeConfigFile(nextConfig);
|
await writeConfigFile(nextConfig);
|
||||||
|
|
||||||
await ensureSystemdUserLingerInteractive({
|
const systemdAvailable =
|
||||||
runtime,
|
process.platform === "linux" ? await isSystemdUserServiceAvailable() : true;
|
||||||
prompter: {
|
if (process.platform === "linux" && !systemdAvailable) {
|
||||||
confirm: prompter.confirm,
|
await prompter.note(
|
||||||
note: prompter.note,
|
"Systemd user services are unavailable. Skipping lingering checks and daemon install.",
|
||||||
},
|
"Systemd",
|
||||||
reason:
|
);
|
||||||
"Linux installs use a systemd user service by default. Without lingering, systemd stops the user session on logout/idle and kills the Gateway.",
|
}
|
||||||
requireConfirm: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const installDaemon =
|
if (process.platform === "linux" && systemdAvailable) {
|
||||||
flow === "quickstart"
|
await ensureSystemdUserLingerInteractive({
|
||||||
? true
|
runtime,
|
||||||
: await prompter.confirm({
|
prompter: {
|
||||||
message: "Install Gateway daemon (recommended)",
|
confirm: prompter.confirm,
|
||||||
initialValue: true,
|
note: prompter.note,
|
||||||
});
|
},
|
||||||
|
reason:
|
||||||
|
"Linux installs use a systemd user service by default. Without lingering, systemd stops the user session on logout/idle and kills the Gateway.",
|
||||||
|
requireConfirm: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const explicitInstallDaemon =
|
||||||
|
typeof opts.installDaemon === "boolean" ? opts.installDaemon : undefined;
|
||||||
|
let installDaemon: boolean;
|
||||||
|
if (explicitInstallDaemon !== undefined) {
|
||||||
|
installDaemon = explicitInstallDaemon;
|
||||||
|
} else if (process.platform === "linux" && !systemdAvailable) {
|
||||||
|
installDaemon = false;
|
||||||
|
} else if (flow === "quickstart") {
|
||||||
|
installDaemon = true;
|
||||||
|
} else {
|
||||||
|
installDaemon = await prompter.confirm({
|
||||||
|
message: "Install Gateway daemon (recommended)",
|
||||||
|
initialValue: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === "linux" && !systemdAvailable && installDaemon) {
|
||||||
|
await prompter.note(
|
||||||
|
"Systemd user services are unavailable; skipping daemon install. Use your container supervisor or `docker compose up -d`.",
|
||||||
|
"Gateway daemon",
|
||||||
|
);
|
||||||
|
installDaemon = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (installDaemon) {
|
if (installDaemon) {
|
||||||
const daemonRuntime =
|
const daemonRuntime =
|
||||||
@@ -609,19 +659,21 @@ export async function runOnboardingWizard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await sleep(1500);
|
if (!opts.skipHealth) {
|
||||||
try {
|
await sleep(1500);
|
||||||
await healthCommand({ json: false, timeoutMs: 10_000 }, runtime);
|
try {
|
||||||
} catch (err) {
|
await healthCommand({ json: false, timeoutMs: 10_000 }, runtime);
|
||||||
runtime.error(`Health check failed: ${String(err)}`);
|
} catch (err) {
|
||||||
await prompter.note(
|
runtime.error(`Health check failed: ${String(err)}`);
|
||||||
[
|
await prompter.note(
|
||||||
"Docs:",
|
[
|
||||||
"https://docs.clawd.bot/gateway/health",
|
"Docs:",
|
||||||
"https://docs.clawd.bot/gateway/troubleshooting",
|
"https://docs.clawd.bot/gateway/health",
|
||||||
].join("\n"),
|
"https://docs.clawd.bot/gateway/troubleshooting",
|
||||||
"Health check help",
|
].join("\n"),
|
||||||
);
|
"Health check help",
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const controlUiAssets = await ensureControlUiAssetsBuilt(runtime);
|
const controlUiAssets = await ensureControlUiAssetsBuilt(runtime);
|
||||||
@@ -676,7 +728,7 @@ export async function runOnboardingWizard(
|
|||||||
"Control UI",
|
"Control UI",
|
||||||
);
|
);
|
||||||
|
|
||||||
if (gatewayProbe.ok) {
|
if (!opts.skipUi && gatewayProbe.ok) {
|
||||||
if (hasBootstrap) {
|
if (hasBootstrap) {
|
||||||
await prompter.note(
|
await prompter.note(
|
||||||
[
|
[
|
||||||
@@ -731,6 +783,8 @@ export async function runOnboardingWizard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (opts.skipUi) {
|
||||||
|
await prompter.note("Skipping Control UI/TUI prompts.", "Control UI");
|
||||||
}
|
}
|
||||||
|
|
||||||
await prompter.note(
|
await prompter.note(
|
||||||
|
|||||||
Reference in New Issue
Block a user