diff --git a/src/cli.ts b/src/cli.ts index 9b9d9fd..721e94b 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -28,7 +28,7 @@ import { type RoutstrdConfig, } from "./utils/config"; import { COCO_LOGS_DIR, logger } from "./utils/logger"; -import { setupIntegration, runIntegrationsForClients } from "./integrations"; +import { setupIntegration, runIntegrationsForClients, type IntegrationKey } from "./integrations"; import { assertLegacyCocodNotRunning, claimLegacyCocodPidFile, @@ -215,7 +215,7 @@ async function requireLocalDaemon(): Promise { } } -async function initDaemon(): Promise { +async function initDaemon(integrationKey?: IntegrationKey): Promise { console.log("Initializing routstrd..."); // Create config directory (0700, correcting existing installs too) @@ -273,7 +273,7 @@ async function initDaemon(): Promise { await startDaemon({ port: String(config.port || 8008), host: config.host || undefined }); - await setupIntegration(config); + await setupIntegration(config, integrationKey); console.log("\nInitialization complete!"); console.log( @@ -574,20 +574,68 @@ program .description( "Initialize routstrd (creates config directory and initializes wallet)", ) - .action(async () => { - await requireLocalDaemon(); - try { - await initDaemon(); - } catch (error) { - // An expected, user-actionable refusal — print the structured message - // without Bun's unhandled-rejection source snippet and stack trace. - if (error instanceof WalletMigrationConflictError) { - console.error(error.message); + .option("--opencode", "Set up OpenCode integration (non-interactive)") + .option("--openclaw", "Set up OpenClaw integration (non-interactive)") + .option("--pi-agent", "Set up Pi Agent integration (non-interactive)") + .option("--claude-code", "Set up Claude Code integration (non-interactive)") + .option("--hermes", "Set up Hermes integration (non-interactive)") + .option("--skip-integration", "Skip integration setup") + .action( + async (options: { + opencode?: boolean; + openclaw?: boolean; + piAgent?: boolean; + claudeCode?: boolean; + hermes?: boolean; + skipIntegration?: boolean; + }) => { + await requireLocalDaemon(); + + const integrationFlags: Record = { + opencode: options.opencode, + openclaw: options.openclaw, + "pi-agent": options.piAgent, + "claude-code": options.claudeCode, + hermes: options.hermes, + }; + const selectedIntegrations = Object.keys(integrationFlags).filter( + (key) => integrationFlags[key], + ); + + if (selectedIntegrations.length > 1) { + console.error( + "Error: use only one integration flag, or use 'routstrd clients add' for multiple clients.", + ); process.exit(1); } - throw error; - } - }); + + if (options.skipIntegration && selectedIntegrations.length > 0) { + console.error( + "Error: --skip-integration cannot be combined with an integration flag.", + ); + process.exit(1); + } + + let integrationKey: IntegrationKey | undefined; + if (options.skipIntegration) { + integrationKey = "skip"; + } else if (selectedIntegrations.length === 1) { + integrationKey = selectedIntegrations[0]; + } + + try { + await initDaemon(integrationKey); + } catch (error) { + // An expected, user-actionable refusal — print the structured message + // without Bun's unhandled-rejection source snippet and stack trace. + if (error instanceof WalletMigrationConflictError) { + console.error(error.message); + process.exit(1); + } + throw error; + } + }, + ); // Start - start the background daemon program diff --git a/src/integrations/index.ts b/src/integrations/index.ts index b2e34be..f4a5a2e 100644 --- a/src/integrations/index.ts +++ b/src/integrations/index.ts @@ -64,9 +64,72 @@ function parseChoice(input: string): number { return 1; } +/** + * Either a client integration key (as used in CLIENT_CONFIGS) or "skip". + */ +export type IntegrationKey = keyof typeof CLIENT_CONFIGS | "skip"; + +/** + * Create/find the API key for a client and run its install integration. + * Shared by the interactive menu in setupIntegration and direct onboarding + * via `routstrd onboard --` flags. + */ +async function installIntegrationByKey( + config: RoutstrdConfig, + key: keyof typeof CLIENT_CONFIGS, +): Promise { + const integrationConfig = CLIENT_CONFIGS[key]; + if (!integrationConfig) { + console.log(`Unknown integration: ${key}`); + return; + } + + const { client, created } = await addDaemonClient( + integrationConfig.name, + ); + + if (created) { + console.log(`Created new API key for ${integrationConfig.name}`); + } else { + console.log(`Using existing API key for ${integrationConfig.name}`); + } + + switch (key) { + case "opencode": + await installOpencodeIntegration(config, client.apiKey, integrationConfig); + return; + case "openclaw": + await installOpenClawIntegration(config, client.apiKey, integrationConfig); + return; + case "pi-agent": + await installPiIntegration(config, client.apiKey, integrationConfig); + return; + case "claude-code": + await installClaudeCodeIntegration(config, client.apiKey, integrationConfig); + return; + case "hermes": + await installHermesIntegration(config, client.apiKey, integrationConfig); + return; + default: + console.log(`Unknown integration: ${key}`); + } +} + export async function setupIntegration( config: RoutstrdConfig, + integrationKey?: IntegrationKey, ): Promise { + // Non-interactive selection (e.g. `routstrd onboard --pi-agent`). + if (integrationKey === "skip") { + console.log("Skipping integration setup."); + return; + } + + if (integrationKey !== undefined) { + await installIntegrationByKey(config, integrationKey); + return; + } + console.log("\nChoose an integration to set up:"); console.log("1. OpenCode (default)"); console.log("2. OpenClaw"); @@ -92,39 +155,5 @@ export async function setupIntegration( return; } - const integrationConfig = CLIENT_CONFIGS[key]!; - const { client, created } = await addDaemonClient( - integrationConfig.name, - ); - - if (created) { - console.log(`Created new API key for ${integrationConfig.name}`); - } else { - console.log(`Using existing API key for ${integrationConfig.name}`); - } - - if (key === "opencode") { - await installOpencodeIntegration(config, client.apiKey, integrationConfig); - return; - } - - if (key === "openclaw") { - await installOpenClawIntegration(config, client.apiKey, integrationConfig); - return; - } - - if (key === "pi-agent") { - await installPiIntegration(config, client.apiKey, integrationConfig); - return; - } - - if (key === "claude-code") { - await installClaudeCodeIntegration(config, client.apiKey, integrationConfig); - return; - } - - if (key === "hermes") { - await installHermesIntegration(config, client.apiKey, integrationConfig); - return; - } + await installIntegrationByKey(config, key); }