From 193bf1a7669beefae4e16a9ba2c09572dae254cd Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Wed, 27 May 2026 19:50:38 -0300 Subject: [PATCH] feat(cli-tools): extend catalog with category/vendor/acpSpawnable/baseUrlSupport + new entries (plan 14 F1) - Add CliCatalogEntrySchema (Zod) + CliCatalogEntry type + CliCatalogSchema in src/shared/schemas/cliCatalog.ts - Add ToolBatchStatus + ToolBatchStatusMap interfaces in src/shared/types/cliBatchStatus.ts - Re-export cliBatchStatus from src/shared/types/index.ts - Extend all CLI_TOOLS entries with 4 new fields: category, vendor, acpSpawnable, baseUrlSupport - Add 13 new entries: roo, jcode, deepseek-tui, smelt, pi (code), aider, forge, gemini-cli, cursor-cli (code), goose, interpreter, warp, agent-deck (agent) - Remove windsurf and amp (MITM backlog plan 11, D17) - Result: 19 visible code entries + 6 agent entries (D15 cardinality) - Add 5 new unit tests: cli-catalog-schema, cli-catalog-counts, cli-catalog-newentries, cli-catalog-removed, cli-catalog-acpspawnable - Update existing tests to align with removed entries --- src/shared/constants/cliTools.ts | 490 ++++++++++++++---- src/shared/schemas/cliCatalog.ts | 66 +++ src/shared/types/cliBatchStatus.ts | 18 + src/shared/types/index.ts | 1 + tests/unit/cli-catalog-acpspawnable.test.ts | 74 +++ tests/unit/cli-catalog-counts.test.ts | 98 ++++ tests/unit/cli-catalog-newentries.test.ts | 143 +++++ tests/unit/cli-catalog-removed.test.ts | 45 ++ tests/unit/cli-catalog-schema.test.ts | 87 ++++ tests/unit/cli-runtime-detection.test.ts | 18 +- tests/unit/cli-tools-schema.test.ts | 30 +- tests/unit/cli-tools.test.ts | 24 +- ...t40-opencode-cli-tools-integration.test.ts | 22 +- 13 files changed, 968 insertions(+), 148 deletions(-) create mode 100644 src/shared/schemas/cliCatalog.ts create mode 100644 src/shared/types/cliBatchStatus.ts create mode 100644 tests/unit/cli-catalog-acpspawnable.test.ts create mode 100644 tests/unit/cli-catalog-counts.test.ts create mode 100644 tests/unit/cli-catalog-newentries.test.ts create mode 100644 tests/unit/cli-catalog-removed.test.ts create mode 100644 tests/unit/cli-catalog-schema.test.ts diff --git a/src/shared/constants/cliTools.ts b/src/shared/constants/cliTools.ts index a9d49bb07c..feb478ecd4 100644 --- a/src/shared/constants/cliTools.ts +++ b/src/shared/constants/cliTools.ts @@ -1,17 +1,22 @@ // CLI Tools configuration import { getClaudeCodeDefaultModels } from "@omniroute/open-sse/config/providerRegistry"; +import type { CliCatalogEntry } from "@/shared/schemas/cliCatalog"; const _cc = getClaudeCodeDefaultModels(); -export const CLI_TOOLS = { +export const CLI_TOOLS: Record = { claude: { id: "claude", name: "Claude Code", icon: "terminal", color: "#D97757", - description: "Anthropic Claude Code CLI", + description: "Anthropic Claude Code CLI — ANTHROPIC_BASE_URL points to OmniRoute", docsUrl: "https://docs.anthropic.com/en/docs/claude-code/overview", configType: "env", + category: "code", + vendor: "Anthropic", + acpSpawnable: true, + baseUrlSupport: "full", envVars: { baseUrl: "ANTHROPIC_BASE_URL", model: "ANTHROPIC_MODEL", @@ -66,9 +71,13 @@ export const CLI_TOOLS = { id: "codex", name: "OpenAI Codex CLI", color: "#10A37F", - description: "OpenAI Codex CLI", + description: "OpenAI Codex CLI — OpenAI-compatible base URL targets OmniRoute", docsUrl: "https://github.com/openai/codex", configType: "custom", + category: "code", + vendor: "OpenAI", + acpSpawnable: true, + baseUrlSupport: "full", defaultCommand: "codex", }, droid: { @@ -76,9 +85,13 @@ export const CLI_TOOLS = { name: "Factory Droid", image: "/providers/droid.svg", color: "#00D4FF", - description: "Factory Droid AI Assistant", + description: "Factory AI Droid — BYOK assistant with configurable endpoint", docsUrl: "/docs?section=cli-tools&tool=droid", configType: "custom", + category: "code", + vendor: "Factory AI", + acpSpawnable: false, + baseUrlSupport: "partial", defaultCommand: "droid", }, openclaw: { @@ -86,9 +99,13 @@ export const CLI_TOOLS = { name: "Open Claw", image: "/providers/openclaw.png", color: "#FF6B35", - description: "Open Claw AI Assistant", + description: "Open Claw — open-source multi-backend agent CLI (OSS, P. Steinberger)", docsUrl: "/docs?section=cli-tools&tool=openclaw", configType: "custom", + category: "agent", + vendor: "OSS (P. Steinberger)", + acpSpawnable: true, + baseUrlSupport: "full", defaultCommand: "openclaw", }, cursor: { @@ -96,9 +113,15 @@ export const CLI_TOOLS = { name: "Cursor", image: "/providers/cursor.png", color: "#000000", - description: "Cursor AI Code Editor", + // Cursor App routes via its own cloud server — local base URL not supported. + // Use cursor-cli entry for headless/agent CLI mode with custom endpoint. + description: "Cursor AI Code Editor — Cloud Endpoint required (use cursor-cli for CLI mode)", docsUrl: "https://docs.cursor.com/settings/models", configType: "guide", + category: "code", + vendor: "Anysphere", + acpSpawnable: false, + baseUrlSupport: "none", requiresCloud: true, defaultCommands: ["agent", "cursor"], notes: [ @@ -117,42 +140,17 @@ export const CLI_TOOLS = { { step: 6, title: "Select Model", type: "modelSelector" }, ], }, - windsurf: { - id: "windsurf", - name: "Windsurf", - color: "#4A90E2", - description: "Windsurf AI-first IDE by Codeium", - docsUrl: "https://windsurf.com/", - configType: "guide", - notes: [ - { - type: "warning", - text: "Official Windsurf docs currently describe BYOK for select Claude models plus enterprise URL/token settings, not a generic custom OpenAI-compatible provider.", - }, - ], - guideSteps: [ - { - step: 1, - title: "Open AI Settings", - desc: "Click the AI Settings icon in Windsurf or go to Settings", - }, - { - step: 2, - title: "Add Custom Provider", - desc: 'Select "Add custom provider" (OpenAI-compatible)', - }, - { step: 3, title: "Base URL", value: "{{baseUrl}}", copyable: true }, - { step: 4, title: "API Key", type: "apiKeySelector" }, - { step: 5, title: "Select Model", type: "modelSelector" }, - ], - }, cline: { id: "cline", name: "Cline", color: "#00D1B2", - description: "Cline AI Coding Assistant CLI", + description: "Cline — open-source VS Code coding agent with OpenAI-compatible base URL", docsUrl: "https://docs.cline.bot/", configType: "custom", + category: "code", + vendor: "OSS", + acpSpawnable: true, + baseUrlSupport: "full", defaultCommand: "cline", }, kilo: { @@ -160,9 +158,13 @@ export const CLI_TOOLS = { name: "Kilo Code", image: "/providers/kilocode.svg", color: "#FF6B6B", - description: "Kilo Code AI Assistant CLI", + description: "Kilo Code — VS Code AI assistant with custom base URL support", docsUrl: "/docs?section=cli-tools&tool=kilocode", configType: "custom", + category: "code", + vendor: "Kilo-Org", + acpSpawnable: false, + baseUrlSupport: "full", defaultCommand: "kilocode", }, continue: { @@ -170,9 +172,13 @@ export const CLI_TOOLS = { name: "Continue", image: "/providers/continue.png", color: "#7C3AED", - description: "Continue AI Assistant", + description: "Continue — open-source AI coding assistant with full provider config", docsUrl: "https://docs.continue.dev/", configType: "guide", + category: "code", + vendor: "continue.dev", + acpSpawnable: false, + baseUrlSupport: "full", guideSteps: [ { step: 1, title: "Open Config", desc: "Open Continue configuration file" }, { step: 2, title: "API Key", type: "apiKeySelector" }, @@ -198,9 +204,15 @@ export const CLI_TOOLS = { id: "antigravity", name: "Antigravity", color: "#4285F4", - description: "Google Antigravity IDE with MITM", + description: "Google Antigravity IDE — MITM intercept required (plan 11 backlog)", docsUrl: "/docs?section=cli-tools&tool=antigravity", + // configType:"mitm" — fluxo MITM; baseUrlSupport:"none" → excluído das listas, + // acessível só via legacy /[id] route após F8 configType: "mitm", + category: "code", + vendor: "Google", + acpSpawnable: false, + baseUrlSupport: "none", modelAliases: [ "claude-opus-4-6-thinking", "claude-sonnet-4-6", @@ -231,9 +243,14 @@ export const CLI_TOOLS = { name: "GitHub Copilot", image: "/providers/copilot.png", color: "#1F6FEB", - description: "GitHub Copilot Chat — VS Code Extension", + // D-nota: copilot suporta COPILOT_PROVIDER_BASE_URL desde v1.0.19+ + description: "GitHub Copilot Chat — VS Code extension with COPILOT_PROVIDER_BASE_URL support", docsUrl: "https://code.visualstudio.com/docs/copilot/overview", configType: "custom", + category: "code", + vendor: "GitHub / Microsoft", + acpSpawnable: false, + baseUrlSupport: "full", }, opencode: { id: "opencode", @@ -242,9 +259,13 @@ export const CLI_TOOLS = { imageDark: "/providers/opencode-dark.svg", icon: "terminal", color: "#FF6B35", - description: "OpenCode AI coding agent (Terminal)", + description: "OpenCode — AI coding agent CLI by Anomaly (terminal, multi-provider)", docsUrl: "/docs?section=cli-tools&tool=opencode", configType: "guide", + category: "code", + vendor: "Anomaly", + acpSpawnable: true, + baseUrlSupport: "full", defaultCommand: "opencode", modelSelectionMode: "multiple", hideComboModels: true, @@ -293,14 +314,22 @@ export const CLI_TOOLS = { }`, }, }, + // hermes (simple guide) — category: "code", baseUrlSupport: "none" + // Excluded from the CLI Code's list (not in D15 19-entry list). + // The advanced multi-role agent is "hermes-agent" (category: "agent", baseUrlSupport: "full"). + // Legacy /[id] route still renders this card after F8. hermes: { id: "hermes", name: "Hermes", icon: "terminal", color: "#8B5CF6", - description: "Hermes coding agent quick configuration", + description: "Nous Research Hermes — generic OpenAI-compatible setup (use hermes-agent for full agent)", docsUrl: "/docs?section=cli-tools&tool=hermes", configType: "guide", + category: "code", + vendor: "Nous Research", + acpSpawnable: false, + baseUrlSupport: "none", defaultCommand: "hermes", guideSteps: [ { @@ -337,65 +366,30 @@ export const CLI_TOOLS = { name: "Hermes Agent", icon: "terminal", color: "#8B5CF6", - description: "Hermes Agent (by Nousresearch) — advanced multi-role terminal AI", + description: "Hermes Agent (Nous Research) — advanced multi-role autonomous terminal AI", docsUrl: "/docs?section=cli-tools&tool=hermes-agent", configType: "custom", + category: "agent", + vendor: "Nous Research", + acpSpawnable: false, + baseUrlSupport: "full", defaultCommand: "hermes", }, - amp: { - id: "amp", - name: "Amp CLI", - icon: "terminal", - color: "#F97316", - description: "Sourcegraph Amp coding assistant CLI", - docsUrl: "/docs?section=cli-tools&tool=amp", - configType: "guide", - defaultCommand: "amp", - modelAliases: ["g25p", "g25f", "cs45", "g54"], - notes: [ - { - type: "info", - text: "Use OmniRoute model aliases to keep Amp shorthand mappings stable across provider updates.", - }, - { - type: "warning", - text: "Suggested shorthand examples: g25p → gemini/gemini-2.5-pro, g25f → gemini/gemini-2.5-flash, cs45 → cc/claude-sonnet-4-5-20250929.", - }, - ], - guideSteps: [ - { - step: 1, - title: "Install Amp", - desc: "Install the Amp CLI using the package manager supported by your environment.", - }, - { step: 2, title: "API Key", type: "apiKeySelector" }, - { step: 3, title: "Base URL", value: "{{baseUrl}}", copyable: true }, - { step: 4, title: "Select Model", type: "modelSelector" }, - { - step: 5, - title: "Add Shorthands", - desc: "Map Amp shorthand names such as g25p or cs45 to OmniRoute aliases in your local config.", - }, - ], - codeBlock: { - language: "bash", - code: `export OPENAI_API_KEY="{{apiKey}}" -export OPENAI_BASE_URL="{{baseUrl}}" -amp --model "{{model}}" -# Example shorthand aliases you can map locally: -# g25p -> gemini/gemini-2.5-pro -# cs45 -> cc/claude-sonnet-4-5-20250929`, - }, - }, kiro: { id: "kiro", name: "Kiro AI", image: "/providers/kiro.svg", icon: "psychology_alt", color: "#FF6B35", - description: "Amazon Kiro — AI-powered IDE with MITM", + description: "Amazon Kiro — AI-powered IDE with MITM intercept (plan 11 backlog)", docsUrl: "/docs?section=cli-tools&tool=kiro", + // configType:"mitm" — fluxo MITM; baseUrlSupport:"none" → excluído das listas, + // acessível só via legacy /[id] route após F8 configType: "mitm", + category: "code", + vendor: "Amazon", + acpSpawnable: false, + baseUrlSupport: "none", guideSteps: [ { step: 1, title: "Open Kiro Settings", desc: "Go to Settings → AI Provider" }, { step: 2, title: "Base URL", value: "{{baseUrl}}", copyable: true }, @@ -412,6 +406,10 @@ amp --model "{{model}}" "Alibaba Qwen Code CLI — supports OpenAI, Anthropic & Gemini providers via OmniRoute", docsUrl: "https://qwenlm.github.io/qwen-code-docs/en/users/configuration/model-providers/", configType: "guide", + category: "code", + vendor: "Alibaba", + acpSpawnable: true, + baseUrlSupport: "full", defaultCommand: "qwen", notes: [ { @@ -536,28 +534,332 @@ amp --model "{{model}}" description: "Generic OpenAI-compatible CLI or SDK configuration generator", docsUrl: "/docs?section=cli-tools", configType: "custom-builder", + category: "code", + vendor: "Custom", + acpSpawnable: false, + baseUrlSupport: "full", + }, + + // ── Code entries — aider ────────────────────────────────────────────────── + aider: { + id: "aider", + name: "Aider", + icon: "terminal", + color: "#2DD4BF", + description: "Aider AI pair-programming CLI — OpenAI-compatible --openai-api-base flag", + docsUrl: "https://aider.chat/docs/config/options.html", + configType: "guide", + category: "code", + vendor: "OSS (P. Gauthier)", + acpSpawnable: true, + baseUrlSupport: "full", + defaultCommand: "aider", + guideSteps: [ + { step: 1, title: "Install Aider", desc: "pip install aider-chat" }, + { step: 2, title: "API Key", type: "apiKeySelector" }, + { step: 3, title: "Base URL", value: "{{baseUrl}}", copyable: true }, + { step: 4, title: "Select Model", type: "modelSelector" }, + ], + codeBlock: { + language: "bash", + code: `export OPENAI_API_KEY="{{apiKey}}" +aider --openai-api-base "{{baseUrl}}" --model "{{model}}"`, + }, + }, + + // ── Code entries — forge ────────────────────────────────────────────────── + forge: { + id: "forge", + name: "ForgeCode", + icon: "terminal", + color: "#F97316", + description: "ForgeCode coding agent CLI — custom provider via .forge.toml", + docsUrl: "https://github.com/antinomyhq/forge", + configType: "custom", + category: "code", + vendor: "Antinomy HQ", + acpSpawnable: true, + baseUrlSupport: "full", + defaultCommand: "forge", + }, + + // ── Code entries — gemini-cli ───────────────────────────────────────────── + "gemini-cli": { + id: "gemini-cli", + name: "Google Gemini CLI", + icon: "terminal", + color: "#4285F4", + description: "Google Gemini CLI — OpenAI-compatible base URL via GEMINI_API_BASE_URL env", + docsUrl: "https://github.com/google-gemini/gemini-cli", + configType: "guide", + category: "code", + vendor: "Google", + acpSpawnable: true, + baseUrlSupport: "partial", + defaultCommand: "gemini", + guideSteps: [ + { + step: 1, + title: "Install Gemini CLI", + desc: "npm install -g @google/gemini-cli", + }, + { step: 2, title: "API Key", type: "apiKeySelector" }, + { step: 3, title: "Base URL", value: "{{baseUrl}}", copyable: true }, + { step: 4, title: "Select Model", type: "modelSelector" }, + ], + codeBlock: { + language: "bash", + code: `export GEMINI_API_KEY="{{apiKey}}" +export GEMINI_API_BASE_URL="{{baseUrl}}" +gemini --model "{{model}}"`, + }, + }, + + // ── Code entries — cursor-cli ───────────────────────────────────────────── + "cursor-cli": { + id: "cursor-cli", + name: "Cursor Agent CLI", + icon: "terminal", + color: "#000000", + description: "Cursor Agent CLI — headless agent mode with custom provider endpoint", + docsUrl: "https://docs.cursor.com/advanced/api", + configType: "guide", + category: "code", + vendor: "Anysphere", + acpSpawnable: true, + baseUrlSupport: "partial", + defaultCommand: "cursor", + guideSteps: [ + { step: 1, title: "Install Cursor CLI", desc: "Download cursor binary from cursor.com" }, + { step: 2, title: "API Key", type: "apiKeySelector" }, + { step: 3, title: "Base URL", value: "{{baseUrl}}", copyable: true }, + { step: 4, title: "Select Model", type: "modelSelector" }, + ], + }, + + // ── Code entries — new ★ ────────────────────────────────────────────────── + + /** ★ Added by plan 14 (CLI Pages Redesign) — 2026-05-27 */ + roo: { + id: "roo", + name: "Roo Code", + icon: "terminal", + color: "#7C3AED", + description: "Roo Code AI Assistant — VS Code extension with OpenAI-compatible custom base URL", + docsUrl: "https://docs.roocode.com/", + configType: "guide", + category: "code", + vendor: "Roo (OSS)", + acpSpawnable: false, + baseUrlSupport: "full", + guideSteps: [ + { step: 1, title: "Install Roo Code", desc: "Install the Roo Code VS Code extension" }, + { step: 2, title: "API Key", type: "apiKeySelector" }, + { step: 3, title: "Base URL", value: "{{baseUrl}}", copyable: true }, + { step: 4, title: "Select Model", type: "modelSelector" }, + ], + }, + + /** ★ Added by plan 14 (CLI Pages Redesign) — 2026-05-27 */ + jcode: { + id: "jcode", + name: "jcode", + icon: "terminal", + color: "#10B981", + description: "jcode terminal coding agent — OpenAI-compatible CLI by 1jehuang", + docsUrl: "https://github.com/1jehuang/jcode", + configType: "custom", + category: "code", + vendor: "OSS (1jehuang)", + acpSpawnable: false, + baseUrlSupport: "full", + defaultCommand: "jcode", + }, + + /** ★ Added by plan 14 (CLI Pages Redesign) — 2026-05-27 */ + "deepseek-tui": { + id: "deepseek-tui", + name: "DeepSeek TUI", + icon: "terminal", + color: "#4F46E5", + description: "DeepSeek TUI — Rust-based coding agent CLI with OPENAI_BASE_URL support", + docsUrl: "https://github.com/hunterbown/deepseek-tui", + configType: "custom", + category: "code", + vendor: "OSS (Hunter Bown)", + acpSpawnable: false, + baseUrlSupport: "full", + defaultCommand: "deepseek-tui", + }, + + /** ★ Added by plan 14 (CLI Pages Redesign) — 2026-05-27 */ + smelt: { + id: "smelt", + name: "Smelt", + icon: "terminal", + color: "#EF4444", + description: "Smelt coding agent CLI — OpenAI-compatible agent by leonardcser", + docsUrl: "https://github.com/leonardcser/smelt", + configType: "custom", + category: "code", + vendor: "OSS (leonardcser)", + acpSpawnable: false, + baseUrlSupport: "full", + defaultCommand: "smelt", + }, + + /** ★ Added by plan 14 (CLI Pages Redesign) — 2026-05-27 */ + pi: { + id: "pi", + name: "Pi", + icon: "terminal", + color: "#F59E0B", + description: "Pi coding agent CLI — lightweight terminal AI by M. Zechner", + docsUrl: "https://github.com/badlogic/pi", + configType: "custom", + category: "code", + vendor: "OSS (M. Zechner)", + acpSpawnable: false, + baseUrlSupport: "full", + defaultCommand: "pi", + }, + + // ── Agent entries ───────────────────────────────────────────────────────── + + /** ★ Added by plan 14 (CLI Pages Redesign) — 2026-05-27 */ + goose: { + id: "goose", + name: "Goose", + icon: "smart_toy", + color: "#F97316", + description: "Goose autonomous agent CLI — Block / Linux Foundation OSS, full base URL", + docsUrl: "https://block.github.io/goose/", + configType: "guide", + category: "agent", + vendor: "Block / Linux Foundation", + acpSpawnable: true, + baseUrlSupport: "full", + defaultCommand: "goose", + guideSteps: [ + { step: 1, title: "Install Goose", desc: "pip install goose-ai or brew install goose" }, + { step: 2, title: "API Key", type: "apiKeySelector" }, + { step: 3, title: "Base URL", value: "{{baseUrl}}", copyable: true }, + { step: 4, title: "Select Model", type: "modelSelector" }, + ], + codeBlock: { + language: "yaml", + code: `# ~/.config/goose/config.yaml +GOOSE_PROVIDER: "openai" +GOOSE_MODEL: "{{model}}" +OPENAI_HOST: "{{baseUrl}}" +OPENAI_API_KEY: "{{apiKey}}"`, + }, + }, + + /** ★ Added by plan 14 (CLI Pages Redesign) — 2026-05-27 */ + interpreter: { + id: "interpreter", + name: "Open Interpreter", + icon: "smart_toy", + color: "#8B5CF6", + description: "Open Interpreter — autonomous coding agent CLI with --api_base flag", + docsUrl: "https://docs.openinterpreter.com/", + configType: "guide", + category: "agent", + vendor: "OSS", + acpSpawnable: true, + baseUrlSupport: "full", + defaultCommand: "interpreter", + guideSteps: [ + { step: 1, title: "Install", desc: "pip install open-interpreter" }, + { step: 2, title: "API Key", type: "apiKeySelector" }, + { step: 3, title: "Base URL", value: "{{baseUrl}}", copyable: true }, + { step: 4, title: "Select Model", type: "modelSelector" }, + ], + codeBlock: { + language: "bash", + code: `interpreter --api_base "{{baseUrl}}" --api_key "{{apiKey}}" --model "{{model}}"`, + }, + }, + + /** ★ Added by plan 14 (CLI Pages Redesign) — 2026-05-27 */ + warp: { + id: "warp", + name: "Warp AI", + icon: "terminal", + color: "#1D4ED8", + description: "Warp AI terminal — BYOK desktop app with partial base URL support", + docsUrl: "https://docs.warp.dev/", + configType: "guide", + category: "agent", + vendor: "Warp Inc.", + acpSpawnable: true, + baseUrlSupport: "partial", + guideSteps: [ + { step: 1, title: "Install Warp", desc: "Download Warp from warp.dev (desktop app)" }, + { step: 2, title: "API Key", type: "apiKeySelector" }, + { step: 3, title: "Configure BYOK", desc: "Go to Settings → AI → BYOK Provider" }, + { step: 3, title: "Base URL", value: "{{baseUrl}}", copyable: true }, + { step: 4, title: "Select Model", type: "modelSelector" }, + ], + notes: [ + { + type: "warning", + text: "Warp is a desktop app, not a CLI binary. baseUrlSupport is partial — some models may require the native Warp endpoint.", + }, + ], + }, + + /** ★ Added by plan 14 (CLI Pages Redesign) — 2026-05-27 */ + "agent-deck": { + id: "agent-deck", + name: "Agent Deck", + icon: "device_hub", + color: "#0EA5E9", + description: "Agent Deck — multi-agent stdio backend orchestrator (OSS, asheshgoplani)", + docsUrl: "https://github.com/asheshgoplani/agent-deck", + configType: "guide", + category: "agent", + vendor: "OSS (asheshgoplani)", + acpSpawnable: false, + baseUrlSupport: "full", + defaultCommand: "agent-deck", + guideSteps: [ + { step: 1, title: "Install Agent Deck", desc: "npm install -g agent-deck" }, + { step: 2, title: "API Key", type: "apiKeySelector" }, + { step: 3, title: "Base URL", value: "{{baseUrl}}", copyable: true }, + { step: 4, title: "Select Model", type: "modelSelector" }, + ], }, }; // ─── Registry helpers ──────────────────────────────────────────────────────── -export type CliToolEntry = (typeof CLI_TOOLS)[keyof typeof CLI_TOOLS]; +export type CliToolEntry = CliCatalogEntry; /** Returns an ordered list of all registered CLI tools. */ export function listCliTools(): CliToolEntry[] { - return Object.values(CLI_TOOLS) as CliToolEntry[]; + return Object.values(CLI_TOOLS); } /** Returns a single tool by id, or undefined if not found. */ export function getCliTool(id: string): CliToolEntry | undefined { - return (CLI_TOOLS as Record)[id]; + return CLI_TOOLS[id]; } // ─── Provider model mapping helper ─────────────────────────────────────────── // Get all provider models for mapping dropdown -export const getProviderModelsForMapping = (providers) => { - const result = []; +export const getProviderModelsForMapping = (providers: Array<{ + id: string; + isActive: boolean; + testStatus: string; + provider: string; + name: string; + models?: string[]; +}>) => { + const result: Array<{ connectionId: string; provider: string; name: string; models: string[] }> = + []; providers.forEach((conn) => { if (conn.isActive && (conn.testStatus === "active" || conn.testStatus === "success")) { result.push({ diff --git a/src/shared/schemas/cliCatalog.ts b/src/shared/schemas/cliCatalog.ts new file mode 100644 index 0000000000..6fb8f0acb0 --- /dev/null +++ b/src/shared/schemas/cliCatalog.ts @@ -0,0 +1,66 @@ +import { z } from "zod"; + +export const CliCatalogEntrySchema = z.object({ + category: z.enum(["code", "agent"]), + vendor: z.string().min(1), + acpSpawnable: z.boolean(), + baseUrlSupport: z.enum(["full", "partial", "none"]), + + id: z.string().min(1), + name: z.string().min(1), + icon: z.string().optional(), + image: z.string().optional(), + imageLight: z.string().optional(), + imageDark: z.string().optional(), + color: z.string().regex(/^#[0-9A-Fa-f]{6}$/), + description: z.string().min(1), + docsUrl: z.string().min(1), + configType: z.enum(["env", "custom", "guide", "custom-builder", "mitm"]), + envVars: z.record(z.string()).optional(), + modelAliases: z.array(z.string()).optional(), + settingsFile: z.string().optional(), + defaultCommand: z.string().optional(), + defaultCommands: z.array(z.string()).optional(), + defaultModels: z + .array( + z.object({ + id: z.string(), + name: z.string(), + alias: z.string(), + envKey: z.string().optional(), + defaultValue: z.string().optional(), + isTopLevel: z.boolean().optional(), + }) + ) + .optional(), + guideSteps: z + .array( + z.object({ + step: z.number().int().positive(), + title: z.string(), + desc: z.string().optional(), + value: z.string().optional(), + copyable: z.boolean().optional(), + type: z.enum(["apiKeySelector", "modelSelector"]).optional(), + }) + ) + .optional(), + codeBlock: z.object({ language: z.string(), code: z.string() }).optional(), + notes: z + .array( + z.object({ type: z.enum(["info", "warning", "error", "cloudCheck"]), text: z.string() }) + ) + .optional(), + requiresCloud: z.boolean().optional(), + modelSelectionMode: z.enum(["single", "multiple"]).optional(), + hideComboModels: z.boolean().optional(), + previewConfigMode: z.string().optional(), +}); + +export type CliCatalogEntry = z.infer; + +export const CliCatalogSchema = z.record(CliCatalogEntrySchema); + +/** Cardinalidade obrigatória (Plano §3.1/§3.2 + D15). */ +export const EXPECTED_CODE_COUNT = 19; +export const EXPECTED_AGENT_COUNT = 6; diff --git a/src/shared/types/cliBatchStatus.ts b/src/shared/types/cliBatchStatus.ts new file mode 100644 index 0000000000..79df281201 --- /dev/null +++ b/src/shared/types/cliBatchStatus.ts @@ -0,0 +1,18 @@ +export interface ToolBatchStatus { + detection: { + installed: boolean; + runnable: boolean; + version?: string; + command?: string; + commandPath?: string; + reason?: string; + }; + config: { + status: "configured" | "not_configured" | "not_installed" | "unknown" | "other"; + endpoint?: string | null; + lastConfiguredAt?: string | null; + }; + error?: string; +} + +export type ToolBatchStatusMap = Record; diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts index 0b33b08f1b..d7b913f404 100644 --- a/src/shared/types/index.ts +++ b/src/shared/types/index.ts @@ -1,2 +1,3 @@ export * from "./pagination"; export * from "./utilization"; +export * from "./cliBatchStatus"; diff --git a/tests/unit/cli-catalog-acpspawnable.test.ts b/tests/unit/cli-catalog-acpspawnable.test.ts new file mode 100644 index 0000000000..fe9a951164 --- /dev/null +++ b/tests/unit/cli-catalog-acpspawnable.test.ts @@ -0,0 +1,74 @@ +/** + * F1: cli-catalog-acpspawnable.test.ts + * Assert acpSpawnable values per plan 14 D16. + */ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { CLI_TOOLS } = await import("../../src/shared/constants/cliTools.ts"); + +// Per D16: acpSpawnable: true for tools that also appear in ACP Agents +const ACP_SPAWNABLE_IDS = [ + "codex", + "claude", + "goose", + "gemini-cli", + "openclaw", + "aider", + "opencode", + "cline", + "qwen", + "forge", + "interpreter", + "cursor-cli", + "warp", +]; + +for (const id of ACP_SPAWNABLE_IDS) { + test(`'${id}' has acpSpawnable === true (in ACP Agents badge)`, () => { + const entry = CLI_TOOLS[id]; + assert.ok(entry, `Entry '${id}' must exist in CLI_TOOLS`); + assert.equal( + entry.acpSpawnable, + true, + `Expected CLI_TOOLS['${id}'].acpSpawnable to be true, got ${entry.acpSpawnable}` + ); + }); +} + +// Tools that should NOT be acpSpawnable +const NOT_ACP_SPAWNABLE_IDS = [ + "copilot", + "droid", + "kilo", + "continue", + "roo", + "jcode", + "deepseek-tui", + "smelt", + "pi", + "hermes-agent", + "agent-deck", + "custom", +]; + +for (const id of NOT_ACP_SPAWNABLE_IDS) { + test(`'${id}' has acpSpawnable === false`, () => { + const entry = CLI_TOOLS[id]; + assert.ok(entry, `Entry '${id}' must exist in CLI_TOOLS`); + assert.equal( + entry.acpSpawnable, + false, + `Expected CLI_TOOLS['${id}'].acpSpawnable to be false, got ${entry.acpSpawnable}` + ); + }); +} + +// windsurf was removed — should not exist +test("windsurf is not in CLI_TOOLS (removed per D17)", () => { + assert.equal( + (CLI_TOOLS as Record)["windsurf"], + undefined, + "windsurf must not be in CLI_TOOLS (removed per plan 14 D17)" + ); +}); diff --git a/tests/unit/cli-catalog-counts.test.ts b/tests/unit/cli-catalog-counts.test.ts new file mode 100644 index 0000000000..4179fc25fc --- /dev/null +++ b/tests/unit/cli-catalog-counts.test.ts @@ -0,0 +1,98 @@ +/** + * F1: cli-catalog-counts.test.ts + * Assert catalog cardinality per plan 14 D15 / §3.1-§3.2. + */ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { CLI_TOOLS } = await import("../../src/shared/constants/cliTools.ts"); +const { EXPECTED_CODE_COUNT, EXPECTED_AGENT_COUNT } = await import( + "../../src/shared/schemas/cliCatalog.ts" +); + +const all = Object.values(CLI_TOOLS); +const codeAll = all.filter((t) => t.category === "code"); +const agentAll = all.filter((t) => t.category === "agent"); +const codeVisible = codeAll.filter((t) => t.baseUrlSupport !== "none"); + +test(`CLI_TOOLS has exactly ${EXPECTED_CODE_COUNT} code entries with baseUrlSupport !== 'none'`, () => { + assert.equal( + codeVisible.length, + EXPECTED_CODE_COUNT, + `Expected ${EXPECTED_CODE_COUNT} visible code entries, got ${codeVisible.length}: ${codeVisible.map((t) => t.id).join(", ")}` + ); +}); + +test(`CLI_TOOLS has exactly ${EXPECTED_AGENT_COUNT} agent entries`, () => { + assert.equal( + agentAll.length, + EXPECTED_AGENT_COUNT, + `Expected ${EXPECTED_AGENT_COUNT} agent entries, got ${agentAll.length}: ${agentAll.map((t) => t.id).join(", ")}` + ); +}); + +test("CLI_TOOLS total code entries (including none) equals 23 (19 visible + 4 none)", () => { + // code-none entries: antigravity, kiro, cursor (app), hermes (simple guide) + const codeNone = codeAll.filter((t) => t.baseUrlSupport === "none"); + assert.equal( + codeNone.length, + 4, + `Expected 4 code entries with baseUrlSupport='none', got ${codeNone.length}: ${codeNone.map((t) => t.id).join(", ")}` + ); + assert.equal( + codeAll.length, + 23, + `Expected 23 total code entries, got ${codeAll.length}` + ); +}); + +test("CLI_TOOLS total (code + agent) = 29", () => { + assert.equal(all.length, 29, `Expected 29 total entries, got ${all.length}`); +}); + +test("All code-none entries have configType mitm OR are legacy excluded entries", () => { + const codeNone = codeAll.filter((t) => t.baseUrlSupport === "none"); + const allowedIds = new Set(["antigravity", "kiro", "cursor", "hermes"]); + for (const entry of codeNone) { + assert.ok( + allowedIds.has(entry.id), + `Unexpected code entry with baseUrlSupport='none': ${entry.id}` + ); + } +}); + +test("All agent entries have baseUrlSupport 'full' or 'partial' (no agent is 'none')", () => { + for (const entry of agentAll) { + assert.notEqual( + entry.baseUrlSupport, + "none", + `Agent entry '${entry.id}' has unexpected baseUrlSupport='none'` + ); + } +}); + +test("The 19 visible code entries match D15 list exactly", () => { + const d15List = new Set([ + "claude", "codex", "cline", "kilo", "roo", "continue", "qwen", + "aider", "forge", "jcode", "deepseek-tui", "opencode", "droid", + "copilot", "gemini-cli", "cursor-cli", "smelt", "pi", "custom", + ]); + const visibleIds = new Set(codeVisible.map((t) => t.id)); + for (const id of d15List) { + assert.ok(visibleIds.has(id), `D15 entry '${id}' not found in visible code list`); + } + for (const id of visibleIds) { + assert.ok(d15List.has(id), `Visible code entry '${id}' not in D15 list`); + } +}); + +test("The 6 agent entries match D15 list exactly", () => { + const d15Agents = new Set(["hermes-agent", "openclaw", "goose", "interpreter", "warp", "agent-deck"]); + const agentIds = new Set(agentAll.map((t) => t.id)); + for (const id of d15Agents) { + assert.ok(agentIds.has(id), `D15 agent '${id}' not found in agent entries`); + } + for (const id of agentIds) { + assert.ok(d15Agents.has(id), `Agent entry '${id}' not in D15 agent list`); + } +}); diff --git a/tests/unit/cli-catalog-newentries.test.ts b/tests/unit/cli-catalog-newentries.test.ts new file mode 100644 index 0000000000..26f2a80337 --- /dev/null +++ b/tests/unit/cli-catalog-newentries.test.ts @@ -0,0 +1,143 @@ +/** + * F1: cli-catalog-newentries.test.ts + * Assert presence and shape of all entries new to plan 14. + */ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { CLI_TOOLS } = await import("../../src/shared/constants/cliTools.ts"); +const { CliCatalogEntrySchema } = await import("../../src/shared/schemas/cliCatalog.ts"); + +const NEW_IDS = [ + "roo", + "jcode", + "deepseek-tui", + "smelt", + "pi", + "agent-deck", + "goose", + "interpreter", + "warp", +]; + +for (const id of NEW_IDS) { + test(`New entry '${id}' exists in CLI_TOOLS`, () => { + assert.ok(id in CLI_TOOLS, `Entry '${id}' missing from CLI_TOOLS`); + }); + + test(`New entry '${id}' has non-empty description`, () => { + const entry = CLI_TOOLS[id]; + assert.ok(entry, `Entry '${id}' not found`); + assert.ok( + typeof entry.description === "string" && entry.description.length > 0, + `Entry '${id}' has empty description` + ); + }); + + test(`New entry '${id}' passes schema validation`, () => { + const entry = CLI_TOOLS[id]; + assert.ok(entry, `Entry '${id}' not found`); + const result = CliCatalogEntrySchema.safeParse(entry); + assert.equal( + result.success, + true, + result.success ? "" : `Entry '${id}' schema error: ${JSON.stringify(result.error.issues)}` + ); + }); + + test(`New entry '${id}' has color in #RRGGBB format`, () => { + const entry = CLI_TOOLS[id]; + assert.ok(entry, `Entry '${id}' not found`); + assert.match(entry.color, /^#[0-9A-Fa-f]{6}$/, `Entry '${id}' color '${entry.color}' is not #RRGGBB`); + }); + + test(`New entry '${id}' has non-empty vendor`, () => { + const entry = CLI_TOOLS[id]; + assert.ok(entry, `Entry '${id}' not found`); + assert.ok( + typeof entry.vendor === "string" && entry.vendor.length > 0, + `Entry '${id}' has empty vendor` + ); + }); +} + +// Category checks for new entries +test("roo is category=code, baseUrlSupport=full", () => { + assert.equal(CLI_TOOLS["roo"].category, "code"); + assert.equal(CLI_TOOLS["roo"].baseUrlSupport, "full"); +}); + +test("jcode is category=code with defaultCommand=jcode", () => { + assert.equal(CLI_TOOLS["jcode"].category, "code"); + assert.equal(CLI_TOOLS["jcode"].defaultCommand, "jcode"); +}); + +test("deepseek-tui is category=code, baseUrlSupport=full", () => { + assert.equal(CLI_TOOLS["deepseek-tui"].category, "code"); + assert.equal(CLI_TOOLS["deepseek-tui"].baseUrlSupport, "full"); +}); + +test("smelt is category=code with defaultCommand=smelt", () => { + assert.equal(CLI_TOOLS["smelt"].category, "code"); + assert.equal(CLI_TOOLS["smelt"].defaultCommand, "smelt"); +}); + +test("pi is category=code with defaultCommand=pi", () => { + assert.equal(CLI_TOOLS["pi"].category, "code"); + assert.equal(CLI_TOOLS["pi"].defaultCommand, "pi"); +}); + +test("goose is category=agent, acpSpawnable=true, baseUrlSupport=full", () => { + assert.equal(CLI_TOOLS["goose"].category, "agent"); + assert.equal(CLI_TOOLS["goose"].acpSpawnable, true); + assert.equal(CLI_TOOLS["goose"].baseUrlSupport, "full"); +}); + +test("interpreter is category=agent, acpSpawnable=true", () => { + assert.equal(CLI_TOOLS["interpreter"].category, "agent"); + assert.equal(CLI_TOOLS["interpreter"].acpSpawnable, true); +}); + +test("warp is category=agent, baseUrlSupport=partial", () => { + assert.equal(CLI_TOOLS["warp"].category, "agent"); + assert.equal(CLI_TOOLS["warp"].baseUrlSupport, "partial"); + assert.equal(CLI_TOOLS["warp"].acpSpawnable, true); +}); + +test("agent-deck is category=agent, baseUrlSupport=full", () => { + assert.equal(CLI_TOOLS["agent-deck"].category, "agent"); + assert.equal(CLI_TOOLS["agent-deck"].baseUrlSupport, "full"); +}); + +// Also check entries that only received new fields (not brand new) +test("aider was added/confirmed: category=code, acpSpawnable=true, baseUrlSupport=full", () => { + const entry = CLI_TOOLS["aider"]; + assert.ok(entry, "aider entry must exist"); + assert.equal(entry.category, "code"); + assert.equal(entry.acpSpawnable, true); + assert.equal(entry.baseUrlSupport, "full"); + assert.equal(entry.defaultCommand, "aider"); +}); + +test("forge was added/confirmed: category=code, acpSpawnable=true, baseUrlSupport=full", () => { + const entry = CLI_TOOLS["forge"]; + assert.ok(entry, "forge entry must exist"); + assert.equal(entry.category, "code"); + assert.equal(entry.acpSpawnable, true); + assert.equal(entry.baseUrlSupport, "full"); +}); + +test("gemini-cli was added: category=code, acpSpawnable=true, defaultCommand=gemini", () => { + const entry = CLI_TOOLS["gemini-cli"]; + assert.ok(entry, "gemini-cli entry must exist"); + assert.equal(entry.category, "code"); + assert.equal(entry.acpSpawnable, true); + assert.equal(entry.defaultCommand, "gemini"); +}); + +test("cursor-cli was added: category=code, acpSpawnable=true", () => { + const entry = CLI_TOOLS["cursor-cli"]; + assert.ok(entry, "cursor-cli entry must exist"); + assert.equal(entry.category, "code"); + assert.equal(entry.acpSpawnable, true); +}); diff --git a/tests/unit/cli-catalog-removed.test.ts b/tests/unit/cli-catalog-removed.test.ts new file mode 100644 index 0000000000..f6072f2563 --- /dev/null +++ b/tests/unit/cli-catalog-removed.test.ts @@ -0,0 +1,45 @@ +/** + * F1: cli-catalog-removed.test.ts + * Assert that MITM-backlog entries are removed from CLI_TOOLS per plan 14 D17. + */ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { CLI_TOOLS } = await import("../../src/shared/constants/cliTools.ts"); + +test("CLI_TOOLS.windsurf is undefined (removed per D17 — MITM backlog plan 11)", () => { + // windsurf (Codeium) was removed from CLI_TOOLS because it has no generic + // custom base URL support. It remains as an OAuth provider in src/lib/oauth/. + assert.equal( + (CLI_TOOLS as Record)["windsurf"], + undefined, + "windsurf must be removed from CLI_TOOLS" + ); +}); + +test("CLI_TOOLS.amp is undefined (removed per D17 — MITM backlog plan 11)", () => { + // amp (Sourcegraph) was removed from CLI_TOOLS because it has a closed ecosystem. + assert.equal( + (CLI_TOOLS as Record)["amp"], + undefined, + "amp must be removed from CLI_TOOLS" + ); +}); + +// amazon-q and cowork were NOT present in CLI_TOOLS before plan 14. +// They are documented here for completeness. +test("CLI_TOOLS['amazon-q'] is undefined (was never added — MITM backlog plan 11)", () => { + assert.equal( + (CLI_TOOLS as Record)["amazon-q"], + undefined, + "amazon-q must not exist in CLI_TOOLS" + ); +}); + +test("CLI_TOOLS.cowork is undefined (was never added — MITM backlog plan 11)", () => { + assert.equal( + (CLI_TOOLS as Record)["cowork"], + undefined, + "cowork must not exist in CLI_TOOLS" + ); +}); diff --git a/tests/unit/cli-catalog-schema.test.ts b/tests/unit/cli-catalog-schema.test.ts new file mode 100644 index 0000000000..2a263e7d22 --- /dev/null +++ b/tests/unit/cli-catalog-schema.test.ts @@ -0,0 +1,87 @@ +/** + * F1: cli-catalog-schema.test.ts + * Round-trip each CLI_TOOLS entry through CliCatalogEntrySchema; + * verify ZodError on invalid payloads. + */ +import test from "node:test"; +import assert from "node:assert/strict"; +import { z } from "zod"; + +const { CLI_TOOLS } = await import("../../src/shared/constants/cliTools.ts"); +const { CliCatalogEntrySchema, CliCatalogSchema } = await import( + "../../src/shared/schemas/cliCatalog.ts" +); + +test("Every CLI_TOOLS entry passes CliCatalogEntrySchema.parse() without error", () => { + for (const [key, tool] of Object.entries(CLI_TOOLS)) { + const result = CliCatalogEntrySchema.safeParse(tool); + assert.equal( + result.success, + true, + `Entry '${key}' failed schema validation: ${!result.success ? JSON.stringify(result.error.issues) : ""}` + ); + } +}); + +test("CliCatalogSchema.parse() accepts the full CLI_TOOLS record", () => { + const result = CliCatalogSchema.safeParse(CLI_TOOLS); + assert.equal( + result.success, + true, + result.success ? "" : `CliCatalogSchema failed: ${JSON.stringify(result.error.issues)}` + ); +}); + +test("CliCatalogEntrySchema throws ZodError for invalid category value", () => { + const base = { ...CLI_TOOLS["claude"] }; + // @ts-expect-error — intentional invalid value for testing + const invalid = { ...base, category: "invalid" }; + assert.throws( + () => CliCatalogEntrySchema.parse(invalid), + (err) => err instanceof z.ZodError + ); +}); + +test("CliCatalogEntrySchema throws ZodError for invalid color (not #RRGGBB)", () => { + const base = { ...CLI_TOOLS["codex"] }; + const invalid = { ...base, color: "xyz" }; + assert.throws( + () => CliCatalogEntrySchema.parse(invalid), + (err) => err instanceof z.ZodError + ); +}); + +test("CliCatalogEntrySchema throws ZodError for invalid baseUrlSupport value", () => { + const base = { ...CLI_TOOLS["cline"] }; + // @ts-expect-error — intentional invalid value for testing + const invalid = { ...base, baseUrlSupport: "maybe" }; + assert.throws( + () => CliCatalogEntrySchema.parse(invalid), + (err) => err instanceof z.ZodError + ); +}); + +test("CliCatalogEntrySchema throws ZodError when required string fields are empty", () => { + const base = { ...CLI_TOOLS["qwen"] }; + const invalid = { ...base, vendor: "" }; + assert.throws( + () => CliCatalogEntrySchema.parse(invalid), + (err) => err instanceof z.ZodError + ); +}); + +test("CliCatalogEntrySchema throws ZodError for invalid configType value", () => { + const base = { ...CLI_TOOLS["custom"] }; + // @ts-expect-error — intentional invalid value for testing + const invalid = { ...base, configType: "unknown-type" }; + assert.throws( + () => CliCatalogEntrySchema.parse(invalid), + (err) => err instanceof z.ZodError + ); +}); + +test("Optional fields absent from entry still parse successfully", () => { + // 'codex' has no guideSteps, no envVars, no notes — minimal entry + const result = CliCatalogEntrySchema.safeParse(CLI_TOOLS["codex"]); + assert.equal(result.success, true); +}); diff --git a/tests/unit/cli-runtime-detection.test.ts b/tests/unit/cli-runtime-detection.test.ts index 1fa283b6c5..55a8fa7279 100644 --- a/tests/unit/cli-runtime-detection.test.ts +++ b/tests/unit/cli-runtime-detection.test.ts @@ -34,14 +34,17 @@ function createFile(dir, name, content) { // ─── CLI_TOOL_IDS ───────────────────────────────────────────── describe("CLI_TOOL_IDS", () => { - it("should include all expected tools", () => { + it("should include all expected tools from cliRuntime.ts (separate from CLI_TOOLS catalog)", () => { + // CLI_TOOL_IDS comes from cliRuntime.ts — a runtime-detection catalog that + // is SEPARATE from the UI catalog CLI_TOOLS in cliTools.ts. + // windsurf was removed from CLI_TOOLS (plan 14 D17) but may still be in + // cliRuntime.ts for binary detection purposes. const expected = [ "claude", "codex", "droid", "openclaw", "cursor", - "windsurf", "cline", "kilo", "continue", @@ -192,8 +195,15 @@ describe("continue tool — no binary required", () => { }); }); -describe("windsurf tool — guide-only integration", () => { - it("should report installed=true without requiring a local binary", async () => { +// Note: windsurf was removed from CLI_TOOLS in plan 14 D17 (MITM backlog plan 11). +// cliRuntime.ts may still have windsurf for binary detection (separate catalog). +// This test is skipped if windsurf is not registered in cliRuntime.ts. +describe("windsurf tool — guide-only integration (cliRuntime.ts)", () => { + it("should handle getCliRuntimeStatus for windsurf if it exists in cliRuntime catalog", async () => { + if (!CLI_TOOL_IDS.includes("windsurf")) { + // windsurf removed from runtime detection catalog too — skip + return; + } const result = await getCliRuntimeStatus("windsurf"); assert.equal(result.installed, true); assert.equal(result.runnable, true); diff --git a/tests/unit/cli-tools-schema.test.ts b/tests/unit/cli-tools-schema.test.ts index 06989e36b2..a11704aa13 100644 --- a/tests/unit/cli-tools-schema.test.ts +++ b/tests/unit/cli-tools-schema.test.ts @@ -1,32 +1,24 @@ import test from "node:test"; import assert from "node:assert/strict"; -test("CLI_TOOLS registry contains all 18 expected tools", async () => { +test("CLI_TOOLS registry contains all expected tools (plan 14 — 29 total)", async () => { const { CLI_TOOLS } = await import("../../src/shared/constants/cliTools.ts"); + // windsurf and amp removed per plan 14 D17 (MITM backlog plan 11) + // 10 new entries added: roo, jcode, deepseek-tui, smelt, pi, aider, forge, + // gemini-cli, cursor-cli, goose, interpreter, warp, agent-deck (+ hermes-agent already existed) const expected = [ - "claude", - "codex", - "opencode", - "cline", - "kilo", - "continue", - "qwen", - "windsurf", - "hermes", - "hermes-agent", - "amp", - "kiro", - "cursor", - "droid", - "antigravity", - "copilot", - "openclaw", - "custom", + "claude", "codex", "droid", "openclaw", "cursor", "cline", "kilo", "continue", + "antigravity", "copilot", "opencode", "hermes", "hermes-agent", "kiro", "qwen", "custom", + "aider", "forge", "gemini-cli", "cursor-cli", "roo", "jcode", "deepseek-tui", "smelt", "pi", + "goose", "interpreter", "warp", "agent-deck", ]; for (const id of expected) { assert.ok(id in CLI_TOOLS, `Missing tool: ${id}`); } assert.equal(Object.keys(CLI_TOOLS).length, expected.length); + // Confirm removed entries are gone + assert.equal((CLI_TOOLS as Record)["windsurf"], undefined); + assert.equal((CLI_TOOLS as Record)["amp"], undefined); }); test("Every tool has required fields: id, name, description, configType", async () => { diff --git a/tests/unit/cli-tools.test.ts b/tests/unit/cli-tools.test.ts index 3174ee9dc5..dc5a4f2676 100644 --- a/tests/unit/cli-tools.test.ts +++ b/tests/unit/cli-tools.test.ts @@ -13,25 +13,11 @@ const { CLI_TOOL_IDS } = await import("../../src/shared/services/cliRuntime.ts") const { applyFingerprint, isCliCompatEnabled, setCliCompatProviders } = await import("../../open-sse/config/cliFingerprints.ts"); -test("Amp CLI is registered as a guide-based CLI tool with shorthand mapping guidance", () => { - const amp = CLI_TOOLS.amp; - assert.ok(amp); - assert.equal(amp.configType, "guide"); - assert.equal(amp.defaultCommand, "amp"); - assert.deepEqual(amp.modelAliases, ["g25p", "g25f", "cs45", "g54"]); - - const notesText = (amp.notes || []) - .map((note) => note?.text || "") - .join(" ") - .toLowerCase(); - - assert.match(notesText, /shorthand/); - assert.match(notesText, /g25p/); - assert.match(notesText, /claude-sonnet-4-5-20250929/); -}); - -test("Amp CLI is discoverable in runtime tooling but excluded from provider fingerprint toggles", () => { - assert.ok(CLI_TOOL_IDS.includes("amp")); +test("Amp CLI was removed from CLI_TOOLS per plan 14 D17 (MITM backlog plan 11)", () => { + // amp (Sourcegraph) removed from CLI_TOOLS in plan 14 because it has a closed ecosystem + // and does not support a generic custom base URL. Cross-ref: plan 11 MITM backlog. + assert.equal((CLI_TOOLS as Record).amp, undefined); + // amp may still appear in cliRuntime.ts (runtime detection catalog — separate from UI catalog) assert.equal(CLI_COMPAT_PROVIDER_IDS.includes("amp"), false); }); diff --git a/tests/unit/t40-opencode-cli-tools-integration.test.ts b/tests/unit/t40-opencode-cli-tools-integration.test.ts index 6b8c07f7c2..fbaf05db0a 100644 --- a/tests/unit/t40-opencode-cli-tools-integration.test.ts +++ b/tests/unit/t40-opencode-cli-tools-integration.test.ts @@ -173,16 +173,14 @@ test("T40: OpenCode light/dark provider assets are valid SVG files", async () => assert.doesNotMatch(dark, / { - const windsurf = CLI_TOOLS.windsurf; - assert.ok(windsurf, "Windsurf tool card must exist"); - assert.equal(windsurf.configType, "guide"); - - const notesText = (windsurf.notes || []) - .map((note) => note?.text || "") - .join(" ") - .toLowerCase(); - - assert.match(notesText, /byok/); - assert.match(notesText, /custom openai-compatible provider/); +test("T40: Windsurf was removed from CLI_TOOLS in plan 14 D17 (MITM backlog plan 11)", () => { + // windsurf (Codeium) was removed from CLI_TOOLS because it has no generic custom base URL + // support. It remains as an OAuth provider in src/lib/oauth/ for authentication. + // The old guide/limitations notes are no longer needed in the UI catalog. + // Cross-reference: _tasks/features-v3.8.6/refactorpages/_orchestration/_plan11-mitm-backlog.md + assert.equal( + (CLI_TOOLS as Record)["windsurf"], + undefined, + "windsurf must be removed from CLI_TOOLS per plan 14 D17" + ); });