mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
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:
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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})` : ""}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user