feat(cli): migrar comandos modulares para Commander (Fase 1.2)

Migra os 8 comandos restantes (doctor, setup, providers, config, status,
logs, update, provider) de parseArgs manual para a API do Commander.
Cada arquivo exporta register<Nome>(program) e run*Command(opts = {}).
Deleta bin/cli/index.mjs (substituído por commands/registry.mjs).
Remove dependência de bin/cli/args.mjs em todos os comandos migrados.
Corrige CWE-310 em doctor.mjs: authTagLength explícito no createDecipheriv.
This commit is contained in:
diegosouzapw
2026-05-14 22:38:59 -03:00
parent 31031422d3
commit 4ed8e4e673
12 changed files with 416 additions and 881 deletions

View File

@@ -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 <tool> Show current config for a tool
omniroute config set <tool> [options] Write config for a tool
omniroute config validate <tool> Validate config format without writing
Options:
--base-url <url> OmniRoute API base URL (default: http://localhost:20128/v1)
--api-key <key> API key for the tool
--model <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 <tool>");
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 <tool>");
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 <tool> [options]");
return 1;
}
if (subcommand === "set") {
if (!toolId) {
printError("Tool ID required. Usage: omniroute config set <tool> [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 <tool>");
return 1;
}
if (subcommand === "validate") {
if (!toolId) {
printError("Tool ID required. Usage: omniroute config validate <tool>");
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 <tool>")
.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 <tool>")
.description("Write config for a tool")
.option("--base-url <url>", "OmniRoute API base URL", "http://localhost:20128/v1")
.option("--api-key <key>", "API key for the tool")
.option("--model <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 <tool>")
.description("Validate config format without writing")
.option("--base-url <url>", "OmniRoute API base URL", "http://localhost:20128/v1")
.option("--api-key <key>", "API key for the tool")
.option("--model <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);
});
}

View File

@@ -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> Host for server liveness probing (default: 127.0.0.1)
--liveness-url <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>", "Host for server liveness probing", "127.0.0.1")
.option("--liveness-url <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");

View File

@@ -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 <level> Filter by level (error, warn, info) — comma-separated
--lines <n> Number of lines to fetch (default: 100)
--timeout <ms> Connection timeout in ms (default: 30000)
--base-url <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 <level>", "Filter by level (error,warn,info) — comma-separated")
.option("--lines <n>", "Number of lines to fetch", "100")
.option("--timeout <ms>", "Connection timeout in ms", "30000")
.option("--base-url <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) {

View File

@@ -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 <name> [options] Add a provider connection
omniroute provider list List configured providers
omniroute provider remove <name|id> Remove a provider connection
omniroute provider test <name|id> Test a provider connection
omniroute provider default <name|id> Set default provider
Options:
--provider <id> Provider id (e.g., openai, anthropic, omniroute)
--api-key <key> API key for the provider
--provider-name <name> Display name for the connection
--default-model <model> Default model to use
--base-url <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 <name> — 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 <name>");
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 <name|id>");
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 <name|id>");
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 <name|id>");
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");
});
}

View File

@@ -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 <id|name>
omniroute providers test-all
omniroute providers validate
Options:
--json Print machine-readable JSON
--search, --q <text> Filter available providers by id, name, alias, or category
--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 <text> Filter by id, name, alias, or category
--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 <id|name>
omniroute providers test <id|name> --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 <query>", "Filter by id, name, alias, or category")
.option("-q, --q <query>", "Alias for --search")
.option("--category <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 <idOrName>")
.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);
});
}

View File

@@ -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);
}

View File

@@ -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 <password>
omniroute setup --add-provider --provider openai --api-key <key>
omniroute setup --non-interactive
Options:
--password <value> Set admin password
--add-provider Add an API-key provider connection
--provider <id> Provider id, for example openai or anthropic
--provider-name <name> Display name for the connection
--api-key <value> Provider API key
--default-model <model> Optional default model
--provider-base-url <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 <value>", "Set admin password")
.option("--add-provider", "Add an API-key provider connection")
.option("--provider <id>", "Provider id, for example openai or anthropic")
.option("--provider-name <name>", "Display name for the connection")
.option("--api-key <value>", "Provider API key")
.option("--default-model <model>", "Optional default model")
.option("--provider-base-url <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);

View File

@@ -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})` : ""}`
);
}
}

View File

@@ -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) =>

View File

@@ -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}`);
}

View File

@@ -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);
});

View File

@@ -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);