mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-06 23:32:12 +03:00
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:
committed by
GitHub
parent
373dd17ccb
commit
1bd4b02110
@@ -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
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
|
||||
235
src/app/api/cli-tools/codewhale-settings/route.ts
Normal file
235
src/app/api/cli-tools/codewhale-settings/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
279
tests/integration/cli-settings-codewhale.test.ts
Normal file
279
tests/integration/cli-settings-codewhale.test.ts
Normal 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;
|
||||
});
|
||||
@@ -44,6 +44,7 @@ const NOT_ACP_SPAWNABLE_IDS = [
|
||||
"roo",
|
||||
"jcode",
|
||||
"deepseek-tui",
|
||||
"codewhale",
|
||||
"smelt",
|
||||
"pi",
|
||||
"hermes-agent",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user