diff --git a/bin/cli/commands/setup.mjs b/bin/cli/commands/setup.mjs index 95418a3a5b..690c4d9e08 100644 --- a/bin/cli/commands/setup.mjs +++ b/bin/cli/commands/setup.mjs @@ -1,3 +1,5 @@ +import { fileURLToPath } from "node:url"; +import { dirname, resolve } from "node:path"; import { createPrompt, printHeading, printInfo, printSuccess } from "../io.mjs"; import { openOmniRouteDb } from "../sqlite.mjs"; import { getSettings, hashManagementPassword, updateSettings } from "../settings-store.mjs"; @@ -10,6 +12,13 @@ import { } from "../provider-catalog.mjs"; import { t } from "../i18n.mjs"; +const PROJECT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../.."); + +async function getListCliTools() { + const { listCliTools } = await import(`${PROJECT_ROOT}/src/shared/constants/cliTools.ts`); + return listCliTools; +} + function wantsProviderSetup(opts) { return opts.addProvider || Boolean(opts.provider) || Boolean(opts.apiKey); } @@ -135,6 +144,7 @@ export function registerSetup(program) { .option("--provider-base-url ", "Optional OpenAI-compatible base URL override") .option("--test-provider", "Test the provider after saving it") .option("--non-interactive", "Read all inputs from flags/env and do not prompt") + .option("--list", "List all supported CLI tools") .action(async (opts, cmd) => { const globalOpts = cmd.optsWithGlobals(); const exitCode = await runSetupCommand({ ...opts, output: globalOpts.output }); @@ -143,6 +153,22 @@ export function registerSetup(program) { } export async function runSetupCommand(opts = {}) { + if (opts.list) { + const listCliTools = await getListCliTools(); + const tools = listCliTools(); + if (opts.json || opts.output === "json") { + console.log(JSON.stringify(tools, null, 2)); + } else { + printHeading("Supported CLI Tools"); + for (const tool of tools) { + const cmd = tool.defaultCommand || tool.defaultCommands?.[0] || ""; + const cmdStr = cmd ? ` \x1b[2m(${cmd})\x1b[0m` : ""; + console.log(` • ${tool.name}${cmdStr}`); + } + } + return 0; + } + const nonInteractive = opts.nonInteractive ?? false; const prompt = createPrompt(); diff --git a/src/shared/constants/cliTools.ts b/src/shared/constants/cliTools.ts index 2f7353ecdb..067a0d9052 100644 --- a/src/shared/constants/cliTools.ts +++ b/src/shared/constants/cliTools.ts @@ -526,6 +526,22 @@ amp --model "{{model}}" }, }; +// ─── Registry helpers ──────────────────────────────────────────────────────── + +export type CliToolEntry = (typeof CLI_TOOLS)[keyof typeof CLI_TOOLS]; + +/** Returns an ordered list of all registered CLI tools. */ +export function listCliTools(): CliToolEntry[] { + return Object.values(CLI_TOOLS) as CliToolEntry[]; +} + +/** Returns a single tool by id, or undefined if not found. */ +export function getCliTool(id: string): CliToolEntry | undefined { + return (CLI_TOOLS as Record)[id]; +} + +// ─── Provider model mapping helper ─────────────────────────────────────────── + // Get all provider models for mapping dropdown export const getProviderModelsForMapping = (providers) => { const result = []; diff --git a/tests/unit/cli-tools-schema.test.ts b/tests/unit/cli-tools-schema.test.ts new file mode 100644 index 0000000000..ed08167391 --- /dev/null +++ b/tests/unit/cli-tools-schema.test.ts @@ -0,0 +1,62 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +test("CLI_TOOLS registry contains all 17 expected tools", async () => { + const { CLI_TOOLS } = await import("../../src/shared/constants/cliTools.ts"); + const expected = [ + "claude", + "codex", + "opencode", + "cline", + "kilo", + "continue", + "qwen", + "windsurf", + "hermes", + "amp", + "kiro", + "cursor", + "droid", + "antigravity", + "copilot", + "openclaw", + "custom", + ]; + for (const id of expected) { + assert.ok(id in CLI_TOOLS, `Missing tool: ${id}`); + } + assert.equal(Object.keys(CLI_TOOLS).length, expected.length); +}); + +test("Every tool has required fields: id, name, description, configType", async () => { + const { CLI_TOOLS } = await import("../../src/shared/constants/cliTools.ts"); + for (const [key, tool] of Object.entries(CLI_TOOLS)) { + assert.equal(typeof tool.id, "string", `${key}.id must be string`); + assert.equal(tool.id, key, `${key}.id must match its registry key`); + assert.equal(typeof tool.name, "string", `${key}.name must be string`); + assert.ok(tool.name.length > 0, `${key}.name must be non-empty`); + assert.equal(typeof tool.description, "string", `${key}.description must be string`); + assert.equal(typeof tool.configType, "string", `${key}.configType must be string`); + } +}); + +test("listCliTools returns all tools as an array", async () => { + const { listCliTools, CLI_TOOLS } = await import("../../src/shared/constants/cliTools.ts"); + const tools = listCliTools(); + assert.ok(Array.isArray(tools)); + assert.equal(tools.length, Object.keys(CLI_TOOLS).length); + for (const tool of tools) { + assert.equal(typeof tool.id, "string"); + } +}); + +test("getCliTool returns correct tool by id", async () => { + const { getCliTool } = await import("../../src/shared/constants/cliTools.ts"); + const claude = getCliTool("claude"); + assert.ok(claude); + assert.equal(claude.id, "claude"); + assert.equal(claude.name, "Claude Code"); + + const missing = getCliTool("nonexistent"); + assert.equal(missing, undefined); +});