diff --git a/bin/cli/commands/config.mjs b/bin/cli/commands/config.mjs index 32d61cba3f..922af53716 100644 --- a/bin/cli/commands/config.mjs +++ b/bin/cli/commands/config.mjs @@ -1,30 +1,8 @@ -import { parseArgs, getStringFlag, hasFlag } from "../args.mjs"; import { printHeading, printInfo, printSuccess, printError } from "../io.mjs"; -import { resolveDataDir } from "../data-dir.mjs"; +import { t } from "../i18n.mjs"; import path from "node:path"; import fs from "node:fs"; -function printConfigHelp() { - console.log(` -Usage: - omniroute config list List all CLI tools and config status - omniroute config get Show current config for a tool - omniroute config set [options] Write config for a tool - omniroute config validate Validate config format without writing - -Options: - --base-url OmniRoute API base URL (default: http://localhost:20128/v1) - --api-key API key for the tool - --model Model identifier (where applicable) - --json Output as JSON - --non-interactive Do not prompt for confirmation - --yes Skip confirmation prompt - --help Show this help - -Tools: claude, codex, opencode, cline, kilocode, continue -`); -} - function ensureBackup(configPath) { if (!fs.existsSync(configPath)) return; const backupDir = path.join(path.dirname(configPath), ".omniroute.bak"); @@ -34,149 +12,180 @@ function ensureBackup(configPath) { return backupPath; } -export async function runConfigCommand(argv) { - const { flags, positionals } = parseArgs(argv); +async function runConfigListCommand(opts = {}) { + const { detectAllTools } = await import("../../../src/lib/cli-helper/tool-detector.js"); + const tools = await detectAllTools(); - if (hasFlag(flags, "help") || hasFlag(flags, "h") || positionals.length === 0) { - printConfigHelp(); - return 0; + if (opts.json) { + console.log(JSON.stringify(tools, null, 2)); + } else { + printHeading("CLI Tool Configuration Status"); + for (const t of tools) { + const status = t.configured + ? "✓ Configured" + : t.installed + ? "✗ Not configured" + : "✗ Not installed"; + console.log(` ${t.name.padEnd(14)} ${status}`); + if (t.version) console.log(` version: ${t.version}`); + console.log(` config: ${t.configPath}`); + } } + return 0; +} - const subcommand = positionals[0]; - const toolId = positionals[1]; - - if (subcommand === "list") { - const { detectAllTools } = await import("../../../src/lib/cli-helper/tool-detector.js"); - const tools = await detectAllTools(); - - if (hasFlag(flags, "json")) { - console.log(JSON.stringify(tools, null, 2)); - } else { - printHeading("CLI Tool Configuration Status"); - for (const t of tools) { - const status = t.configured - ? "✓ Configured" - : t.installed - ? "✗ Not configured" - : "✗ Not installed"; - console.log(` ${t.name.padEnd(14)} ${status}`); - if (t.version) console.log(` version: ${t.version}`); - console.log(` config: ${t.configPath}`); - } +async function runConfigGetCommand(toolId, opts = {}) { + if (!toolId) { + printError("Tool ID required. Usage: omniroute config get "); + return 1; + } + const { detectTool } = await import("../../../src/lib/cli-helper/tool-detector.js"); + const tool = await detectTool(toolId); + if (!tool) { + printError(`Unknown tool: ${toolId}`); + return 1; + } + if (opts.json) { + console.log(JSON.stringify(tool, null, 2)); + } else { + printHeading(`${tool.name} Configuration`); + console.log(` Installed: ${tool.installed ? "Yes" : "No"}`); + console.log(` Configured: ${tool.configured ? "Yes" : "No"}`); + console.log(` Config: ${tool.configPath}`); + if (tool.version) console.log(` Version: ${tool.version}`); + if (tool.configContents) { + console.log(`\n Contents:`); + console.log(tool.configContents); } - return 0; } + return 0; +} - if (subcommand === "get") { - if (!toolId) { - printError("Tool ID required. Usage: omniroute config get "); - return 1; - } - const { detectTool } = await import("../../../src/lib/cli-helper/tool-detector.js"); - const tool = await detectTool(toolId); - if (!tool) { - printError(`Unknown tool: ${toolId}`); - return 1; - } - if (hasFlag(flags, "json")) { - console.log(JSON.stringify(tool, null, 2)); - } else { - printHeading(`${tool.name} Configuration`); - console.log(` Installed: ${tool.installed ? "Yes" : "No"}`); - console.log(` Configured: ${tool.configured ? "Yes" : "No"}`); - console.log(` Config: ${tool.configPath}`); - if (tool.version) console.log(` Version: ${tool.version}`); - if (tool.configContents) { - console.log(`\n Contents:`); - console.log(tool.configContents); - } - } - return 0; +async function runConfigSetCommand(toolId, opts = {}) { + if (!toolId) { + printError("Tool ID required. Usage: omniroute config set [options]"); + return 1; } - if (subcommand === "set") { - if (!toolId) { - printError("Tool ID required. Usage: omniroute config set [options]"); - return 1; - } + const baseUrl = opts.baseUrl || "http://localhost:20128/v1"; + const apiKey = opts.apiKey; + const model = opts.model; - const baseUrl = - getStringFlag(flags, "base-url", "OMNIROUTE_BASE_URL") || "http://localhost:20128/v1"; - const apiKey = getStringFlag(flags, "api-key", "OMNIROUTE_API_KEY"); - const model = getStringFlag(flags, "model"); + if (!apiKey) { + printError("API key required. Use --api-key or set OMNIROUTE_API_KEY."); + return 1; + } - if (!apiKey) { - printError("API key required. Use --api-key or set OMNIROUTE_API_KEY."); - return 1; - } + const { generateConfig } = await import("../../../src/lib/cli-helper/config-generator/index.js"); + const result = await generateConfig(toolId, { baseUrl, apiKey, model }); - const { generateConfig } = - await import("../../../src/lib/cli-helper/config-generator/index.js"); - const result = await generateConfig(toolId, { baseUrl, apiKey, model }); + if (!result.success) { + printError(result.error || "Failed to generate config"); + return 1; + } - if (!result.success) { - printError(result.error || "Failed to generate config"); - return 1; - } + const nonInteractive = opts.nonInteractive || opts.yes; - const nonInteractive = hasFlag(flags, "non-interactive") || hasFlag(flags, "yes"); + if (!nonInteractive) { + console.log(`\n About to write config to: ${result.configPath}`); + console.log(` Content preview:\n`); + console.log(result.content); + console.log(""); - if (!nonInteractive) { - console.log(`\n About to write config to: ${result.configPath}`); - console.log(` Content preview:\n`); - console.log(result.content); - console.log(""); + const readline = await import("node:readline"); + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + const answer = await new Promise((resolve) => rl.question("Proceed? [y/N] ", resolve)); + rl.close(); - const readline = await import("node:readline"); - const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); - const answer = await new Promise((resolve) => rl.question("Proceed? [y/N] ", resolve)); - rl.close(); + if (!/^y(es)?$/i.test(answer)) { + console.log("Aborted."); + return 0; + } + } - if (!/^y(es)?$/i.test(answer)) { - console.log("Aborted."); - return 0; - } - } + const dir = path.dirname(result.configPath); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); - const dir = path.dirname(result.configPath); - if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + const backupPath = ensureBackup(result.configPath); + if (backupPath) printInfo(`Backup saved to: ${backupPath}`); - const backupPath = ensureBackup(result.configPath); - if (backupPath) printInfo(`Backup saved to: ${backupPath}`); + fs.writeFileSync(result.configPath, result.content, "utf-8"); + printSuccess(`Config written to ${result.configPath}`); + return 0; +} - fs.writeFileSync(result.configPath, result.content, "utf-8"); - printSuccess(`Config written to ${result.configPath}`); - return 0; +async function runConfigValidateCommand(toolId, opts = {}) { + if (!toolId) { + printError("Tool ID required. Usage: omniroute config validate "); + return 1; } - if (subcommand === "validate") { - if (!toolId) { - printError("Tool ID required. Usage: omniroute config validate "); - return 1; - } + const baseUrl = opts.baseUrl || "http://localhost:20128/v1"; + const apiKey = opts.apiKey || "test-key"; + const model = opts.model; - const baseUrl = - getStringFlag(flags, "base-url", "OMNIROUTE_BASE_URL") || "http://localhost:20128/v1"; - const apiKey = getStringFlag(flags, "api-key", "OMNIROUTE_API_KEY") || "test-key"; - const model = getStringFlag(flags, "model"); + const { generateConfig } = await import("../../../src/lib/cli-helper/config-generator/index.js"); + const result = await generateConfig(toolId, { baseUrl, apiKey, model }); - const { generateConfig } = - await import("../../../src/lib/cli-helper/config-generator/index.js"); - const result = await generateConfig(toolId, { baseUrl, apiKey, model }); + if (!result.success) { + printError(`Validation failed: ${result.error}`); + return 1; + } - if (!result.success) { - printError(`Validation failed: ${result.error}`); - return 1; - } - - printSuccess(`Config for ${toolId} is valid`); - if (hasFlag(flags, "json")) { - console.log(JSON.stringify({ valid: true, content: result.content }, null, 2)); - } - return 0; + printSuccess(`Config for ${toolId} is valid`); + if (opts.json) { + console.log(JSON.stringify({ valid: true, content: result.content }, null, 2)); } - - printError(`Unknown subcommand: ${subcommand}`); - printConfigHelp(); - return 1; + return 0; +} + +export function registerConfig(program) { + const config = program.command("config").description("Show or update CLI tool configuration"); + + config + .command("list") + .description("List all CLI tools and config status") + .option("--json", "Output as JSON") + .action(async (opts, cmd) => { + const globalOpts = cmd.parent.optsWithGlobals(); + const exitCode = await runConfigListCommand({ ...opts, output: globalOpts.output }); + if (exitCode !== 0) process.exit(exitCode); + }); + + config + .command("get ") + .description("Show current config for a tool") + .option("--json", "Output as JSON") + .action(async (tool, opts, cmd) => { + const globalOpts = cmd.parent.optsWithGlobals(); + const exitCode = await runConfigGetCommand(tool, { ...opts, output: globalOpts.output }); + if (exitCode !== 0) process.exit(exitCode); + }); + + config + .command("set ") + .description("Write config for a tool") + .option("--base-url ", "OmniRoute API base URL", "http://localhost:20128/v1") + .option("--api-key ", "API key for the tool") + .option("--model ", "Model identifier (where applicable)") + .option("--non-interactive", "Do not prompt for confirmation") + .option("--yes", "Skip confirmation prompt") + .action(async (tool, opts, cmd) => { + const globalOpts = cmd.parent.optsWithGlobals(); + const exitCode = await runConfigSetCommand(tool, { ...opts, output: globalOpts.output }); + if (exitCode !== 0) process.exit(exitCode); + }); + + config + .command("validate ") + .description("Validate config format without writing") + .option("--base-url ", "OmniRoute API base URL", "http://localhost:20128/v1") + .option("--api-key ", "API key for the tool") + .option("--model ", "Model identifier (where applicable)") + .option("--json", "Output as JSON") + .action(async (tool, opts, cmd) => { + const globalOpts = cmd.parent.optsWithGlobals(); + const exitCode = await runConfigValidateCommand(tool, { ...opts, output: globalOpts.output }); + if (exitCode !== 0) process.exit(exitCode); + }); } diff --git a/bin/cli/commands/doctor.mjs b/bin/cli/commands/doctor.mjs index 3fe11b1a31..3426f5b730 100644 --- a/bin/cli/commands/doctor.mjs +++ b/bin/cli/commands/doctor.mjs @@ -4,9 +4,9 @@ import os from "node:os"; import path from "node:path"; import { createDecipheriv, scryptSync } from "node:crypto"; import { pathToFileURL } from "node:url"; -import { parseArgs, getStringFlag, hasFlag } from "../args.mjs"; import { resolveDataDir, resolveStoragePath } from "../data-dir.mjs"; import { printHeading } from "../io.mjs"; +import { t } from "../i18n.mjs"; const STATIC_SALT = "omniroute-field-encryption-v1"; const KEY_LENGTH = 32; @@ -181,8 +181,11 @@ function decryptCredentialSample(value, key) { const [ivHex, encryptedHex, authTagHex] = body.split(":"); if (!ivHex || !encryptedHex || !authTagHex) throw new Error("Malformed encrypted value"); - const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(ivHex, "hex")); - decipher.setAuthTag(Buffer.from(authTagHex, "hex")); + const authTagBuf = Buffer.from(authTagHex, "hex"); + const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(ivHex, "hex"), { + authTagLength: authTagBuf.length, + }); + decipher.setAuthTag(authTagBuf); let decrypted = decipher.update(encryptedHex, "hex", "utf8"); decrypted += decipher.final("utf8"); return decrypted; @@ -487,25 +490,6 @@ export async function collectDoctorChecks(context = {}, options = {}) { }; } -function printDoctorHelp() { - console.log(` -Usage: - omniroute doctor - omniroute doctor --json - omniroute doctor --no-liveness - omniroute doctor --host 0.0.0.0 - -Options: - --json Print machine-readable JSON - --no-liveness Skip HTTP health endpoint probing - --host Host for server liveness probing (default: 127.0.0.1) - --liveness-url Full health endpoint URL override - -Checks: - config, database, storage/encryption, ports, Node runtime, native binary, memory, server liveness, CLI tools -`); -} - function printCheck(check) { const label = check.status.toUpperCase().padEnd(4); const color = @@ -513,20 +497,31 @@ function printCheck(check) { console.log(`${color}${label}\x1b[0m ${check.name}: ${check.message}`); } -export async function runDoctorCommand(argv, context = {}) { - const { flags } = parseArgs(argv); - if (hasFlag(flags, "help") || hasFlag(flags, "h")) { - printDoctorHelp(); - return 0; - } +export function registerDoctor(program) { + program + .command("doctor") + .description(t("doctor.title")) + .option("--no-liveness", "Skip HTTP health endpoint probing") + .option("--host ", "Host for server liveness probing", "127.0.0.1") + .option("--liveness-url ", "Full health endpoint URL override") + .action(async (opts, cmd) => { + const globalOpts = cmd.optsWithGlobals(); + const exitCode = await runDoctorCommand({ ...opts, output: globalOpts.output }); + if (exitCode !== 0) process.exit(exitCode); + }); +} + +export async function runDoctorCommand(opts = {}, context = {}) { + const isJson = (opts.output ?? "table") === "json"; + const skipLiveness = !(opts.liveness ?? true); const result = await collectDoctorChecks(context, { - skipLiveness: hasFlag(flags, "no-liveness"), - livenessHost: getStringFlag(flags, "host"), - livenessUrl: getStringFlag(flags, "liveness-url"), + skipLiveness, + livenessHost: opts.host, + livenessUrl: opts.livenessUrl, }); - if (hasFlag(flags, "json")) { + if (isJson) { console.log(JSON.stringify(result, null, 2)); } else { printHeading("OmniRoute Doctor"); diff --git a/bin/cli/commands/logs.mjs b/bin/cli/commands/logs.mjs index 28763922b1..87b2f72547 100644 --- a/bin/cli/commands/logs.mjs +++ b/bin/cli/commands/logs.mjs @@ -1,35 +1,29 @@ -import { parseArgs, getStringFlag, hasFlag } from "../args.mjs"; -import { printHeading, printInfo, printError } from "../io.mjs"; +import { printInfo, printError } from "../io.mjs"; +import { t } from "../i18n.mjs"; -function printLogsHelp() { - console.log(` -Usage: - omniroute logs [options] - -Options: - --follow Stream logs in real-time - --filter Filter by level (error, warn, info) — comma-separated - --lines Number of lines to fetch (default: 100) - --timeout Connection timeout in ms (default: 30000) - --base-url OmniRoute API base URL (default: http://localhost:20128) - --json Output as JSON - --help Show this help -`); +export function registerLogs(program) { + program + .command("logs") + .description("Stream request logs") + .option("--follow", "Stream logs in real-time") + .option("--filter ", "Filter by level (error,warn,info) — comma-separated") + .option("--lines ", "Number of lines to fetch", "100") + .option("--timeout ", "Connection timeout in ms", "30000") + .option("--base-url ", "OmniRoute API base URL", "http://localhost:20128") + .action(async (opts, cmd) => { + const globalOpts = cmd.optsWithGlobals(); + const exitCode = await runLogsCommand({ ...opts, output: globalOpts.output }); + if (exitCode !== 0) process.exit(exitCode); + }); } -export async function runLogsCommand(argv) { - const { flags } = parseArgs(argv); - - if (hasFlag(flags, "help") || hasFlag(flags, "h")) { - printLogsHelp(); - return 0; - } - - const baseUrl = getStringFlag(flags, "base-url") || "http://localhost:20128"; - const follow = hasFlag(flags, "follow"); - const filter = getStringFlag(flags, "filter"); - const lines = getStringFlag(flags, "lines") || "100"; - const timeout = parseInt(getStringFlag(flags, "timeout") || "30000", 10); +export async function runLogsCommand(opts = {}) { + const baseUrl = opts.baseUrl || opts["base-url"] || "http://localhost:20128"; + const follow = opts.follow ?? false; + const filter = opts.filter ?? ""; + const lines = opts.lines || "100"; + const timeout = parseInt(String(opts.timeout || "30000"), 10); + const isJson = opts.output === "json"; const filters = filter ? filter.split(",").map((f) => f.trim()) : []; @@ -42,7 +36,7 @@ export async function runLogsCommand(argv) { const processLine = (line) => { if (!line.trim()) return; - if (hasFlag(flags, "json")) { + if (isJson) { console.log(line); return; } @@ -64,9 +58,9 @@ export async function runLogsCommand(argv) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split("\n"); - buffer = lines.pop() || ""; - for (const line of lines) processLine(line); + const parts = buffer.split("\n"); + buffer = parts.pop() || ""; + for (const line of parts) processLine(line); } if (buffer) processLine(buffer); } catch (err) { diff --git a/bin/cli/commands/provider-cmd.mjs b/bin/cli/commands/provider-cmd.mjs index bbd977d8a3..e6e44183ea 100644 --- a/bin/cli/commands/provider-cmd.mjs +++ b/bin/cli/commands/provider-cmd.mjs @@ -1,278 +1,18 @@ -import { parseArgs, getStringFlag, hasFlag } from "../args.mjs"; -import { printHeading, printInfo, printSuccess, printError } from "../io.mjs"; -import { resolveDataDir, resolveStoragePath } from "../data-dir.mjs"; -import path from "node:path"; -import fs from "node:fs"; +export function registerProvider(program) { + program + .command("provider [subcommand]") + .description("Manage provider connections (use 'providers' for the full interface)") + .allowUnknownOption() + .allowExcessArguments() + .action(() => { + console.log(` + Use \`omniroute providers\` for the full provider management interface: -function printProviderHelp() { - console.log(` -Usage: - omniroute provider add [options] Add a provider connection - omniroute provider list List configured providers - omniroute provider remove Remove a provider connection - omniroute provider test Test a provider connection - omniroute provider default Set default provider - -Options: - --provider Provider id (e.g., openai, anthropic, omniroute) - --api-key API key for the provider - --provider-name Display name for the connection - --default-model Default model to use - --base-url Custom base URL override - --json Output as JSON - --yes Skip confirmation - --help Show this help + omniroute providers available — show provider catalog + omniroute providers list — list configured connections + omniroute providers test — test a provider connection + omniroute providers test-all — test all active connections + omniroute providers validate — validate local configuration `); -} - -export async function runProviderCommand(argv) { - const { flags, positionals } = parseArgs(argv); - - if (hasFlag(flags, "help") || hasFlag(flags, "h") || positionals.length === 0) { - printProviderHelp(); - return 0; - } - - const subcommand = positionals[0]; - - if (subcommand === "add") { - const providerName = positionals[1] || getStringFlag(flags, "provider"); - const apiKey = getStringFlag(flags, "api-key"); - const displayName = getStringFlag(flags, "provider-name"); - const defaultModel = getStringFlag(flags, "default-model"); - const baseUrl = getStringFlag(flags, "base-url"); - - if (!providerName) { - printError("Provider name required. Usage: omniroute provider add "); - return 1; - } - - if (providerName === "omniroute") { - // Special case: add OmniRoute as a provider in OpenCode config - const opencodePath = path.join( - process.env.HOME || os.homedir(), - ".config", - "opencode", - "opencode.json" - ); - const { generateConfig } = - await import("../../../src/lib/cli-helper/config-generator/index.js"); - const result = await generateConfig("opencode", { - baseUrl: baseUrl || "http://localhost:20128/v1", - apiKey: apiKey || "", - }); - - if (!result.success) { - printError(result.error || "Failed to generate config"); - return 1; - } - - if (!hasFlag(flags, "yes")) { - console.log(`\n About to write OpenCode config to: ${opencodePath}`); - console.log(` Content:\n`); - console.log(result.content); - console.log(""); - const readline = await import("node:readline"); - const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); - const answer = await new Promise((resolve) => rl.question("Proceed? [y/N] ", resolve)); - rl.close(); - if (!/^y(es)?$/i.test(answer)) { - printInfo("Aborted."); - return 0; - } - } - - const dir = path.dirname(opencodePath); - if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); - fs.writeFileSync(opencodePath, result.content, "utf-8"); - printSuccess(`OpenCode config written to ${opencodePath}`); - return 0; - } - - // Generic provider addition via SQLite - const dbPath = resolveStoragePath(resolveDataDir()); - if (!fs.existsSync(dbPath)) { - printError("Database not found. Run `omniroute setup` first."); - return 1; - } - - const { default: Database } = await import("better-sqlite3"); - const db = new Database(dbPath); - - try { - const stmt = db.prepare(` - INSERT INTO provider_connections (provider, name, api_key, default_model, provider_specific_data) - VALUES (?, ?, ?, ?, ?) - `); - const specificData = baseUrl ? JSON.stringify({ baseUrl }) : null; - stmt.run( - providerName, - displayName || providerName, - apiKey || "", - defaultModel || null, - specificData - ); - printSuccess(`Provider "${displayName || providerName}" added`); - } finally { - db.close(); - } - - return 0; - } - - if (subcommand === "list") { - const dbPath = resolveStoragePath(resolveDataDir()); - if (!fs.existsSync(dbPath)) { - if (isJson()) console.log(JSON.stringify([])); - else printInfo("No database found. Run `omniroute setup` first."); - return 0; - } - - const { default: Database } = await import("better-sqlite3"); - const db = new Database(dbPath); - try { - const rows = db - .prepare("SELECT id, provider, name, default_model FROM provider_connections") - .all(); - if (isJson()) { - console.log(JSON.stringify(rows, null, 2)); - } else { - printHeading("Configured Providers"); - for (const r of rows) { - console.log( - ` [${r.id}] ${r.name} (${r.provider})${r.default_model ? ` — model: ${r.default_model}` : ""}` - ); - } - } - } finally { - db.close(); - } - return 0; - } - - if (subcommand === "remove") { - const target = positionals[1]; - if (!target) { - printError("Provider name or ID required. Usage: omniroute provider remove "); - return 1; - } - - const dbPath = resolveStoragePath(resolveDataDir()); - if (!fs.existsSync(dbPath)) { - printError("Database not found."); - return 1; - } - - const { default: Database } = await import("better-sqlite3"); - const db = new Database(dbPath); - try { - const isId = /^\d+$/.test(target); - const stmt = isId - ? db.prepare("DELETE FROM provider_connections WHERE id = ?") - : db.prepare("DELETE FROM provider_connections WHERE name = ? OR provider = ?"); - const result = stmt.run(isId ? parseInt(target, 10) : target); - if (result.changes > 0) { - printSuccess(`Removed ${result.changes} provider(s)`); - } else { - printError("Provider not found"); - } - } finally { - db.close(); - } - return 0; - } - - if (subcommand === "test") { - const target = positionals[1]; - if (!target) { - printError("Provider name or ID required. Usage: omniroute provider test "); - return 1; - } - - const dbPath = resolveStoragePath(resolveDataDir()); - if (!fs.existsSync(dbPath)) { - printError("Database not found."); - return 1; - } - - const { default: Database } = await import("better-sqlite3"); - const db = new Database(dbPath); - try { - const isId = /^\d+$/.test(target); - const row = isId - ? db.prepare("SELECT * FROM provider_connections WHERE id = ?").get(parseInt(target, 10)) - : db - .prepare("SELECT * FROM provider_connections WHERE name = ? OR provider = ?") - .get(target); - - if (!row) { - printError("Provider not found"); - return 1; - } - - const { testProviderApiKey } = await import("../provider-test.mjs"); - const result = await testProviderApiKey({ - provider: row.provider, - apiKey: row.api_key, - defaultModel: row.default_model, - baseUrl: row.provider_specific_data ? JSON.parse(row.provider_specific_data).baseUrl : null, - }); - - if (isJson()) { - console.log(JSON.stringify(result, null, 2)); - } else if (result.valid) { - printSuccess(`Provider "${row.name}" is reachable`); - } else { - printError(`Provider test failed: ${result.error || "unknown error"}`); - } - } finally { - db.close(); - } - return 0; - } - - if (subcommand === "default") { - const target = positionals[1]; - if (!target) { - printError("Provider name or ID required. Usage: omniroute provider default "); - return 1; - } - - const dbPath = resolveStoragePath(resolveDataDir()); - if (!fs.existsSync(dbPath)) { - printError("Database not found."); - return 1; - } - - const { default: Database } = await import("better-sqlite3"); - const db = new Database(dbPath); - try { - const isId = /^\d+$/.test(target); - const row = isId - ? db.prepare("SELECT * FROM provider_connections WHERE id = ?").get(parseInt(target, 10)) - : db - .prepare("SELECT * FROM provider_connections WHERE name = ? OR provider = ?") - .get(target); - - if (!row) { - printError("Provider not found"); - return 1; - } - - db.prepare("UPDATE provider_connections SET is_default = 0").run(); - db.prepare("UPDATE provider_connections SET is_default = 1 WHERE id = ?").run(row.id); - printSuccess(`Default provider set to "${row.name}"`); - } finally { - db.close(); - } - return 0; - } - - printError(`Unknown subcommand: ${subcommand}`); - printProviderHelp(); - return 1; -} - -function isJson() { - return process.argv.includes("--json"); + }); } diff --git a/bin/cli/commands/providers.mjs b/bin/cli/commands/providers.mjs index 8401a34f6d..b9affdeb8c 100644 --- a/bin/cli/commands/providers.mjs +++ b/bin/cli/commands/providers.mjs @@ -1,4 +1,3 @@ -import { parseArgs, getStringFlag, hasFlag } from "../args.mjs"; import { printHeading } from "../io.mjs"; import { getAvailableProviderCategories, loadAvailableProviders } from "../provider-catalog.mjs"; import { testProviderApiKey } from "../provider-test.mjs"; @@ -9,6 +8,7 @@ import { updateProviderTestResult, } from "../provider-store.mjs"; import { openOmniRouteDb } from "../sqlite.mjs"; +import { t } from "../i18n.mjs"; function publicConnection(connection) { return { @@ -24,113 +24,6 @@ function publicConnection(connection) { }; } -function printProvidersHelp() { - console.log(` -Usage: - omniroute providers available - omniroute providers available --search openai - omniroute providers available --category api-key - omniroute providers list - omniroute providers test - omniroute providers test-all - omniroute providers validate - -Options: - --json Print machine-readable JSON - --search, --q Filter available providers by id, name, alias, or category - --category Filter available providers by category - -Notes: - "available" shows the OmniRoute provider catalog. - "list" shows provider connections already configured in local SQLite. - Provider commands read local SQLite directly and do not require the server to be running. - API-key provider tests update test_status, last_tested, and error fields in SQLite. -`); -} - -function printAvailableHelp() { - console.log(` -Usage: - omniroute providers available - omniroute providers available --search openai - omniroute providers available --category api-key - omniroute providers available --json - -Options: - --json Print machine-readable JSON - --search, --q Filter by id, name, alias, or category - --category Filter by category, for example api-key, oauth, free - -Notes: - Shows the OmniRoute provider catalog, not locally configured provider connections. -`); -} - -function printListHelp() { - console.log(` -Usage: - omniroute providers list - omniroute providers list --json - -Options: - --json Print machine-readable JSON - -Notes: - Lists provider connections already configured in local SQLite. -`); -} - -function printTestHelp() { - console.log(` -Usage: - omniroute providers test - omniroute providers test --json - -Options: - --json Print machine-readable JSON - -Notes: - Tests one configured provider connection and updates test status in local SQLite. -`); -} - -function printTestAllHelp() { - console.log(` -Usage: - omniroute providers test-all - omniroute providers test-all --json - -Options: - --json Print machine-readable JSON - -Notes: - Tests every active configured provider connection and updates test status in local SQLite. -`); -} - -function printValidateHelp() { - console.log(` -Usage: - omniroute providers validate - omniroute providers validate --json - -Options: - --json Print machine-readable JSON - -Notes: - Validates local provider configuration without calling upstream providers. -`); -} - -function printProvidersSubcommandHelp(subcommand) { - if (subcommand === "available") printAvailableHelp(); - else if (subcommand === "list") printListHelp(); - else if (subcommand === "test") printTestHelp(); - else if (subcommand === "test-all") printTestAllHelp(); - else if (subcommand === "validate") printValidateHelp(); - else printProvidersHelp(); -} - function statusColor(status) { if (status === "active" || status === "success") return "\x1b[32m"; if (status === "error" || status === "expired" || status === "unavailable") return "\x1b[31m"; @@ -186,11 +79,11 @@ function publicAvailableProvider(provider) { }; } -function filterAvailableProviders(providers, flags) { - const search = String(getStringFlag(flags, "search") || getStringFlag(flags, "q") || "") +function filterAvailableProviders(providers, opts) { + const search = String(opts.search || opts.q || "") .trim() .toLowerCase(); - const category = normalizeCategoryFilter(getStringFlag(flags, "category")); + const category = normalizeCategoryFilter(opts.category); return providers.filter((provider) => { if (category && provider.category !== category) return false; @@ -284,12 +177,12 @@ function validateConnection(connection) { }; } -async function availableCommand(flags) { +export async function runAvailableCommand(opts = {}) { const allProviders = loadAvailableProviders(); - const providers = filterAvailableProviders(allProviders, flags).map(publicAvailableProvider); + const providers = filterAvailableProviders(allProviders, opts).map(publicAvailableProvider); const categories = getAvailableProviderCategories(allProviders); - if (hasFlag(flags, "json")) { + if (opts.json) { console.log(JSON.stringify({ count: providers.length, categories, providers }, null, 2)); } else { printHeading("OmniRoute Available Providers"); @@ -299,11 +192,11 @@ async function availableCommand(flags) { return 0; } -async function listCommand(flags) { +export async function runListCommand(opts = {}) { const { db } = await openOmniRouteDb(); try { const connections = listProviderConnections(db).map(publicConnection); - if (hasFlag(flags, "json")) { + if (opts.json) { console.log(JSON.stringify({ providers: connections }, null, 2)); } else { printHeading("OmniRoute Providers"); @@ -315,7 +208,7 @@ async function listCommand(flags) { } } -async function testCommand(flags, selector) { +export async function runTestCommand(selector, opts = {}) { if (!selector) { console.error("Provider id or name is required."); return 1; @@ -330,7 +223,7 @@ async function testCommand(flags, selector) { } const result = await runProviderTest(db, connection); - if (hasFlag(flags, "json")) { + if (opts.json) { console.log(JSON.stringify(result, null, 2)); } else if (result.valid) { console.log(`\x1b[32mOK\x1b[0m ${connection.name}: provider test passed`); @@ -343,7 +236,7 @@ async function testCommand(flags, selector) { } } -async function testAllCommand(flags) { +export async function runTestAllCommand(opts = {}) { const { db } = await openOmniRouteDb(); try { const connections = listProviderConnections(db); @@ -361,7 +254,7 @@ async function testAllCommand(flags) { results.push(await runProviderTest(db, connection)); } - if (hasFlag(flags, "json")) { + if (opts.json) { console.log(JSON.stringify({ results }, null, 2)); } else { printHeading("OmniRoute Provider Tests"); @@ -383,11 +276,11 @@ async function testAllCommand(flags) { } } -async function validateCommand(flags) { +export async function runValidateCommand(opts = {}) { const { db } = await openOmniRouteDb(); try { const results = listProviderConnections(db).map(validateConnection); - if (hasFlag(flags, "json")) { + if (opts.json) { console.log(JSON.stringify({ results }, null, 2)); } else { printHeading("OmniRoute Provider Validation"); @@ -406,23 +299,59 @@ async function validateCommand(flags) { } } -export async function runProvidersCommand(argv) { - const { flags, positionals } = parseArgs(argv); - const requestedSubcommand = positionals[0]; - const subcommand = requestedSubcommand || "list"; +export function registerProviders(program) { + const providers = program.command("providers").description(t("providers.title")); - if (hasFlag(flags, "help") || hasFlag(flags, "h")) { - printProvidersSubcommandHelp(requestedSubcommand); - return 0; - } + providers + .command("available") + .description("Show available providers in the OmniRoute catalog") + .option("--json", "Print machine-readable JSON") + .option("--search ", "Filter by id, name, alias, or category") + .option("-q, --q ", "Alias for --search") + .option("--category ", "Filter by category (api-key, oauth, free)") + .action(async (opts, cmd) => { + const globalOpts = cmd.parent.optsWithGlobals(); + const exitCode = await runAvailableCommand({ ...opts, output: globalOpts.output }); + if (exitCode !== 0) process.exit(exitCode); + }); - if (subcommand === "available") return availableCommand(flags); - if (subcommand === "list") return listCommand(flags); - if (subcommand === "test") return testCommand(flags, positionals[1]); - if (subcommand === "test-all") return testAllCommand(flags); - if (subcommand === "validate") return validateCommand(flags); + providers + .command("list") + .description("List configured provider connections") + .option("--json", "Print machine-readable JSON") + .action(async (opts, cmd) => { + const globalOpts = cmd.parent.optsWithGlobals(); + const exitCode = await runListCommand({ ...opts, output: globalOpts.output }); + if (exitCode !== 0) process.exit(exitCode); + }); - console.error(`Unknown providers subcommand: ${subcommand}`); - printProvidersHelp(); - return 1; + providers + .command("test ") + .description("Test a configured provider connection") + .option("--json", "Print machine-readable JSON") + .action(async (idOrName, opts, cmd) => { + const globalOpts = cmd.parent.optsWithGlobals(); + const exitCode = await runTestCommand(idOrName, { ...opts, output: globalOpts.output }); + if (exitCode !== 0) process.exit(exitCode); + }); + + providers + .command("test-all") + .description("Test all active provider connections") + .option("--json", "Print machine-readable JSON") + .action(async (opts, cmd) => { + const globalOpts = cmd.parent.optsWithGlobals(); + const exitCode = await runTestAllCommand({ ...opts, output: globalOpts.output }); + if (exitCode !== 0) process.exit(exitCode); + }); + + providers + .command("validate") + .description("Validate local provider configuration without calling upstream") + .option("--json", "Print machine-readable JSON") + .action(async (opts, cmd) => { + const globalOpts = cmd.parent.optsWithGlobals(); + const exitCode = await runValidateCommand({ ...opts, output: globalOpts.output }); + if (exitCode !== 0) process.exit(exitCode); + }); } diff --git a/bin/cli/commands/registry.mjs b/bin/cli/commands/registry.mjs index 35d569903e..0c463e268d 100644 --- a/bin/cli/commands/registry.mjs +++ b/bin/cli/commands/registry.mjs @@ -1,82 +1,21 @@ import { registerServe } from "./serve.mjs"; -import { runDoctorCommand } from "./doctor.mjs"; -import { runSetupCommand } from "./setup.mjs"; -import { runProvidersCommand } from "./providers.mjs"; -import { runProviderCommand } from "./provider-cmd.mjs"; -import { runConfigCommand } from "./config.mjs"; -import { runStatusCommand } from "./status.mjs"; -import { runLogsCommand } from "./logs.mjs"; -import { runUpdateCommand } from "./update.mjs"; -import { t } from "../i18n.mjs"; - -function argvAfter(cmdName) { - const idx = process.argv.findIndex((a, i) => i >= 2 && a === cmdName); - return idx >= 0 ? process.argv.slice(idx + 1) : []; -} - -function legacyAction(name, handler) { - return async () => { - const exitCode = await handler(argvAfter(name)); - process.exit(exitCode ?? 0); - }; -} +import { registerDoctor } from "./doctor.mjs"; +import { registerSetup } from "./setup.mjs"; +import { registerProviders } from "./providers.mjs"; +import { registerProvider } from "./provider-cmd.mjs"; +import { registerConfig } from "./config.mjs"; +import { registerStatus } from "./status.mjs"; +import { registerLogs } from "./logs.mjs"; +import { registerUpdate } from "./update.mjs"; export function registerCommands(program) { registerServe(program); - - program - .command("doctor") - .description(t("doctor.title")) - .allowUnknownOption() - .allowExcessArguments() - .action(legacyAction("doctor", runDoctorCommand)); - - program - .command("setup") - .description(t("setup.title")) - .allowUnknownOption() - .allowExcessArguments() - .action(legacyAction("setup", runSetupCommand)); - - program - .command("providers") - .description(t("providers.title")) - .allowUnknownOption() - .allowExcessArguments() - .action(legacyAction("providers", runProvidersCommand)); - - program - .command("provider") - .description(t("providers.title")) - .allowUnknownOption() - .allowExcessArguments() - .action(legacyAction("provider", runProviderCommand)); - - program - .command("config") - .description("Show or update CLI tool configuration") - .allowUnknownOption() - .allowExcessArguments() - .action(legacyAction("config", runConfigCommand)); - - program - .command("status") - .description("Show OmniRoute status dashboard") - .allowUnknownOption() - .allowExcessArguments() - .action(legacyAction("status", runStatusCommand)); - - program - .command("logs") - .description("Stream request logs") - .allowUnknownOption() - .allowExcessArguments() - .action(legacyAction("logs", runLogsCommand)); - - program - .command("update") - .description(t("update.checking")) - .allowUnknownOption() - .allowExcessArguments() - .action(legacyAction("update", runUpdateCommand)); + registerDoctor(program); + registerSetup(program); + registerProviders(program); + registerProvider(program); + registerConfig(program); + registerStatus(program); + registerLogs(program); + registerUpdate(program); } diff --git a/bin/cli/commands/setup.mjs b/bin/cli/commands/setup.mjs index 2128b0625d..95418a3a5b 100644 --- a/bin/cli/commands/setup.mjs +++ b/bin/cli/commands/setup.mjs @@ -1,4 +1,3 @@ -import { parseArgs, getStringFlag, hasFlag } from "../args.mjs"; import { createPrompt, printHeading, printInfo, printSuccess } from "../io.mjs"; import { openOmniRouteDb } from "../sqlite.mjs"; import { getSettings, hashManagementPassword, updateSettings } from "../settings-store.mjs"; @@ -9,18 +8,14 @@ import { getProviderDisplayName, resolveProviderChoice, } from "../provider-catalog.mjs"; +import { t } from "../i18n.mjs"; -function wantsProviderSetup(flags) { - return ( - hasFlag(flags, "add-provider") || - Boolean(getStringFlag(flags, "provider", "OMNIROUTE_PROVIDER")) || - Boolean(getStringFlag(flags, "api-key", "OMNIROUTE_API_KEY")) - ); +function wantsProviderSetup(opts) { + return opts.addProvider || Boolean(opts.provider) || Boolean(opts.apiKey); } -async function resolvePassword(flags, prompt, nonInteractive) { - const flagPassword = getStringFlag(flags, "password", "OMNIROUTE_SETUP_PASSWORD"); - if (flagPassword) return flagPassword; +async function resolvePassword(opts, prompt, nonInteractive) { + if (opts.password) return opts.password; if (nonInteractive) return ""; const answer = await prompt.ask("Set an admin password now? [y/N]", "N"); @@ -34,8 +29,8 @@ async function resolvePassword(flags, prompt, nonInteractive) { return password; } -async function setupPassword(db, flags, prompt, nonInteractive) { - const password = await resolvePassword(flags, prompt, nonInteractive); +async function setupPassword(db, opts, prompt, nonInteractive) { + const password = await resolvePassword(opts, prompt, nonInteractive); if (!password) { const settings = getSettings(db); if (!settings.password) { @@ -60,12 +55,12 @@ async function setupPassword(db, flags, prompt, nonInteractive) { return true; } -async function resolveProviderInput(flags, prompt, nonInteractive) { - let provider = getStringFlag(flags, "provider", "OMNIROUTE_PROVIDER"); - let apiKey = getStringFlag(flags, "api-key", "OMNIROUTE_API_KEY"); - let name = getStringFlag(flags, "provider-name", "OMNIROUTE_PROVIDER_NAME"); - const defaultModel = getStringFlag(flags, "default-model", "OMNIROUTE_DEFAULT_MODEL"); - const baseUrl = getStringFlag(flags, "provider-base-url", "OMNIROUTE_PROVIDER_BASE_URL"); +async function resolveProviderInput(opts, prompt, nonInteractive) { + let provider = opts.provider; + let apiKey = opts.apiKey; + let name = opts.providerName; + const defaultModel = opts.defaultModel; + const baseUrl = opts.providerBaseUrl; if (!provider && !nonInteractive) { console.log("Choose a provider:"); @@ -95,19 +90,19 @@ async function resolveProviderInput(flags, prompt, nonInteractive) { }; } -async function setupProvider(db, flags, prompt, nonInteractive) { - if (!wantsProviderSetup(flags) && nonInteractive) return null; +async function setupProvider(db, opts, prompt, nonInteractive) { + if (!wantsProviderSetup(opts) && nonInteractive) return null; - if (!wantsProviderSetup(flags)) { + if (!wantsProviderSetup(opts)) { const answer = await prompt.ask("Add your first provider now? [Y/n]", "Y"); if (/^n(o)?$/i.test(answer)) return null; } - const input = await resolveProviderInput(flags, prompt, nonInteractive); + const input = await resolveProviderInput(opts, prompt, nonInteractive); const connection = upsertApiKeyProviderConnection(db, input); printSuccess(`Provider configured: ${connection.name}`); - if (hasFlag(flags, "test-provider")) { + if (opts.testProvider) { printInfo(`Testing provider connection: ${connection.provider}`); const result = await testProviderApiKey({ provider: input.provider, @@ -127,44 +122,28 @@ async function setupProvider(db, flags, prompt, nonInteractive) { return connection; } -function printSetupHelp() { - console.log(` -Usage: - omniroute setup - omniroute setup --password - omniroute setup --add-provider --provider openai --api-key - omniroute setup --non-interactive - -Options: - --password Set admin password - --add-provider Add an API-key provider connection - --provider Provider id, for example openai or anthropic - --provider-name Display name for the connection - --api-key Provider API key - --default-model Optional default model - --provider-base-url Optional OpenAI-compatible base URL override - --test-provider Test the provider after saving it - --non-interactive Read all inputs from flags/env and do not prompt - -Environment: - OMNIROUTE_SETUP_PASSWORD - OMNIROUTE_PROVIDER - OMNIROUTE_PROVIDER_NAME - OMNIROUTE_PROVIDER_BASE_URL - OMNIROUTE_API_KEY - OMNIROUTE_DEFAULT_MODEL - DATA_DIR -`); +export function registerSetup(program) { + program + .command("setup") + .description(t("setup.title")) + .option("--password ", "Set admin password") + .option("--add-provider", "Add an API-key provider connection") + .option("--provider ", "Provider id, for example openai or anthropic") + .option("--provider-name ", "Display name for the connection") + .option("--api-key ", "Provider API key") + .option("--default-model ", "Optional default model") + .option("--provider-base-url ", "Optional OpenAI-compatible base URL override") + .option("--test-provider", "Test the provider after saving it") + .option("--non-interactive", "Read all inputs from flags/env and do not prompt") + .action(async (opts, cmd) => { + const globalOpts = cmd.optsWithGlobals(); + const exitCode = await runSetupCommand({ ...opts, output: globalOpts.output }); + if (exitCode !== 0) process.exit(exitCode); + }); } -export async function runSetupCommand(argv) { - const { flags } = parseArgs(argv); - if (hasFlag(flags, "help") || hasFlag(flags, "h")) { - printSetupHelp(); - return 0; - } - - const nonInteractive = hasFlag(flags, "non-interactive"); +export async function runSetupCommand(opts = {}) { + const nonInteractive = opts.nonInteractive ?? false; const prompt = createPrompt(); try { @@ -173,8 +152,8 @@ export async function runSetupCommand(argv) { printInfo(`Database: ${dbPath}`); const before = getSettings(db); - const passwordChanged = await setupPassword(db, flags, prompt, nonInteractive); - const providerConnection = await setupProvider(db, flags, prompt, nonInteractive); + const passwordChanged = await setupPassword(db, opts, prompt, nonInteractive); + const providerConnection = await setupProvider(db, opts, prompt, nonInteractive); updateSettings(db, { setupComplete: true }); const after = getSettings(db); diff --git a/bin/cli/commands/status.mjs b/bin/cli/commands/status.mjs index b7d3446aea..263b69b97d 100644 --- a/bin/cli/commands/status.mjs +++ b/bin/cli/commands/status.mjs @@ -1,6 +1,6 @@ -import { parseArgs, getStringFlag, hasFlag } from "../args.mjs"; -import { printHeading, printInfo, printSuccess } from "../io.mjs"; +import { printHeading } from "../io.mjs"; import { resolveDataDir, resolveStoragePath } from "../data-dir.mjs"; +import { t } from "../i18n.mjs"; import path from "node:path"; import fs from "node:fs"; import os from "node:os"; @@ -20,10 +20,21 @@ function formatBytes(bytes) { return `${(bytes / 1048576).toFixed(1)} MB`; } -export async function runStatusCommand(argv) { - const { flags } = parseArgs(argv); - const isJson = hasFlag(flags, "json"); - const isVerbose = hasFlag(flags, "verbose"); +export function registerStatus(program) { + program + .command("status") + .description("Show OmniRoute status dashboard") + .option("-v, --verbose", "Show additional details") + .action(async (opts, cmd) => { + const globalOpts = cmd.optsWithGlobals(); + const exitCode = await runStatusCommand({ ...opts, output: globalOpts.output }); + if (exitCode !== 0) process.exit(exitCode); + }); +} + +export async function runStatusCommand(opts = {}) { + const isJson = opts.output === "json"; + const isVerbose = opts.verbose; const dataDir = resolveDataDir(); const dbPath = resolveStoragePath(dataDir); @@ -72,10 +83,10 @@ export async function runStatusCommand(argv) { if (status.tools) { console.log("\n CLI Tools:"); - for (const t of status.tools) { - const icon = t.configured ? "✓" : t.installed ? "~" : "✗"; + for (const tool of status.tools) { + const icon = tool.configured ? "✓" : tool.installed ? "~" : "✗"; console.log( - ` ${icon} ${t.name.padEnd(14)} ${t.installed ? "installed" : "not installed"}${t.version ? ` (${t.version})` : ""}` + ` ${icon} ${tool.name.padEnd(14)} ${tool.installed ? "installed" : "not installed"}${tool.version ? ` (${tool.version})` : ""}` ); } } diff --git a/bin/cli/commands/update.mjs b/bin/cli/commands/update.mjs index 8b8a394090..2dec6ac0e1 100644 --- a/bin/cli/commands/update.mjs +++ b/bin/cli/commands/update.mjs @@ -1,29 +1,12 @@ -import { parseArgs, getStringFlag, hasFlag } from "../args.mjs"; import { printHeading, printInfo, printSuccess, printError } from "../io.mjs"; import { homedir } from "node:os"; import path from "node:path"; import { execFile } from "node:child_process"; import { promisify } from "node:util"; +import { t } from "../i18n.mjs"; const execFileAsync = promisify(execFile); -function printUpdateHelp() { - console.log(` -Usage: - omniroute update [options] - -Options: - --check Check for available update without applying - --dry-run Show what would be updated without applying - --backup Create backup before updating (default: true) - --no-backup Skip backup creation - --help Show this help - -Environment: - OMNIRoute_AUTO_UPDATE Set to "true" to enable auto-update check on startup -`); -} - async function getCurrentVersion() { try { const { readFileSync } = await import("node:fs"); @@ -77,17 +60,26 @@ async function createBackup() { } } -export async function runUpdateCommand(argv) { - const { flags } = parseArgs(argv); +export function registerUpdate(program) { + program + .command("update") + .description(t("update.checking")) + .option("--check", "Check for available update without applying") + .option("--dry-run", "Show what would be updated without applying") + .option("--no-backup", "Skip backup creation") + .option("--yes", "Skip confirmation prompt") + .action(async (opts, cmd) => { + const globalOpts = cmd.optsWithGlobals(); + const exitCode = await runUpdateCommand({ ...opts, output: globalOpts.output }); + if (exitCode !== 0) process.exit(exitCode); + }); +} - if (hasFlag(flags, "help") || hasFlag(flags, "h")) { - printUpdateHelp(); - return 0; - } - - const checkOnly = hasFlag(flags, "check"); - const dryRun = hasFlag(flags, "dry-run"); - const skipBackup = hasFlag(flags, "no-backup"); +export async function runUpdateCommand(opts = {}) { + const checkOnly = opts.check ?? false; + const dryRun = opts.dryRun ?? false; + const skipBackup = !(opts.backup ?? true); + const skipConfirm = opts.yes ?? false; const current = await getCurrentVersion(); const latest = await getLatestVersion(); @@ -136,7 +128,7 @@ export async function runUpdateCommand(argv) { } } - if (!hasFlag(flags, "yes")) { + if (!skipConfirm) { const readline = await import("node:readline"); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const answer = await new Promise((resolve) => diff --git a/bin/cli/index.mjs b/bin/cli/index.mjs deleted file mode 100644 index a908b54745..0000000000 --- a/bin/cli/index.mjs +++ /dev/null @@ -1,44 +0,0 @@ -import { runDoctorCommand } from "./commands/doctor.mjs"; -import { runProvidersCommand } from "./commands/providers.mjs"; -import { runSetupCommand } from "./commands/setup.mjs"; -import { runConfigCommand } from "./commands/config.mjs"; -import { runStatusCommand } from "./commands/status.mjs"; -import { runLogsCommand } from "./commands/logs.mjs"; -import { runUpdateCommand } from "./commands/update.mjs"; -import { runProviderCommand } from "./commands/provider-cmd.mjs"; - -export async function runCliCommand(command, argv, context = {}) { - if (command === "doctor") { - return runDoctorCommand(argv, context); - } - - if (command === "providers") { - return runProvidersCommand(argv, context); - } - - if (command === "setup") { - return runSetupCommand(argv, context); - } - - if (command === "config") { - return runConfigCommand(argv); - } - - if (command === "status") { - return runStatusCommand(argv); - } - - if (command === "logs") { - return runLogsCommand(argv); - } - - if (command === "update") { - return runUpdateCommand(argv); - } - - if (command === "provider") { - return runProviderCommand(argv); - } - - throw new Error(`Unknown CLI command: ${command}`); -} diff --git a/tests/unit/cli-providers-command.test.ts b/tests/unit/cli-providers-command.test.ts index a39607d2c2..06f1fdd1a8 100644 --- a/tests/unit/cli-providers-command.test.ts +++ b/tests/unit/cli-providers-command.test.ts @@ -50,9 +50,9 @@ async function createProvider(dataDir: string) { test("providers list succeeds with configured providers", async () => { await withProvidersEnv(async (dataDir) => { await createProvider(dataDir); - const { runProvidersCommand } = await import("../../bin/cli/commands/providers.mjs"); + const { runListCommand } = await import("../../bin/cli/commands/providers.mjs"); - const exitCode = await runProvidersCommand(["list", "--json"]); + const exitCode = await runListCommand({ json: true }); assert.equal(exitCode, 0); }); @@ -60,7 +60,7 @@ test("providers list succeeds with configured providers", async () => { test("providers available lists supported provider catalog", async () => { await withProvidersEnv(async () => { - const { runProvidersCommand } = await import("../../bin/cli/commands/providers.mjs"); + const { runAvailableCommand } = await import("../../bin/cli/commands/providers.mjs"); const logs: string[] = []; const originalLog = console.log; console.log = (...args: unknown[]) => { @@ -68,7 +68,7 @@ test("providers available lists supported provider catalog", async () => { }; try { - const exitCode = await runProvidersCommand(["available", "--json", "--search", "openai"]); + const exitCode = await runAvailableCommand({ json: true, search: "openai" }); assert.equal(exitCode, 0); } finally { console.log = originalLog; @@ -91,8 +91,8 @@ test("providers test updates provider status from upstream result", async () => headers: { "Content-Type": "application/json" }, })) as typeof fetch; - const { runProvidersCommand } = await import("../../bin/cli/commands/providers.mjs"); - const exitCode = await runProvidersCommand(["test", "OpenAI CLI"]); + const { runTestCommand } = await import("../../bin/cli/commands/providers.mjs"); + const exitCode = await runTestCommand("OpenAI CLI", {}); assert.equal(exitCode, 0); @@ -139,8 +139,8 @@ test("providers validate fails encrypted API keys without storage key", async () ); db.close(); - const { runProvidersCommand } = await import("../../bin/cli/commands/providers.mjs"); - const exitCode = await runProvidersCommand(["validate", "--json"]); + const { runValidateCommand } = await import("../../bin/cli/commands/providers.mjs"); + const exitCode = await runValidateCommand({ json: true }); assert.equal(exitCode, 1); }); diff --git a/tests/unit/cli-setup-command.test.ts b/tests/unit/cli-setup-command.test.ts index 390bafad81..abfc980052 100644 --- a/tests/unit/cli-setup-command.test.ts +++ b/tests/unit/cli-setup-command.test.ts @@ -41,20 +41,15 @@ test("setup command writes password, setup state, and provider in non-interactiv await withTempEnv(async (dataDir) => { const { runSetupCommand } = await import("../../bin/cli/commands/setup.mjs"); - const exitCode = await runSetupCommand([ - "--non-interactive", - "--password", - "super-secret", - "--add-provider", - "--provider", - "openai", - "--provider-name", - "OpenAI CLI", - "--api-key", - "sk-test", - "--default-model", - "gpt-4o-mini", - ]); + const exitCode = await runSetupCommand({ + nonInteractive: true, + password: "super-secret", + addProvider: true, + provider: "openai", + providerName: "OpenAI CLI", + apiKey: "sk-test", + defaultModel: "gpt-4o-mini", + }); assert.equal(exitCode, 0); @@ -91,7 +86,7 @@ test("setup command can mark onboarding complete without provider in non-interac await withTempEnv(async (dataDir) => { const { runSetupCommand } = await import("../../bin/cli/commands/setup.mjs"); - const exitCode = await runSetupCommand(["--non-interactive", "--password", "super-secret"]); + const exitCode = await runSetupCommand({ nonInteractive: true, password: "super-secret" }); assert.equal(exitCode, 0); @@ -113,14 +108,12 @@ test("setup command disables login when no password is configured", async () => await withTempEnv(async (dataDir) => { const { runSetupCommand } = await import("../../bin/cli/commands/setup.mjs"); - const exitCode = await runSetupCommand([ - "--non-interactive", - "--add-provider", - "--provider", - "openai", - "--api-key", - "sk-test", - ]); + const exitCode = await runSetupCommand({ + nonInteractive: true, + addProvider: true, + provider: "openai", + apiKey: "sk-test", + }); assert.equal(exitCode, 0); @@ -147,15 +140,13 @@ test("setup command can test provider and persist active status", async () => { const { runSetupCommand } = await import("../../bin/cli/commands/setup.mjs"); - const exitCode = await runSetupCommand([ - "--non-interactive", - "--add-provider", - "--provider", - "openai", - "--api-key", - "sk-test", - "--test-provider", - ]); + const exitCode = await runSetupCommand({ + nonInteractive: true, + addProvider: true, + provider: "openai", + apiKey: "sk-test", + testProvider: true, + }); assert.equal(exitCode, 0); assert.equal(calls.length, 1);