feat(cli-tools): add CodeWhale CLI tool (#5996)

CodeWhale (https://github.com/Hmbown/CodeWhale) is the actively-maintained
successor to DeepSeek TUI — same author, renamed project. Added as a dual
entry alongside the existing "deepseek-tui" catalog entry (rather than a
hard rename) so users who still run the old DeepSeek TUI binary keep a
working dashboard card, while new users are steered to "codewhale".

New /api/cli-tools/codewhale-settings route writes the primary
~/.codewhale/config.toml and keeps an existing legacy
~/.deepseek/config.toml in sync (read fallback + best-effort write sync),
mirroring deepseek-tui-settings/route.ts. CLI_TOOLS and cliRuntime catalogs
updated; catalog cardinality tests/constants bumped accordingly (18→19
visible code tools, 28→29 total).


Inspired-by: https://github.com/decolua/9router/pull/1761

Co-authored-by: aristorinjuang <aristorinjuang@gmail.com>
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-03 00:57:06 -03:00
committed by GitHub
parent 373dd17ccb
commit 1bd4b02110
10 changed files with 573 additions and 11 deletions

View File

@@ -27,6 +27,7 @@
- **feat(providers):** add Charm Hyper as an OpenAI-compatible (API-key) provider. (thanks @whale9820)
- **feat(providers):** add SumoPod and X5Lab as OpenAI-compatible (API-key) providers. (thanks @rigelra15)
- **feat(server):** support reverse-proxy subpath deployment via OMNIROUTE_BASE_PATH (basePath-aware auth redirects). (thanks @SillyHippy)
- **feat(cli-tools):** add CodeWhale CLI tool (successor to DeepSeek TUI). (thanks @aristorinjuang)
### 🔧 Bug Fixes

View File

@@ -12,7 +12,7 @@ OmniRoute integrates with three categories of CLI tools spread across three dedi
| Page | Route | Concept | Count |
| -------------- | ----------------------- | ------------------------------------------------------------------------- | ------------ |
| **CLI Code's** | `/dashboard/cli-code` | Coding tools you point at OmniRoute (Client → CLI → OmniRoute → Provider) | 19 |
| **CLI Code's** | `/dashboard/cli-code` | Coding tools you point at OmniRoute (Client → CLI → OmniRoute → Provider) | 20 |
| **CLI Agents** | `/dashboard/cli-agents` | Autonomous agents you point at OmniRoute (same flow, broader scope) | 6 |
| **ACP Agents** | `/dashboard/acp-agents` | CLIs that OmniRoute spawns as backend via stdio/ACP (reverse flow) | see registry |
@@ -90,7 +90,7 @@ Entries with `baseUrlSupport: "none"` are **not shown** in the dashboard pages
---
## 1. CLI Code's Catalog (19 tools)
## 1. CLI Code's Catalog (20 tools)
Tools that support custom base URL and appear in `/dashboard/cli-code`:
@@ -107,6 +107,7 @@ Tools that support custom base URL and appear in `/dashboard/cli-code`:
| forge | ForgeCode | Antinomy HQ | full | custom | true |
| jcode | jcode | 1jehuang (OSS) | full | custom | false |
| deepseek-tui | DeepSeek TUI | Hunter Bown (OSS) | full | custom | false |
| codewhale | CodeWhale | Hmbown (OSS) | full | custom | false |
| opencode | OpenCode | Anomaly (ex-SST) | full | guide | true |
| droid | Factory Droid | Factory AI | partial | guide | false |
| copilot | GitHub Copilot CLI | GitHub/MS | full | custom | false |
@@ -198,7 +199,8 @@ New tools with `configType: "custom"` have dedicated settings API routes:
| ------------------------------------------- | ------------------------------ |
| `POST /api/cli-tools/forge-settings` | ForgeCode (.forge.toml) |
| `POST /api/cli-tools/jcode-settings` | jcode (--base-url flag) |
| `POST /api/cli-tools/deepseek-tui-settings` | DeepSeek TUI (OPENAI_BASE_URL) |
| `POST /api/cli-tools/deepseek-tui-settings` | DeepSeek TUI (OPENAI_BASE_URL, legacy) |
| `POST /api/cli-tools/codewhale-settings` | CodeWhale (OPENAI_BASE_URL, primary + legacy `~/.deepseek` sync) |
| `POST /api/cli-tools/smelt-settings` | Smelt |
| `POST /api/cli-tools/pi-settings` | Pi coding agent |

View File

@@ -0,0 +1,235 @@
"use server";
import { NextResponse } from "next/server";
import fs from "fs/promises";
import path from "path";
import { requireCliToolsAuth } from "@/lib/api/requireCliToolsAuth";
import {
ensureCliConfigWriteAllowed,
getCliPrimaryConfigPath,
getCliRuntimeStatus,
} from "@/shared/services/cliRuntime";
import { createBackup } from "@/shared/services/backupService";
import { saveCliToolLastConfigured, deleteCliToolLastConfigured } from "@/lib/db/cliToolState";
import { cliModelConfigSchema } from "@/shared/validation/schemas";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
import { resolveApiKey } from "@/shared/services/apiKeyResolver";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
const TOOL_ID = "codewhale";
/**
* CodeWhale is the actively-maintained successor to DeepSeek TUI (same
* author, renamed project — https://github.com/Hmbown/CodeWhale). It reads
* its config from ~/.codewhale/config.toml. Users upgrading from the old
* DeepSeek TUI binary may still have ~/.deepseek/config.toml around, so we
* read/write that path as a legacy fallback.
*/
const getPrimaryConfigPath = (): string =>
getCliPrimaryConfigPath(TOOL_ID) ?? path.join(process.env.HOME ?? "~", ".codewhale", "config.toml");
const getLegacyConfigPath = (): string =>
path.join(process.env.HOME ?? "~", ".deepseek", "config.toml");
const getPrimaryConfigDir = () => path.dirname(getPrimaryConfigPath());
/**
* Render the OmniRoute config block in CodeWhale TOML format.
* CodeWhale reads OPENAI_BASE_URL and OPENAI_API_KEY from its config.
* Reference: https://github.com/Hmbown/CodeWhale
*/
function renderCodewhaleConfig(baseUrl: string, apiKey: string, model: string): string {
return [
"# CodeWhale config — managed by OmniRoute (plan 14)",
"",
"[openai]",
`base_url = "${baseUrl}"`,
`api_key = "${apiKey}"`,
`model = "${model}"`,
"",
].join("\n");
}
/**
* Check if the config file contains OmniRoute settings.
*/
const hasOmniRouteConfig = (content: string | null): boolean => {
if (!content) return false;
return content.includes("managed by OmniRoute");
};
// Read current config.toml — prefers the primary ~/.codewhale path, falling
// back to the legacy ~/.deepseek path for users upgrading from DeepSeek TUI.
const readConfig = async (): Promise<string | null> => {
for (const candidate of [getPrimaryConfigPath(), getLegacyConfigPath()]) {
try {
return await fs.readFile(candidate, "utf-8");
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
}
}
return null;
};
// GET — check CodeWhale CLI and return current config
export async function GET(request: Request) {
const authError = await requireCliToolsAuth(request);
if (authError) return authError;
try {
const runtime = await getCliRuntimeStatus(TOOL_ID);
if (!runtime.installed || !runtime.runnable) {
return NextResponse.json({
installed: runtime.installed,
runnable: runtime.runnable,
command: runtime.command,
commandPath: runtime.commandPath,
runtimeMode: runtime.runtimeMode,
reason: runtime.reason,
config: null,
message:
runtime.installed && !runtime.runnable
? "CodeWhale is installed but not runnable"
: "CodeWhale is not installed",
});
}
const config = await readConfig();
return NextResponse.json({
installed: runtime.installed,
runnable: runtime.runnable,
command: runtime.command,
commandPath: runtime.commandPath,
runtimeMode: runtime.runtimeMode,
reason: runtime.reason,
config,
hasOmniRoute: hasOmniRouteConfig(config),
configPath: getPrimaryConfigPath(),
});
} catch (err) {
return NextResponse.json(
{ error: { message: sanitizeErrorMessage(err) } },
{ status: 500 }
);
}
}
// POST — write OmniRoute settings to CodeWhale's config.toml (primary), and
// keep the legacy ~/.deepseek/config.toml in sync when it already exists so
// users who have not yet upgraded their CLI binary keep working.
export async function POST(request: Request) {
const authError = await requireCliToolsAuth(request);
if (authError) return authError;
let rawBody;
try {
rawBody = await request.json();
} catch {
return NextResponse.json(
{ error: { message: "Invalid JSON body" } },
{ status: 400 }
);
}
try {
const writeGuard = ensureCliConfigWriteAllowed();
if (writeGuard) {
return NextResponse.json({ error: writeGuard }, { status: 403 });
}
// Extract keyId BEFORE Zod validation — Zod strips unknown fields
const keyId = typeof rawBody?.keyId === "string" ? rawBody.keyId.trim() : null;
const validation = validateBody(cliModelConfigSchema, rawBody);
if (isValidationFailure(validation)) {
return NextResponse.json({ error: validation.error }, { status: 400 });
}
const { baseUrl, model } = validation.data;
const apiKey = await resolveApiKey(keyId, validation.data.apiKey);
const primaryPath = getPrimaryConfigPath();
const legacyPath = getLegacyConfigPath();
const content = renderCodewhaleConfig(baseUrl, apiKey, model);
// Always write the primary (~/.codewhale) config.
await fs.mkdir(getPrimaryConfigDir(), { recursive: true });
await createBackup(TOOL_ID, primaryPath);
await fs.writeFile(primaryPath, content, "utf-8");
// Best-effort: keep the legacy (~/.deepseek) config in sync only if it
// already exists — never create a fresh legacy directory for new users.
try {
await fs.access(legacyPath);
await createBackup(TOOL_ID, legacyPath);
await fs.writeFile(legacyPath, content, "utf-8");
} catch {
/* legacy config not present — nothing to sync */
}
// Persist last-configured timestamp
try {
saveCliToolLastConfigured(TOOL_ID);
} catch {
/* non-critical */
}
return NextResponse.json({
success: true,
message: "CodeWhale settings applied successfully!",
configPath: primaryPath,
});
} catch (err) {
return NextResponse.json(
{ error: { message: sanitizeErrorMessage(err) } },
{ status: 500 }
);
}
}
// DELETE — remove OmniRoute CodeWhale config (primary + legacy, if present)
export async function DELETE(request: Request) {
const authError = await requireCliToolsAuth(request);
if (authError) return authError;
try {
const writeGuard = ensureCliConfigWriteAllowed();
if (writeGuard) {
return NextResponse.json({ error: writeGuard }, { status: 403 });
}
const primaryPath = getPrimaryConfigPath();
const legacyPath = getLegacyConfigPath();
// Backup + remove primary before removing
await createBackup(TOOL_ID, primaryPath);
await fs.rm(primaryPath, { force: true });
// Best-effort: remove legacy config too, if present
try {
await fs.access(legacyPath);
await createBackup(TOOL_ID, legacyPath);
await fs.rm(legacyPath, { force: true });
} catch {
/* legacy config not present */
}
// Clear last-configured timestamp
try {
deleteCliToolLastConfigured(TOOL_ID);
} catch {
/* non-critical */
}
return NextResponse.json({
success: true,
message: "CodeWhale settings removed successfully",
});
} catch (err) {
return NextResponse.json(
{ error: { message: sanitizeErrorMessage(err) } },
{ status: 500 }
);
}
}

View File

@@ -645,7 +645,13 @@ aider --openai-api-base "{{baseUrl}}" --model "{{model}}"`,
defaultCommand: "jcode",
},
/** ★ Added by plan 14 (CLI Pages Redesign) — 2026-05-27 */
/**
* ★ Added by plan 14 (CLI Pages Redesign) — 2026-05-27
* Kept as a legacy/dual entry after CodeWhale (see below) took over as the
* actively-maintained successor. Existing users who still have DeepSeek
* TUI installed keep a working dashboard card; new users are steered to
* "codewhale" instead.
*/
"deepseek-tui": {
id: "deepseek-tui",
name: "DeepSeek TUI",
@@ -661,6 +667,29 @@ aider --openai-api-base "{{baseUrl}}" --model "{{model}}"`,
defaultCommand: "deepseek-tui",
},
/**
* ★ Added 2026-07-02 (dual-entry, see deepseek-tui above). CodeWhale is
* the actively-maintained successor to DeepSeek TUI — same author, new
* name. Config lives under ~/.codewhale/config.toml; the settings route
* also keeps ~/.deepseek/config.toml (legacy) in sync for upgrading
* users. Reference: https://github.com/Hmbown/CodeWhale
*/
codewhale: {
id: "codewhale",
name: "CodeWhale",
icon: "terminal",
color: "#4F46E5",
description:
"CodeWhale — Rust-based coding agent CLI with OPENAI_BASE_URL support (successor to DeepSeek TUI)",
docsUrl: "https://github.com/Hmbown/CodeWhale",
configType: "custom",
category: "code",
vendor: "OSS (Hmbown)",
acpSpawnable: false,
baseUrlSupport: "full",
defaultCommand: "codewhale",
},
/** ★ Added by plan 14 (CLI Pages Redesign) — 2026-05-27 */
smelt: {
id: "smelt",

View File

@@ -60,5 +60,7 @@ export type CliCatalogEntry = z.infer<typeof CliCatalogEntrySchema>;
export const CliCatalogSchema = z.record(CliCatalogEntrySchema);
/** Cardinalidade obrigatória (Plano §3.1/§3.2 + D15). +1 (crush, decolua/9router#1233). */
export const EXPECTED_CODE_COUNT = 19;
// +1 (2026-07-02): "codewhale" added as a dual entry alongside "deepseek-tui"
// (CodeWhale is the actively-maintained successor to DeepSeek TUI).
export const EXPECTED_CODE_COUNT = 20;
export const EXPECTED_AGENT_COUNT = 6;

View File

@@ -210,6 +210,15 @@ const CLI_TOOLS: Record<string, any> = {
config: ".config/deepseek-tui/config.toml",
},
},
codewhale: {
defaultCommand: "codewhale",
envBinKey: "CLI_CODEWHALE_BIN",
requiresBinary: true,
healthcheckTimeoutMs: 8000,
paths: {
config: ".codewhale/config.toml",
},
},
smelt: {
defaultCommand: "smelt",
envBinKey: "CLI_SMELT_BIN",

View File

@@ -0,0 +1,279 @@
/**
* Integration tests for /api/cli-tools/codewhale-settings
*
* CodeWhale (https://github.com/Hmbown/CodeWhale) is the actively-maintained
* successor to DeepSeek TUI (same author, renamed project). This route mirrors
* deepseek-tui-settings/route.ts but writes/reads a dual config path:
* - primary: ~/.codewhale/config.toml
* - legacy: ~/.deepseek/config.toml (kept in sync when it already exists,
* so users upgrading their CLI binary keep working)
*/
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-codewhale-settings-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = "test-api-key-secret-codewhale";
process.env.JWT_SECRET = "test-jwt-secret-codewhale";
const core = await import("../../src/lib/db/core.ts");
const localDb = await import("../../src/lib/localDb.ts");
const { GET, POST, DELETE } = await import("../../src/app/api/cli-tools/codewhale-settings/route.ts");
async function resetStorage() {
delete process.env.INITIAL_PASSWORD;
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
async function enableAuth() {
process.env.INITIAL_PASSWORD = "test-bootstrap";
await localDb.updateSettings({ requireLogin: true, password: "" });
}
test.beforeEach(async () => {
await resetStorage();
});
// ── Test 1: GET without auth → 401 ──────────────────────────────────────────
test("codewhale-settings GET: returns 401 when auth required and no token", async () => {
await enableAuth();
const res = await GET(new Request("http://localhost/api/cli-tools/codewhale-settings"));
assert.equal(res.status, 401, `Expected 401, got ${res.status}`);
});
// ── Test 2: GET without auth requirement → 200 ───────────────────────────────
test("codewhale-settings GET: returns 200 when auth not required", async () => {
const res = await GET(new Request("http://localhost/api/cli-tools/codewhale-settings"));
assert.equal(res.status, 200, `Expected 200, got ${res.status}`);
const body = await res.json();
assert.ok(
"installed" in body || "config" in body,
"Response should contain installed or config field"
);
});
// ── Test 3: POST with invalid body → 400 ─────────────────────────────────────
test("codewhale-settings POST: 400 when baseUrl is missing", async () => {
const res = await POST(
new Request("http://localhost/api/cli-tools/codewhale-settings", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ apiKey: "sk-test", model: "deepseek-v4-pro" }),
})
);
assert.equal(res.status, 400, `Expected 400, got ${res.status}`);
const body = await res.json();
assert.ok(body.error !== undefined);
});
test("codewhale-settings POST: 400 when model is missing", async () => {
const res = await POST(
new Request("http://localhost/api/cli-tools/codewhale-settings", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ baseUrl: "http://localhost:20128", apiKey: "sk-test" }),
})
);
assert.equal(res.status, 400, `Expected 400, got ${res.status}`);
});
// ── Test 4: POST with valid body → writes PRIMARY config.toml only (no legacy dir) ──
test("codewhale-settings POST: writes primary ~/.codewhale/config.toml for a fresh install", async () => {
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "codewhale-home-"));
const origHome = process.env.HOME;
process.env.HOME = tmpHome;
try {
const res = await POST(
new Request("http://localhost/api/cli-tools/codewhale-settings", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
baseUrl: "http://localhost:20128",
apiKey: "sk-test-codewhale-key",
model: "deepseek-v4-pro",
}),
})
);
assert.ok([200, 403, 500].includes(res.status), `Unexpected status ${res.status}`);
if (res.status === 200) {
const body = await res.json();
assert.equal(body.success, true);
const primaryPath = path.join(tmpHome, ".codewhale", "config.toml");
assert.ok(fs.existsSync(primaryPath), "Primary ~/.codewhale/config.toml must be written");
const content = fs.readFileSync(primaryPath, "utf-8");
assert.ok(content.includes("managed by OmniRoute"), "Config should have OmniRoute marker");
assert.ok(content.includes("http://localhost:20128"), "Config should contain base URL");
assert.ok(content.includes("[openai]"), "Config should have [openai] section");
// No legacy ~/.deepseek dir existed before the write — must NOT be created.
const legacyPath = path.join(tmpHome, ".deepseek", "config.toml");
assert.ok(
!fs.existsSync(legacyPath),
"Legacy ~/.deepseek/config.toml must not be created for a fresh install"
);
}
} finally {
process.env.HOME = origHome;
fs.rmSync(tmpHome, { recursive: true, force: true });
}
});
// ── Test 5: POST keeps an EXISTING legacy ~/.deepseek config in sync ────────
test("codewhale-settings POST: syncs an existing legacy ~/.deepseek/config.toml", async () => {
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "codewhale-home-legacy-"));
const origHome = process.env.HOME;
process.env.HOME = tmpHome;
try {
// Simulate an existing DeepSeek TUI install (pre-CodeWhale upgrade).
const legacyDir = path.join(tmpHome, ".deepseek");
fs.mkdirSync(legacyDir, { recursive: true });
fs.writeFileSync(path.join(legacyDir, "config.toml"), 'provider = "deepseek"\n');
const res = await POST(
new Request("http://localhost/api/cli-tools/codewhale-settings", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
baseUrl: "http://localhost:20128",
apiKey: "sk-test-codewhale-key",
model: "deepseek-v4-flash",
}),
})
);
assert.ok([200, 403, 500].includes(res.status), `Unexpected status ${res.status}`);
if (res.status === 200) {
const primaryPath = path.join(tmpHome, ".codewhale", "config.toml");
const legacyPath = path.join(tmpHome, ".deepseek", "config.toml");
assert.ok(fs.existsSync(primaryPath), "Primary config must be written");
assert.ok(fs.existsSync(legacyPath), "Legacy config must still exist");
const primaryContent = fs.readFileSync(primaryPath, "utf-8");
const legacyContent = fs.readFileSync(legacyPath, "utf-8");
assert.ok(primaryContent.includes("http://localhost:20128"));
assert.ok(
legacyContent.includes("http://localhost:20128"),
"Legacy config must be kept in sync with the new base URL"
);
assert.equal(primaryContent, legacyContent, "Primary and legacy configs should match");
}
} finally {
process.env.HOME = origHome;
fs.rmSync(tmpHome, { recursive: true, force: true });
}
});
// ── Test 6: GET reads from legacy path when only legacy config exists ───────
test("codewhale-settings GET: falls back to legacy ~/.deepseek/config.toml when primary is absent", async () => {
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "codewhale-home-getlegacy-"));
const origHome = process.env.HOME;
process.env.HOME = tmpHome;
try {
const legacyDir = path.join(tmpHome, ".deepseek");
fs.mkdirSync(legacyDir, { recursive: true });
fs.writeFileSync(
path.join(legacyDir, "config.toml"),
'# managed by OmniRoute (plan 14)\n[openai]\nbase_url = "http://localhost:20128"\n'
);
const res = await GET(new Request("http://localhost/api/cli-tools/codewhale-settings"));
assert.equal(res.status, 200);
const body = await res.json();
if (body.config) {
assert.ok(body.config.includes("managed by OmniRoute"));
assert.equal(body.hasOmniRoute, true);
}
} finally {
process.env.HOME = origHome;
fs.rmSync(tmpHome, { recursive: true, force: true });
}
});
// ── Test 7: DELETE → removes both primary and legacy config files ───────────
test("codewhale-settings DELETE: removes primary and legacy config files", async () => {
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "codewhale-home-del-"));
const origHome = process.env.HOME;
process.env.HOME = tmpHome;
try {
const primaryDir = path.join(tmpHome, ".codewhale");
const legacyDir = path.join(tmpHome, ".deepseek");
fs.mkdirSync(primaryDir, { recursive: true });
fs.mkdirSync(legacyDir, { recursive: true });
fs.writeFileSync(
path.join(primaryDir, "config.toml"),
'# managed by OmniRoute (plan 14)\n[openai]\nbase_url = "http://localhost:20128"\n'
);
fs.writeFileSync(
path.join(legacyDir, "config.toml"),
'# managed by OmniRoute (plan 14)\n[openai]\nbase_url = "http://localhost:20128"\n'
);
const res = await DELETE(
new Request("http://localhost/api/cli-tools/codewhale-settings", { method: "DELETE" })
);
assert.ok([200, 403, 500].includes(res.status), `Expected 200/403/500, got ${res.status}`);
if (res.status === 200) {
const body = await res.json();
assert.equal(body.success, true);
assert.ok(!fs.existsSync(path.join(primaryDir, "config.toml")), "Primary config removed");
assert.ok(!fs.existsSync(path.join(legacyDir, "config.toml")), "Legacy config removed");
}
} finally {
process.env.HOME = origHome;
fs.rmSync(tmpHome, { recursive: true, force: true });
}
});
// ── Test 8: Error sanitization (Hard Rule #12) ───────────────────────────────
test("codewhale-settings: error responses do not leak stack traces", async () => {
const badReq = new Request("http://localhost/api/cli-tools/codewhale-settings", {
method: "POST",
headers: { "content-type": "application/json" },
body: "{ bad json }",
});
const res = await POST(badReq);
const bodyStr = JSON.stringify(await res.json());
assert.ok(
!bodyStr.match(/\s+at\s+\/[^\s]/),
"Error response must not contain absolute-path stack traces"
);
});
// ── Test 9: Hard Rule #13 (no exec/spawn) ────────────────────────────────────
test("codewhale-settings route.ts: does not call exec() or spawn() directly", () => {
const routePath = path.resolve(
import.meta.dirname,
"../../src/app/api/cli-tools/codewhale-settings/route.ts"
);
const content = fs.readFileSync(routePath, "utf-8");
assert.ok(!content.match(/\bexec\s*\(/), "Handler must not use exec()");
assert.ok(!content.match(/\bspawn\s*\(/), "Handler must not use spawn()");
});
test.after(async () => {
await resetStorage();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
delete process.env.DATA_DIR;
delete process.env.API_KEY_SECRET;
delete process.env.JWT_SECRET;
});

View File

@@ -44,6 +44,7 @@ const NOT_ACP_SPAWNABLE_IDS = [
"roo",
"jcode",
"deepseek-tui",
"codewhale",
"smelt",
"pi",
"hermes-agent",

View File

@@ -30,7 +30,7 @@ test(`CLI_TOOLS has exactly ${EXPECTED_AGENT_COUNT} agent entries`, () => {
);
});
test("CLI_TOOLS total code entries (including none) equals 23 (19 visible + 4 none)", () => {
test("CLI_TOOLS total code entries (including none) equals 24 (20 visible + 4 none)", () => {
// code-none entries: antigravity, kiro, cursor (app), hermes (simple guide)
const codeNone = codeAll.filter((t) => t.baseUrlSupport === "none");
assert.equal(
@@ -38,11 +38,11 @@ test("CLI_TOOLS total code entries (including none) equals 23 (19 visible + 4 no
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}`);
assert.equal(codeAll.length, 24, `Expected 24 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("CLI_TOOLS total (code + agent) = 30", () => {
assert.equal(all.length, 30, `Expected 30 total entries, got ${all.length}`);
});
test("All code-none entries have configType mitm OR are legacy excluded entries", () => {
@@ -66,7 +66,7 @@ test("All agent entries have baseUrlSupport 'full' or 'partial' (no agent is 'no
}
});
test("The 19 visible code entries match D15 list + crush exactly", () => {
test("The 20 visible code entries match D15 list exactly (+ crush + codewhale)", () => {
const d15List = new Set([
"claude",
"codex",
@@ -79,6 +79,7 @@ test("The 19 visible code entries match D15 list + crush exactly", () => {
"forge",
"jcode",
"deepseek-tui",
"codewhale",
"opencode",
"droid",
"copilot",

View File

@@ -1,12 +1,14 @@
import test from "node:test";
import assert from "node:assert/strict";
test("CLI_TOOLS registry contains all expected tools (plan 14 — 28 total + crush)", async () => {
test("CLI_TOOLS registry contains all expected tools (plan 14 — 30 total + crush + codewhale)", async () => {
const { CLI_TOOLS } = await import("../../src/shared/constants/cliTools.ts");
// windsurf and amp removed per plan 14 D17 (MITM backlog plan 11)
// New entries added: roo, jcode, deepseek-tui, smelt, pi, aider, forge,
// cursor-cli, goose, interpreter, warp, agent-deck (+ hermes-agent already existed)
// crush added — ported from upstream decolua/9router#1233
// codewhale added 2026-07-02 as a dual entry alongside deepseek-tui
// (CodeWhale is the actively-maintained successor to DeepSeek TUI).
const expected = [
"claude",
"codex",
@@ -30,6 +32,7 @@ test("CLI_TOOLS registry contains all expected tools (plan 14 — 28 total + cru
"roo",
"jcode",
"deepseek-tui",
"codewhale",
"smelt",
"pi",
"goose",