mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 14:38:25 +00:00
refactor(commands): split CLI commands
This commit is contained in:
470
src/commands/configure.wizard.ts
Normal file
470
src/commands/configure.wizard.ts
Normal file
@@ -0,0 +1,470 @@
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import {
|
||||
CONFIG_PATH_CLAWDBOT,
|
||||
readConfigFileSnapshot,
|
||||
resolveGatewayPort,
|
||||
writeConfigFile,
|
||||
} from "../config/config.js";
|
||||
import { ensureControlUiAssetsBuilt } from "../infra/control-ui-assets.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { note } from "../terminal/note.js";
|
||||
import { resolveUserPath, sleep } from "../utils.js";
|
||||
import { createClackPrompter } from "../wizard/clack-prompter.js";
|
||||
import { WizardCancelledError } from "../wizard/prompts.js";
|
||||
import { removeChannelConfigWizard } from "./configure.channels.js";
|
||||
import { maybeInstallDaemon } from "./configure.daemon.js";
|
||||
import { promptGatewayConfig } from "./configure.gateway.js";
|
||||
import { promptAuthConfig } from "./configure.gateway-auth.js";
|
||||
import type {
|
||||
ChannelsWizardMode,
|
||||
ConfigureWizardParams,
|
||||
WizardSection,
|
||||
} from "./configure.shared.js";
|
||||
import {
|
||||
CONFIGURE_SECTION_OPTIONS,
|
||||
intro,
|
||||
outro,
|
||||
select,
|
||||
text,
|
||||
} from "./configure.shared.js";
|
||||
import { healthCommand } from "./health.js";
|
||||
import { formatHealthCheckFailure } from "./health-format.js";
|
||||
import { setupChannels } from "./onboard-channels.js";
|
||||
import {
|
||||
applyWizardMetadata,
|
||||
DEFAULT_WORKSPACE,
|
||||
ensureWorkspaceAndSessions,
|
||||
guardCancel,
|
||||
printWizardHeader,
|
||||
probeGatewayReachable,
|
||||
resolveControlUiLinks,
|
||||
summarizeExistingConfig,
|
||||
} from "./onboard-helpers.js";
|
||||
import { promptRemoteGatewayConfig } from "./onboard-remote.js";
|
||||
import { setupSkills } from "./onboard-skills.js";
|
||||
|
||||
type ConfigureSectionChoice = WizardSection | "__continue";
|
||||
|
||||
async function promptConfigureSection(
|
||||
runtime: RuntimeEnv,
|
||||
hasSelection: boolean,
|
||||
): Promise<ConfigureSectionChoice> {
|
||||
return guardCancel(
|
||||
await select<ConfigureSectionChoice>({
|
||||
message: "Select sections to configure",
|
||||
options: [
|
||||
...CONFIGURE_SECTION_OPTIONS,
|
||||
{
|
||||
value: "__continue",
|
||||
label: "Continue",
|
||||
hint: hasSelection ? "Done" : "Skip for now",
|
||||
},
|
||||
],
|
||||
initialValue: CONFIGURE_SECTION_OPTIONS[0]?.value,
|
||||
}),
|
||||
runtime,
|
||||
);
|
||||
}
|
||||
|
||||
async function promptChannelMode(
|
||||
runtime: RuntimeEnv,
|
||||
): Promise<ChannelsWizardMode> {
|
||||
return guardCancel(
|
||||
await select({
|
||||
message: "Channels",
|
||||
options: [
|
||||
{
|
||||
value: "configure",
|
||||
label: "Configure/link",
|
||||
hint: "Add/update channels; disable unselected accounts",
|
||||
},
|
||||
{
|
||||
value: "remove",
|
||||
label: "Remove channel config",
|
||||
hint: "Delete channel tokens/settings from clawdbot.json",
|
||||
},
|
||||
],
|
||||
initialValue: "configure",
|
||||
}),
|
||||
runtime,
|
||||
) as ChannelsWizardMode;
|
||||
}
|
||||
|
||||
export async function runConfigureWizard(
|
||||
opts: ConfigureWizardParams,
|
||||
runtime: RuntimeEnv = defaultRuntime,
|
||||
) {
|
||||
try {
|
||||
printWizardHeader(runtime);
|
||||
intro(
|
||||
opts.command === "update"
|
||||
? "Clawdbot update wizard"
|
||||
: "Clawdbot configure",
|
||||
);
|
||||
const prompter = createClackPrompter();
|
||||
|
||||
const snapshot = await readConfigFileSnapshot();
|
||||
const baseConfig: ClawdbotConfig = snapshot.valid ? snapshot.config : {};
|
||||
|
||||
if (snapshot.exists) {
|
||||
const title = snapshot.valid
|
||||
? "Existing config detected"
|
||||
: "Invalid config";
|
||||
note(summarizeExistingConfig(baseConfig), title);
|
||||
if (!snapshot.valid && snapshot.issues.length > 0) {
|
||||
note(
|
||||
[
|
||||
...snapshot.issues.map((iss) => `- ${iss.path}: ${iss.message}`),
|
||||
"",
|
||||
"Docs: https://docs.clawd.bot/gateway/configuration",
|
||||
].join("\n"),
|
||||
"Config issues",
|
||||
);
|
||||
}
|
||||
if (!snapshot.valid) {
|
||||
outro(
|
||||
"Config invalid. Run `clawdbot doctor` to repair it, then re-run configure.",
|
||||
);
|
||||
runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const localUrl = "ws://127.0.0.1:18789";
|
||||
const localProbe = await probeGatewayReachable({
|
||||
url: localUrl,
|
||||
token:
|
||||
baseConfig.gateway?.auth?.token ?? process.env.CLAWDBOT_GATEWAY_TOKEN,
|
||||
password:
|
||||
baseConfig.gateway?.auth?.password ??
|
||||
process.env.CLAWDBOT_GATEWAY_PASSWORD,
|
||||
});
|
||||
const remoteUrl = baseConfig.gateway?.remote?.url?.trim() ?? "";
|
||||
const remoteProbe = remoteUrl
|
||||
? await probeGatewayReachable({
|
||||
url: remoteUrl,
|
||||
token: baseConfig.gateway?.remote?.token,
|
||||
})
|
||||
: null;
|
||||
|
||||
const mode = guardCancel(
|
||||
await select({
|
||||
message: "Where will the Gateway run?",
|
||||
options: [
|
||||
{
|
||||
value: "local",
|
||||
label: "Local (this machine)",
|
||||
hint: localProbe.ok
|
||||
? `Gateway reachable (${localUrl})`
|
||||
: `No gateway detected (${localUrl})`,
|
||||
},
|
||||
{
|
||||
value: "remote",
|
||||
label: "Remote (info-only)",
|
||||
hint: !remoteUrl
|
||||
? "No remote URL configured yet"
|
||||
: remoteProbe?.ok
|
||||
? `Gateway reachable (${remoteUrl})`
|
||||
: `Configured but unreachable (${remoteUrl})`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
runtime,
|
||||
) as "local" | "remote";
|
||||
|
||||
if (mode === "remote") {
|
||||
let remoteConfig = await promptRemoteGatewayConfig(baseConfig, prompter);
|
||||
remoteConfig = applyWizardMetadata(remoteConfig, {
|
||||
command: opts.command,
|
||||
mode,
|
||||
});
|
||||
await writeConfigFile(remoteConfig);
|
||||
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
|
||||
outro("Remote gateway configured.");
|
||||
return;
|
||||
}
|
||||
|
||||
let nextConfig = { ...baseConfig };
|
||||
let workspaceDir =
|
||||
nextConfig.agents?.defaults?.workspace ??
|
||||
baseConfig.agents?.defaults?.workspace ??
|
||||
DEFAULT_WORKSPACE;
|
||||
let gatewayPort = resolveGatewayPort(baseConfig);
|
||||
let gatewayToken: string | undefined =
|
||||
nextConfig.gateway?.auth?.token ??
|
||||
baseConfig.gateway?.auth?.token ??
|
||||
process.env.CLAWDBOT_GATEWAY_TOKEN;
|
||||
|
||||
const persistConfig = async () => {
|
||||
nextConfig = applyWizardMetadata(nextConfig, {
|
||||
command: opts.command,
|
||||
mode,
|
||||
});
|
||||
await writeConfigFile(nextConfig);
|
||||
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
|
||||
};
|
||||
|
||||
if (opts.sections) {
|
||||
const selected = opts.sections;
|
||||
if (!selected || selected.length === 0) {
|
||||
outro("No changes selected.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (selected.includes("workspace")) {
|
||||
const workspaceInput = guardCancel(
|
||||
await text({
|
||||
message: "Workspace directory",
|
||||
initialValue: workspaceDir,
|
||||
}),
|
||||
runtime,
|
||||
);
|
||||
workspaceDir = resolveUserPath(
|
||||
String(workspaceInput ?? "").trim() || DEFAULT_WORKSPACE,
|
||||
);
|
||||
nextConfig = {
|
||||
...nextConfig,
|
||||
agents: {
|
||||
...nextConfig.agents,
|
||||
defaults: {
|
||||
...nextConfig.agents?.defaults,
|
||||
workspace: workspaceDir,
|
||||
},
|
||||
},
|
||||
};
|
||||
await ensureWorkspaceAndSessions(workspaceDir, runtime);
|
||||
}
|
||||
|
||||
if (selected.includes("model")) {
|
||||
nextConfig = await promptAuthConfig(nextConfig, runtime, prompter);
|
||||
}
|
||||
|
||||
if (selected.includes("gateway")) {
|
||||
const gateway = await promptGatewayConfig(nextConfig, runtime);
|
||||
nextConfig = gateway.config;
|
||||
gatewayPort = gateway.port;
|
||||
gatewayToken = gateway.token;
|
||||
}
|
||||
|
||||
if (selected.includes("channels")) {
|
||||
const channelMode = await promptChannelMode(runtime);
|
||||
if (channelMode === "configure") {
|
||||
nextConfig = await setupChannels(nextConfig, runtime, prompter, {
|
||||
allowDisable: true,
|
||||
allowSignalInstall: true,
|
||||
});
|
||||
} else {
|
||||
nextConfig = await removeChannelConfigWizard(nextConfig, runtime);
|
||||
}
|
||||
}
|
||||
|
||||
if (selected.includes("skills")) {
|
||||
const wsDir = resolveUserPath(workspaceDir);
|
||||
nextConfig = await setupSkills(nextConfig, wsDir, runtime, prompter);
|
||||
}
|
||||
|
||||
await persistConfig();
|
||||
|
||||
if (selected.includes("daemon")) {
|
||||
if (!selected.includes("gateway")) {
|
||||
const portInput = guardCancel(
|
||||
await text({
|
||||
message: "Gateway port for daemon install",
|
||||
initialValue: String(gatewayPort),
|
||||
validate: (value) =>
|
||||
Number.isFinite(Number(value)) ? undefined : "Invalid port",
|
||||
}),
|
||||
runtime,
|
||||
);
|
||||
gatewayPort = Number.parseInt(String(portInput), 10);
|
||||
}
|
||||
|
||||
await maybeInstallDaemon({ runtime, port: gatewayPort, gatewayToken });
|
||||
}
|
||||
|
||||
if (selected.includes("health")) {
|
||||
await sleep(1000);
|
||||
try {
|
||||
await healthCommand({ json: false, timeoutMs: 10_000 }, runtime);
|
||||
} catch (err) {
|
||||
runtime.error(formatHealthCheckFailure(err));
|
||||
note(
|
||||
[
|
||||
"Docs:",
|
||||
"https://docs.clawd.bot/gateway/health",
|
||||
"https://docs.clawd.bot/gateway/troubleshooting",
|
||||
].join("\n"),
|
||||
"Health check help",
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let ranSection = false;
|
||||
let didConfigureGateway = false;
|
||||
|
||||
while (true) {
|
||||
const choice = await promptConfigureSection(runtime, ranSection);
|
||||
if (choice === "__continue") break;
|
||||
ranSection = true;
|
||||
|
||||
if (choice === "workspace") {
|
||||
const workspaceInput = guardCancel(
|
||||
await text({
|
||||
message: "Workspace directory",
|
||||
initialValue: workspaceDir,
|
||||
}),
|
||||
runtime,
|
||||
);
|
||||
workspaceDir = resolveUserPath(
|
||||
String(workspaceInput ?? "").trim() || DEFAULT_WORKSPACE,
|
||||
);
|
||||
nextConfig = {
|
||||
...nextConfig,
|
||||
agents: {
|
||||
...nextConfig.agents,
|
||||
defaults: {
|
||||
...nextConfig.agents?.defaults,
|
||||
workspace: workspaceDir,
|
||||
},
|
||||
},
|
||||
};
|
||||
await ensureWorkspaceAndSessions(workspaceDir, runtime);
|
||||
await persistConfig();
|
||||
}
|
||||
|
||||
if (choice === "model") {
|
||||
nextConfig = await promptAuthConfig(nextConfig, runtime, prompter);
|
||||
await persistConfig();
|
||||
}
|
||||
|
||||
if (choice === "gateway") {
|
||||
const gateway = await promptGatewayConfig(nextConfig, runtime);
|
||||
nextConfig = gateway.config;
|
||||
gatewayPort = gateway.port;
|
||||
gatewayToken = gateway.token;
|
||||
didConfigureGateway = true;
|
||||
await persistConfig();
|
||||
}
|
||||
|
||||
if (choice === "channels") {
|
||||
const channelMode = await promptChannelMode(runtime);
|
||||
if (channelMode === "configure") {
|
||||
nextConfig = await setupChannels(nextConfig, runtime, prompter, {
|
||||
allowDisable: true,
|
||||
allowSignalInstall: true,
|
||||
});
|
||||
} else {
|
||||
nextConfig = await removeChannelConfigWizard(nextConfig, runtime);
|
||||
}
|
||||
await persistConfig();
|
||||
}
|
||||
|
||||
if (choice === "skills") {
|
||||
const wsDir = resolveUserPath(workspaceDir);
|
||||
nextConfig = await setupSkills(nextConfig, wsDir, runtime, prompter);
|
||||
await persistConfig();
|
||||
}
|
||||
|
||||
if (choice === "daemon") {
|
||||
if (!didConfigureGateway) {
|
||||
const portInput = guardCancel(
|
||||
await text({
|
||||
message: "Gateway port for daemon install",
|
||||
initialValue: String(gatewayPort),
|
||||
validate: (value) =>
|
||||
Number.isFinite(Number(value)) ? undefined : "Invalid port",
|
||||
}),
|
||||
runtime,
|
||||
);
|
||||
gatewayPort = Number.parseInt(String(portInput), 10);
|
||||
}
|
||||
await maybeInstallDaemon({
|
||||
runtime,
|
||||
port: gatewayPort,
|
||||
gatewayToken,
|
||||
});
|
||||
}
|
||||
|
||||
if (choice === "health") {
|
||||
await sleep(1000);
|
||||
try {
|
||||
await healthCommand({ json: false, timeoutMs: 10_000 }, runtime);
|
||||
} catch (err) {
|
||||
runtime.error(formatHealthCheckFailure(err));
|
||||
note(
|
||||
[
|
||||
"Docs:",
|
||||
"https://docs.clawd.bot/gateway/health",
|
||||
"https://docs.clawd.bot/gateway/troubleshooting",
|
||||
].join("\n"),
|
||||
"Health check help",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!ranSection) {
|
||||
outro("No changes selected.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const controlUiAssets = await ensureControlUiAssetsBuilt(runtime);
|
||||
if (!controlUiAssets.ok && controlUiAssets.message) {
|
||||
runtime.error(controlUiAssets.message);
|
||||
}
|
||||
|
||||
const bind = nextConfig.gateway?.bind ?? "loopback";
|
||||
const links = resolveControlUiLinks({
|
||||
bind,
|
||||
port: gatewayPort,
|
||||
customBindHost: nextConfig.gateway?.customBindHost,
|
||||
basePath: nextConfig.gateway?.controlUi?.basePath,
|
||||
});
|
||||
// Try both new and old passwords since gateway may still have old config.
|
||||
const newPassword =
|
||||
nextConfig.gateway?.auth?.password ??
|
||||
process.env.CLAWDBOT_GATEWAY_PASSWORD;
|
||||
const oldPassword =
|
||||
baseConfig.gateway?.auth?.password ??
|
||||
process.env.CLAWDBOT_GATEWAY_PASSWORD;
|
||||
const token =
|
||||
nextConfig.gateway?.auth?.token ?? process.env.CLAWDBOT_GATEWAY_TOKEN;
|
||||
|
||||
let gatewayProbe = await probeGatewayReachable({
|
||||
url: links.wsUrl,
|
||||
token,
|
||||
password: newPassword,
|
||||
});
|
||||
// If new password failed and it's different from old password, try old too.
|
||||
if (!gatewayProbe.ok && newPassword !== oldPassword && oldPassword) {
|
||||
gatewayProbe = await probeGatewayReachable({
|
||||
url: links.wsUrl,
|
||||
token,
|
||||
password: oldPassword,
|
||||
});
|
||||
}
|
||||
const gatewayStatusLine = gatewayProbe.ok
|
||||
? "Gateway: reachable"
|
||||
: `Gateway: not detected${gatewayProbe.detail ? ` (${gatewayProbe.detail})` : ""}`;
|
||||
|
||||
note(
|
||||
[
|
||||
`Web UI: ${links.httpUrl}`,
|
||||
`Gateway WS: ${links.wsUrl}`,
|
||||
gatewayStatusLine,
|
||||
"Docs: https://docs.clawd.bot/web/control-ui",
|
||||
].join("\n"),
|
||||
"Control UI",
|
||||
);
|
||||
|
||||
outro("Configure complete.");
|
||||
} catch (err) {
|
||||
if (err instanceof WizardCancelledError) {
|
||||
runtime.exit(0);
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user