feat(onboard): allow non-interactive integration via --pi-agent/--opencode flags

This commit is contained in:
redshift
2026-08-30 11:30:20 +01:00
parent 0da2407119
commit 61448b9b85
2 changed files with 127 additions and 50 deletions
+63 -15
View File
@@ -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<void> {
}
}
async function initDaemon(): Promise<void> {
async function initDaemon(integrationKey?: IntegrationKey): Promise<void> {
console.log("Initializing routstrd...");
// Create config directory (0700, correcting existing installs too)
@@ -273,7 +273,7 @@ async function initDaemon(): Promise<void> {
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<string, boolean | undefined> = {
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
+64 -35
View File
@@ -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 --<client>` flags.
*/
async function installIntegrationByKey(
config: RoutstrdConfig,
key: keyof typeof CLIENT_CONFIGS,
): Promise<void> {
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<void> {
// 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);
}