feat(cliproxyapi): add DB schema, upstream proxy config & settings UI

Part 1 of CLIProxyAPI integration (#902).

- DB migration for version_manager + upstream_proxy_config tables
- DB modules: versionManager.ts (tool lifecycle CRUD), upstreamProxy.ts (proxy config CRUD + SSRF guard)
- localDb.ts re-exports for new modules
- Provider registration: UPSTREAM_PROXY_PROVIDERS in providers.ts
- Zod validation schemas for version-manager API routes
- Settings UI: CliproxyapiSettingsTab (Advanced tab)
- 47 unit tests for DB modules
This commit is contained in:
oyi77
2026-04-02 13:27:51 +07:00
parent e121fec599
commit 8fc97a7f91
10 changed files with 1548 additions and 2 deletions

View File

@@ -0,0 +1,193 @@
"use client";
import { useState, useEffect } from "react";
import { Card, Button, Input, Toggle } from "@/shared/components";
export default function CliproxyapiSettingsTab() {
const [settings, setSettings] = useState<Record<string, any>>({});
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [message, setMessage] = useState<{ type: string; text: string } | null>(null);
const [toolState, setToolState] = useState<any>(null);
useEffect(() => {
fetch("/api/settings")
.then((r) => r.json())
.then((data) => {
setSettings(data);
setLoading(false);
})
.catch(() => setLoading(false));
fetch("/api/version-manager/status")
.then((r) => r.json())
.then((data) => {
const entry = Array.isArray(data) ? data.find((t: any) => t.tool === "cliproxyapi") : null;
setToolState(entry);
})
.catch(() => {});
}, []);
const updateSetting = async (key: string, value: any) => {
setSaving(true);
setMessage(null);
try {
const res = await fetch("/api/settings", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ [key]: value }),
});
if (res.ok) {
setSettings((prev) => ({ ...prev, [key]: value }));
setMessage({ type: "success", text: "Setting saved" });
}
} catch {
setMessage({ type: "error", text: "Failed to save setting" });
} finally {
setSaving(false);
}
};
const cpaEnabled = settings.cliproxyapi_fallback_enabled === true;
const cpaUrl = settings.cliproxyapi_url || "http://127.0.0.1:8317";
const cpaCodes = settings.cliproxyapi_fallback_codes || "502,401,403,429,503";
const statusColor =
toolState?.status === "running"
? "text-green-600 dark:text-green-400"
: toolState?.status === "error"
? "text-red-600 dark:text-red-400"
: "text-text-muted";
const statusIcon =
toolState?.status === "running"
? "check_circle"
: toolState?.status === "error"
? "error"
: "help";
return (
<div className="space-y-4">
{message && (
<div
className={`flex items-center gap-2 px-3 py-2 rounded-lg text-xs ${
message.type === "success"
? "bg-green-500/10 text-green-600 dark:text-green-400"
: "bg-red-500/10 text-red-600 dark:text-red-400"
}`}
>
<span className="material-symbols-outlined text-[14px]">
{message.type === "success" ? "check_circle" : "error"}
</span>
{message.text}
</div>
)}
<Card padding="md">
<div className="flex items-center gap-3 mb-4">
<div className="size-8 rounded-lg flex items-center justify-center bg-indigo-500/10">
<span className="material-symbols-outlined text-indigo-500 text-xl">swap_horiz</span>
</div>
<div>
<h3 className="font-medium text-sm">CLIProxyAPI Fallback</h3>
<p className="text-xs text-text-muted">
When enabled, failed requests are retried through CLIProxyAPI (localhost:8317)
</p>
</div>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between">
<label className="text-sm text-text-main">Enable CLIProxyAPI Fallback</label>
<Toggle
checked={cpaEnabled}
onChange={(checked) => updateSetting("cliproxyapi_fallback_enabled", checked)}
/>
</div>
{cpaEnabled && (
<>
<div>
<label className="text-xs text-text-muted mb-1.5 block">CLIProxyAPI URL</label>
<Input
value={cpaUrl}
onChange={(e) => updateSetting("cliproxyapi_url", e.target.value)}
placeholder="http://127.0.0.1:8317"
className="w-full"
/>
</div>
<div>
<label className="text-xs text-text-muted mb-1.5 block">
Fallback Status Codes (comma-separated)
</label>
<Input
value={cpaCodes}
onChange={(e) => updateSetting("cliproxyapi_fallback_codes", e.target.value)}
placeholder="502,401,403,429,503"
className="w-full"
/>
</div>
</>
)}
</div>
</Card>
<Card padding="md">
<h3 className="font-medium text-sm mb-4">CLIProxyAPI Status</h3>
{loading ? (
<div className="flex items-center gap-2 text-text-muted text-sm">
<span className="material-symbols-outlined animate-spin text-base">
progress_activity
</span>
Loading...
</div>
) : toolState ? (
<div className="grid grid-cols-2 gap-3">
<div className="p-3 rounded-lg bg-bg-secondary">
<p className="text-xs text-text-muted mb-1">Status</p>
<div className="flex items-center gap-1.5">
<span className={`material-symbols-outlined text-sm ${statusColor}`}>
{statusIcon}
</span>
<p className={`text-sm font-medium capitalize ${statusColor}`}>
{toolState.status?.replace("_", " ") || "Unknown"}
</p>
</div>
</div>
<div className="p-3 rounded-lg bg-bg-secondary">
<p className="text-xs text-text-muted mb-1">Version</p>
<p className="text-sm font-medium">
{toolState.installedVersion ? `v${toolState.installedVersion}` : "Not installed"}
</p>
</div>
<div className="p-3 rounded-lg bg-bg-secondary">
<p className="text-xs text-text-muted mb-1">Health</p>
<p
className={`text-sm font-medium ${
toolState.healthStatus === "healthy"
? "text-green-600 dark:text-green-400"
: toolState.healthStatus === "unhealthy"
? "text-red-600 dark:text-red-400"
: "text-text-muted"
}`}
>
{toolState.healthStatus === "healthy"
? "Healthy"
: toolState.healthStatus === "unhealthy"
? "Unhealthy"
: "Unknown"}
</p>
</div>
<div className="p-3 rounded-lg bg-bg-secondary">
<p className="text-xs text-text-muted mb-1">Port</p>
<p className="text-sm font-mono">{toolState.port || 8317}</p>
</div>
</div>
) : (
<p className="text-sm text-text-muted">CLIProxyAPI not detected</p>
)}
</Card>
</div>
);
}

View File

@@ -21,6 +21,7 @@ import CacheStatsCard from "./components/CacheStatsCard";
import CacheSettingsTab from "./components/CacheSettingsTab";
import MemorySkillsTab from "./components/MemorySkillsTab";
import ResilienceTab from "./components/ResilienceTab";
import CliproxyapiSettingsTab from "./components/CliproxyapiSettingsTab";
const tabs = [
{ id: "general", labelKey: "general", icon: "settings" },
@@ -113,7 +114,12 @@ export default function SettingsPage() {
{activeTab === "resilience" && <ResilienceTab />}
{activeTab === "advanced" && <ProxyTab />}
{activeTab === "advanced" && (
<div className="flex flex-col gap-6">
<ProxyTab />
<CliproxyapiSettingsTab />
</div>
)}
</div>
{/* App Info */}

View File

@@ -0,0 +1,56 @@
-- Migration 016: Version Manager & Upstream Proxy tables
--
-- Adds two tables for CLIProxyAPI integration:
-- version_manager — binary lifecycle management for CLI tools (CLIProxyAPI, etc.)
-- upstream_proxy_config — per-provider routing mode (native vs CLIProxyAPI vs fallback)
-- --------------------------------------------------------------------------
-- Table: version_manager
-- Tracks installed versions, process state, and update settings for
-- externally managed CLI tools (initially CLIProxyAPI).
-- --------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS version_manager (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tool TEXT NOT NULL UNIQUE, -- 'cliproxyapi' | 'omniroute'
current_version TEXT, -- latest known release version
installed_version TEXT, -- currently installed version
pinned_version TEXT, -- user-pinned version (null = auto)
binary_path TEXT, -- absolute path to binary
status TEXT NOT NULL DEFAULT 'not_installed', -- not_installed | installed | running | stopped | error
pid INTEGER, -- process ID when running
port INTEGER DEFAULT 8317, -- managed tool's port
api_key TEXT, -- API key for CLIProxyAPI (from its config)
management_key TEXT, -- CLIProxyAPI management secret key
auto_update INTEGER NOT NULL DEFAULT 1, -- 1 = auto-update enabled
auto_start INTEGER NOT NULL DEFAULT 0, -- 1 = start with OmniRoute
last_health_check TEXT, -- ISO timestamp
last_update_check TEXT, -- ISO timestamp
health_status TEXT DEFAULT 'unknown', -- unknown | healthy | unhealthy | timeout
config_overrides TEXT, -- JSON blob for CLIProxyAPI config overrides
error_message TEXT, -- last error message
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- --------------------------------------------------------------------------
-- Table: upstream_proxy_config
-- Per-provider routing configuration for CLIProxyAPI passthrough mode.
-- Determines whether each provider uses OmniRoute's native executor,
-- delegates to CLIProxyAPI, or uses fallback (native first, then CLIProxyAPI).
-- --------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS upstream_proxy_config (
id INTEGER PRIMARY KEY AUTOINCREMENT,
provider_id TEXT NOT NULL UNIQUE, -- e.g., 'antigravity', 'claude', 'codex'
mode TEXT NOT NULL DEFAULT 'native', -- 'native' | 'cliproxyapi' | 'fallback'
cliproxyapi_model_mapping TEXT, -- JSON: { "ag/gemini-3-pro": "gemini-3-pro-high", ... }
native_priority INTEGER NOT NULL DEFAULT 1, -- order in fallback chain (1 = first)
cliproxyapi_priority INTEGER NOT NULL DEFAULT 2, -- order in fallback chain
enabled INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_upc_provider ON upstream_proxy_config(provider_id);
CREATE INDEX IF NOT EXISTS idx_upc_mode ON upstream_proxy_config(mode);
CREATE INDEX IF NOT EXISTS idx_vm_tool ON version_manager(tool);
CREATE INDEX IF NOT EXISTS idx_vm_status ON version_manager(status);

230
src/lib/db/upstreamProxy.ts Normal file
View File

@@ -0,0 +1,230 @@
/** Upstream proxy config persistence for upstream_proxy_config table. */
import { getDbInstance } from "./core";
interface UpstreamProxyConfig {
id: number;
providerId: string;
mode: string;
cliproxyapiModelMapping: Record<string, unknown> | null;
nativePriority: number;
cliproxyapiPriority: number;
enabled: boolean;
createdAt: string;
updatedAt: string;
}
interface UpstreamProxyRow {
id: unknown;
provider_id: unknown;
mode: unknown;
cliproxyapi_model_mapping: unknown;
native_priority: unknown;
cliproxyapi_priority: unknown;
enabled: unknown;
created_at: unknown;
updated_at: unknown;
}
function toRecord(value: unknown): Record<string, unknown> {
return value && typeof value === "object" ? (value as Record<string, unknown>) : {};
}
const BLOCKED_HOSTNAMES = ["metadata.google.internal", "169.254.169.254", "metadata.aws.internal"];
function isPrivateHost(hostname: string): boolean {
if (BLOCKED_HOSTNAMES.includes(hostname)) return true;
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1") return false;
if (
/^10\./.test(hostname) ||
/^172\.(1[6-9]|2\d|3[01])\./.test(hostname) ||
/^192\.168\./.test(hostname)
)
return true;
if (
/^0\./.test(hostname) ||
/^127\./.test(hostname) ||
/^224\./.test(hostname) ||
/^169\.254\./.test(hostname)
)
return true;
return false;
}
export function validateProxyUrl(
url: string
): { valid: true; url: string } | { valid: false; error: string } {
try {
const parsed = new URL(url);
if (!["http:", "https:"].includes(parsed.protocol)) {
return {
valid: false,
error: `Unsupported protocol "${parsed.protocol}" — use http or https`,
};
}
if (isPrivateHost(parsed.hostname)) {
return {
valid: false,
error: `Proxy URL cannot point to private/internal address "${parsed.hostname}"`,
};
}
return { valid: true, url };
} catch {
return { valid: false, error: `Invalid URL: "${url}"` };
}
}
function rowToConfig(record: Record<string, unknown>): UpstreamProxyConfig {
return {
id: record.id as number,
providerId: record.provider_id as string,
mode: record.mode as string,
cliproxyapiModelMapping:
record.cliproxyapi_model_mapping && typeof record.cliproxyapi_model_mapping === "string"
? JSON.parse(record.cliproxyapi_model_mapping)
: null,
nativePriority: record.native_priority as number,
cliproxyapiPriority: record.cliproxyapi_priority as number,
enabled: record.enabled === 1 || record.enabled === true,
createdAt: record.created_at as string,
updatedAt: record.updated_at as string,
};
}
export async function getUpstreamProxyConfigs() {
const db = getDbInstance();
const rows = db
.prepare("SELECT * FROM upstream_proxy_config ORDER BY provider_id")
.all() as UpstreamProxyRow[];
return rows.map((row) => rowToConfig(toRecord(row)));
}
export async function getUpstreamProxyConfig(providerId: string) {
const db = getDbInstance();
const row = db
.prepare("SELECT * FROM upstream_proxy_config WHERE provider_id = ?")
.get(providerId) as UpstreamProxyRow | undefined;
if (!row) return null;
return rowToConfig(toRecord(row));
}
export async function upsertUpstreamProxyConfig(data: {
providerId: string;
mode?: string;
cliproxyapiModelMapping?: Record<string, unknown> | null;
nativePriority?: number;
cliproxyapiPriority?: number;
enabled?: boolean;
}) {
const db = getDbInstance();
const mode = data.mode ?? "native";
const cliproxyapiModelMapping =
data.cliproxyapiModelMapping !== undefined
? JSON.stringify(data.cliproxyapiModelMapping)
: null;
const nativePriority = data.nativePriority ?? 1;
const cliproxyapiPriority = data.cliproxyapiPriority ?? 2;
const enabled = data.enabled !== false ? 1 : 0;
db.prepare(
`INSERT INTO upstream_proxy_config
(provider_id, mode, cliproxyapi_model_mapping, native_priority, cliproxyapi_priority, enabled, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
ON CONFLICT(provider_id) DO UPDATE SET
mode = excluded.mode,
cliproxyapi_model_mapping = excluded.cliproxyapi_model_mapping,
native_priority = excluded.native_priority,
cliproxyapi_priority = excluded.cliproxyapi_priority,
enabled = excluded.enabled,
updated_at = datetime('now')`
).run(
data.providerId,
mode,
cliproxyapiModelMapping,
nativePriority,
cliproxyapiPriority,
enabled
);
return getUpstreamProxyConfig(data.providerId);
}
export async function updateUpstreamProxyConfig(
providerId: string,
updates: Record<string, unknown>
) {
const db = getDbInstance();
const current = await getUpstreamProxyConfig(providerId);
if (!current) {
throw new Error(`Provider ${providerId} not found`);
}
const sets: string[] = ["updated_at = datetime('now')"];
const params: unknown[] = [];
if (updates.mode !== undefined) {
sets.push("mode = ?");
params.push(updates.mode);
}
if (updates.cliproxyapiModelMapping !== undefined) {
sets.push("cliproxyapi_model_mapping = ?");
params.push(
updates.cliproxyapiModelMapping === null
? null
: JSON.stringify(updates.cliproxyapiModelMapping)
);
}
if (updates.nativePriority !== undefined) {
sets.push("native_priority = ?");
params.push(updates.nativePriority);
}
if (updates.cliproxyapiPriority !== undefined) {
sets.push("cliproxyapi_priority = ?");
params.push(updates.cliproxyapiPriority);
}
if (updates.enabled !== undefined) {
sets.push("enabled = ?");
params.push(updates.enabled === true ? 1 : 0);
}
params.push(providerId);
db.prepare(`UPDATE upstream_proxy_config SET ${sets.join(", ")} WHERE provider_id = ?`).run(
...params
);
return getUpstreamProxyConfig(providerId);
}
export async function deleteUpstreamProxyConfig(providerId: string) {
const db = getDbInstance();
const result = db
.prepare("DELETE FROM upstream_proxy_config WHERE provider_id = ?")
.run(providerId);
return result.changes > 0;
}
export async function getProvidersByMode(mode: string) {
const db = getDbInstance();
const rows = db
.prepare(
"SELECT * FROM upstream_proxy_config WHERE mode = ? AND enabled = 1 ORDER BY provider_id"
)
.all(mode) as UpstreamProxyRow[];
return rows.map((row) => rowToConfig(toRecord(row)));
}
export async function getFallbackChainForProvider(providerId: string) {
const config = await getUpstreamProxyConfig(providerId);
if (!config) return [];
const chain: { executor: "native" | "cliproxyapi"; priority: number }[] = [];
if (config.enabled) {
chain.push({ executor: "native", priority: config.nativePriority });
if (config.mode === "cliproxyapi" || config.mode === "fallback") {
chain.push({ executor: "cliproxyapi", priority: config.cliproxyapiPriority });
}
}
chain.sort((a, b) => a.priority - b.priority);
return chain;
}

View File

@@ -0,0 +1,300 @@
/** Version manager tool state persistence. */
import { getDbInstance } from "./core";
interface VersionManagerRow {
id?: unknown;
tool?: unknown;
current_version?: unknown;
installed_version?: unknown;
pinned_version?: unknown;
binary_path?: unknown;
status?: unknown;
pid?: unknown;
port?: unknown;
api_key?: unknown;
management_key?: unknown;
auto_update?: unknown;
auto_start?: unknown;
last_health_check?: unknown;
last_update_check?: unknown;
health_status?: unknown;
config_overrides?: unknown;
error_message?: unknown;
created_at?: unknown;
updated_at?: unknown;
}
function toRecord(value: unknown): Record<string, unknown> {
return value && typeof value === "object" ? (value as Record<string, unknown>) : {};
}
function parseConfigOverrides(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== "string" || value.trim() === "") return null;
try {
const parsed = JSON.parse(value);
return typeof parsed === "object" && parsed !== null ? parsed : null;
} catch {
return null;
}
}
function stringifyConfigOverrides(value: Record<string, unknown> | null): string | null {
if (value === null) return null;
try {
return JSON.stringify(value);
} catch {
return null;
}
}
interface VersionManagerTool {
id: number;
tool: string;
currentVersion: string | null;
installedVersion: string | null;
pinnedVersion: string | null;
binaryPath: string | null;
status: string;
pid: number | null;
port: number;
apiKey: string | null;
managementKey: string | null;
autoUpdate: boolean;
autoStart: boolean;
lastHealthCheck: string | null;
lastUpdateCheck: string | null;
healthStatus: string;
configOverrides: Record<string, unknown> | null;
errorMessage: string | null;
createdAt: string;
updatedAt: string;
}
function rowToVersionManager(row: VersionManagerRow): VersionManagerTool {
const record = toRecord(row);
return {
id: typeof record.id === "number" ? record.id : 0,
tool: typeof record.tool === "string" ? record.tool : "",
currentVersion:
record.current_version === null
? null
: typeof record.current_version === "string"
? record.current_version
: null,
installedVersion:
record.installed_version === null
? null
: typeof record.installed_version === "string"
? record.installed_version
: null,
pinnedVersion:
record.pinned_version === null
? null
: typeof record.pinned_version === "string"
? record.pinned_version
: null,
binaryPath:
record.binary_path === null
? null
: typeof record.binary_path === "string"
? record.binary_path
: null,
status: typeof record.status === "string" ? record.status : "not_installed",
pid: record.pid === null ? null : typeof record.pid === "number" ? record.pid : null,
port: typeof record.port === "number" ? record.port : 8317,
apiKey:
record.api_key === null ? null : typeof record.api_key === "string" ? record.api_key : null,
managementKey:
record.management_key === null
? null
: typeof record.management_key === "string"
? record.management_key
: null,
autoUpdate:
record.auto_update === 1 || record.auto_update === true || record.auto_update === "1",
autoStart: record.auto_start === 1 || record.auto_start === true || record.auto_start === "1",
lastHealthCheck:
record.last_health_check === null
? null
: typeof record.last_health_check === "string"
? record.last_health_check
: null,
lastUpdateCheck:
record.last_update_check === null
? null
: typeof record.last_update_check === "string"
? record.last_update_check
: null,
healthStatus: typeof record.health_status === "string" ? record.health_status : "unknown",
configOverrides: parseConfigOverrides(record.config_overrides),
errorMessage:
record.error_message === null
? null
: typeof record.error_message === "string"
? record.error_message
: null,
createdAt: typeof record.created_at === "string" ? record.created_at : "",
updatedAt: typeof record.updated_at === "string" ? record.updated_at : "",
};
}
export async function getVersionManagerStatus(): Promise<VersionManagerTool[]> {
const db = getDbInstance();
const rows = db.prepare("SELECT * FROM version_manager").all() as VersionManagerRow[];
return rows.map(rowToVersionManager);
}
export async function getVersionManagerTool(tool: string): Promise<VersionManagerTool | null> {
const db = getDbInstance();
const row = db.prepare("SELECT * FROM version_manager WHERE tool = ?").get(tool) as
| VersionManagerRow
| undefined;
if (!row) return null;
return rowToVersionManager(row);
}
export async function upsertVersionManagerTool(data: {
tool: string;
currentVersion?: string | null;
installedVersion?: string | null;
pinnedVersion?: string | null;
binaryPath?: string | null;
status?: string;
pid?: number | null;
port?: number;
apiKey?: string | null;
managementKey?: string | null;
autoUpdate?: boolean;
autoStart?: boolean;
healthStatus?: string;
configOverrides?: Record<string, unknown> | null;
errorMessage?: string | null;
}): Promise<VersionManagerTool> {
const db = getDbInstance();
db.prepare(
`
INSERT INTO version_manager (
tool, current_version, installed_version, pinned_version, binary_path,
status, pid, port, api_key, management_key, auto_update, auto_start,
health_status, config_overrides, error_message, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
ON CONFLICT(tool) DO UPDATE SET
current_version = excluded.current_version,
installed_version = excluded.installed_version,
pinned_version = excluded.pinned_version,
binary_path = excluded.binary_path,
status = excluded.status,
pid = excluded.pid,
port = excluded.port,
api_key = excluded.api_key,
management_key = excluded.management_key,
auto_update = excluded.auto_update,
auto_start = excluded.auto_start,
health_status = excluded.health_status,
config_overrides = excluded.config_overrides,
error_message = excluded.error_message,
updated_at = datetime('now')
`
).run(
data.tool,
data.currentVersion ?? null,
data.installedVersion ?? null,
data.pinnedVersion ?? null,
data.binaryPath ?? null,
data.status ?? "not_installed",
data.pid ?? null,
data.port ?? 8317,
data.apiKey ?? null,
data.managementKey ?? null,
data.autoUpdate !== undefined ? (data.autoUpdate ? 1 : 0) : 1,
data.autoStart !== undefined ? (data.autoStart ? 1 : 0) : 0,
data.healthStatus ?? "unknown",
stringifyConfigOverrides(data.configOverrides ?? null),
data.errorMessage ?? null
);
const result = await getVersionManagerTool(data.tool);
if (!result) throw new Error("Failed to retrieve inserted version manager tool");
return result;
}
export async function updateVersionManagerTool(
tool: string,
updates: Record<string, unknown>
): Promise<VersionManagerTool | null> {
const db = getDbInstance();
const existing = await getVersionManagerTool(tool);
if (!existing) return null;
const sets: string[] = ["updated_at = datetime('now')"];
const params: Record<string, unknown> = { tool };
for (const [key, value] of Object.entries(updates)) {
const dbKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
if (key === "configOverrides") {
sets.push("config_overrides = @configOverrides");
params.configOverrides = stringifyConfigOverrides(value as Record<string, unknown> | null);
} else if (key === "autoUpdate" || key === "autoStart") {
sets.push(`${dbKey} = @${key}`);
params[key] = value === true ? 1 : 0;
} else if (value === null) {
sets.push(`${dbKey} = null`);
} else {
sets.push(`${dbKey} = @${key}`);
params[key] = value;
}
}
db.prepare(`UPDATE version_manager SET ${sets.join(", ")} WHERE tool = @tool`).run(params);
return getVersionManagerTool(tool);
}
export async function deleteVersionManagerTool(tool: string): Promise<boolean> {
const db = getDbInstance();
const result = db.prepare("DELETE FROM version_manager WHERE tool = ?").run(tool);
return result.changes > 0;
}
export async function updateToolHealth(tool: string, healthStatus: string): Promise<boolean> {
const db = getDbInstance();
const result = db
.prepare(
"UPDATE version_manager SET health_status = ?, last_health_check = datetime('now') WHERE tool = ?"
)
.run(healthStatus, tool);
return result.changes > 0;
}
export async function updateToolVersion(
tool: string,
field: "current_version" | "installed_version",
version: string
): Promise<boolean> {
const db = getDbInstance();
const result = db
.prepare(`UPDATE version_manager SET ${field} = ?, updated_at = datetime('now') WHERE tool = ?`)
.run(version, tool);
return result.changes > 0;
}
export async function setToolStatus(
tool: string,
status: string,
pid?: number,
errorMessage?: string
): Promise<boolean> {
const db = getDbInstance();
const result = db
.prepare(
pid !== undefined
? "UPDATE version_manager SET status = ?, pid = ?, error_message = ?, updated_at = datetime('now') WHERE tool = ?"
: "UPDATE version_manager SET status = ?, error_message = ?, updated_at = datetime('now') WHERE tool = ?"
)
.run(
...(pid !== undefined
? [status, pid, errorMessage ?? null, tool]
: [status, errorMessage ?? null, tool])
);
return result.changes > 0;
}

View File

@@ -210,6 +210,28 @@ export {
export type { QuotaSnapshotRow, ProviderUtilizationPoint } from "@/shared/types/utilization";
export {
getVersionManagerStatus,
getVersionManagerTool,
upsertVersionManagerTool,
updateVersionManagerTool,
deleteVersionManagerTool,
updateToolHealth,
updateToolVersion,
setToolStatus,
} from "./db/versionManager";
export {
getUpstreamProxyConfigs,
getUpstreamProxyConfig,
upsertUpstreamProxyConfig,
updateUpstreamProxyConfig,
deleteUpstreamProxyConfig,
getProvidersByMode,
getFallbackChainForProvider,
validateProxyUrl,
} from "./db/upstreamProxy";
export {
getProviderLimitsCache,
getAllProviderLimitsCache,

View File

@@ -611,12 +611,35 @@ export function isAnthropicCompatibleProvider(providerId) {
return typeof providerId === "string" && providerId.startsWith(ANTHROPIC_COMPATIBLE_PREFIX);
}
export const UPSTREAM_PROXY_PROVIDERS = {
cliproxyapi: {
id: "cliproxyapi",
alias: "cpa",
name: "CLIProxyAPI",
icon: "proxy",
color: "#6366F1",
textIcon: "CPA",
website: "https://github.com/router-for-me/CLIProxyAPI",
defaultPort: 8317,
healthEndpoint: "/v1/models",
managementPrefix: "/v0/management",
configDir: "~/.cli-proxy-api",
binaryName: "cli-proxy-api",
githubRepo: "router-for-me/CLIProxyAPI",
},
};
export function isClaudeCodeCompatibleProvider(providerId) {
return typeof providerId === "string" && providerId.startsWith(CLAUDE_CODE_COMPATIBLE_PREFIX);
}
// All providers (combined)
export const AI_PROVIDERS = { ...FREE_PROVIDERS, ...OAUTH_PROVIDERS, ...APIKEY_PROVIDERS };
export const AI_PROVIDERS = {
...FREE_PROVIDERS,
...OAUTH_PROVIDERS,
...APIKEY_PROVIDERS,
...UPSTREAM_PROXY_PROVIDERS,
};
// Auth methods
export const AUTH_METHODS = {

View File

@@ -1320,3 +1320,11 @@ export const updateAutoDisableAccountsSchema = z
threshold: z.number().int().min(1).max(10).optional(),
})
.strict();
export const versionManagerToolSchema = z.object({
tool: z.string().trim().min(1),
});
export const versionManagerInstallSchema = versionManagerToolSchema.extend({
version: z.string().trim().optional(),
});

View File

@@ -0,0 +1,340 @@
import { describe, it, beforeEach, afterEach, after } from "node:test";
import assert from "node:assert/strict";
import path from "node:path";
import fs from "node:fs";
import os from "node:os";
import Database from "better-sqlite3";
const fileTmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "omni-dbupc-test-"));
const SCHEMA = `
CREATE TABLE IF NOT EXISTS upstream_proxy_config (
id INTEGER PRIMARY KEY AUTOINCREMENT,
provider_id TEXT NOT NULL UNIQUE,
mode TEXT NOT NULL DEFAULT 'native',
cliproxyapi_model_mapping TEXT,
native_priority INTEGER NOT NULL DEFAULT 1,
cliproxyapi_priority INTEGER NOT NULL DEFAULT 2,
enabled INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
`;
let testDb;
let testDbPath;
beforeEach(() => {
testDbPath = path.join(fileTmpDir, `test-${Date.now()}.db`);
testDb = new Database(testDbPath);
testDb.exec(SCHEMA);
});
afterEach(() => {
testDb.close();
try {
fs.unlinkSync(testDbPath);
} catch {}
});
after(() => {
if (fs.existsSync(fileTmpDir)) fs.rmSync(fileTmpDir, { recursive: true, force: true });
});
function upsert(db, data) {
db.prepare(
`
INSERT OR REPLACE INTO upstream_proxy_config
(provider_id, mode, cliproxyapi_model_mapping, native_priority, cliproxyapi_priority, enabled, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
`
).run(
data.providerId,
data.mode ?? "native",
data.cliproxyapiModelMapping !== undefined
? JSON.stringify(data.cliproxyapiModelMapping)
: null,
data.nativePriority ?? 1,
data.cliproxyapiPriority ?? 2,
data.enabled !== false ? 1 : 0
);
return getConfig(db, data.providerId);
}
function getConfig(db, providerId) {
const row = db
.prepare("SELECT * FROM upstream_proxy_config WHERE provider_id = ?")
.get(providerId);
if (!row) return null;
return {
id: row.id,
providerId: row.provider_id,
mode: row.mode,
cliproxyapiModelMapping:
row.cliproxyapi_model_mapping && typeof row.cliproxyapi_model_mapping === "string"
? JSON.parse(row.cliproxyapi_model_mapping)
: null,
nativePriority: row.native_priority,
cliproxyapiPriority: row.cliproxyapi_priority,
enabled: row.enabled === 1,
createdAt: row.created_at,
updatedAt: row.updated_at,
};
}
function getAllConfigs(db) {
return db
.prepare("SELECT * FROM upstream_proxy_config ORDER BY provider_id")
.all()
.map((r) => ({
id: r.id,
providerId: r.provider_id,
mode: r.mode,
cliproxyapiModelMapping:
r.cliproxyapi_model_mapping && typeof r.cliproxyapi_model_mapping === "string"
? JSON.parse(r.cliproxyapi_model_mapping)
: null,
nativePriority: r.native_priority,
cliproxyapiPriority: r.cliproxyapi_priority,
enabled: r.enabled === 1,
createdAt: r.created_at,
updatedAt: r.updated_at,
}));
}
function getProvidersByMode(db, mode) {
return db
.prepare(
"SELECT * FROM upstream_proxy_config WHERE mode = ? AND enabled = 1 ORDER BY provider_id"
)
.all(mode)
.map((r) => ({
id: r.id,
providerId: r.provider_id,
mode: r.mode,
cliproxyapiModelMapping: r.cliproxyapi_model_mapping
? JSON.parse(r.cliproxyapi_model_mapping)
: null,
nativePriority: r.native_priority,
cliproxyapiPriority: r.cliproxyapi_priority,
enabled: r.enabled === 1,
}));
}
function getFallbackChain(db, providerId) {
const config = getConfig(db, providerId);
if (!config) return [];
const chain = [];
if (config.enabled) {
chain.push({ executor: "native", priority: config.nativePriority });
if (config.mode === "cliproxyapi" || config.mode === "fallback") {
chain.push({ executor: "cliproxyapi", priority: config.cliproxyapiPriority });
}
}
chain.sort((a, b) => a.priority - b.priority);
return chain;
}
describe("db/upstreamProxy (logic)", () => {
describe("upsertUpstreamProxyConfig", () => {
it("should insert and read back", () => {
const config = upsert(testDb, { providerId: "claude", mode: "native" });
assert.equal(config.providerId, "claude");
assert.equal(config.mode, "native");
assert.equal(config.enabled, true);
assert.equal(config.nativePriority, 1);
assert.equal(config.cliproxyapiPriority, 2);
assert.equal(config.cliproxyapiModelMapping, null);
assert.ok(config.id > 0);
});
it("should replace on conflict", () => {
upsert(testDb, { providerId: "t", mode: "native" });
const replaced = upsert(testDb, { providerId: "t", mode: "fallback" });
assert.equal(replaced.mode, "fallback");
});
it("should store model mapping as JSON", () => {
const mapping = { "ag/gemini-3-pro": "gemini-3-pro-high" };
const config = upsert(testDb, { providerId: "mapped", cliproxyapiModelMapping: mapping });
assert.deepEqual(config.cliproxyapiModelMapping, mapping);
});
it("should store null model mapping", () => {
const config = upsert(testDb, { providerId: "nomap", cliproxyapiModelMapping: null });
assert.equal(config.cliproxyapiModelMapping, null);
});
it("should handle enabled=false", () => {
assert.equal(upsert(testDb, { providerId: "dis", enabled: false }).enabled, false);
});
it("should set custom priorities", () => {
const config = upsert(testDb, {
providerId: "pri",
nativePriority: 5,
cliproxyapiPriority: 10,
});
assert.equal(config.nativePriority, 5);
assert.equal(config.cliproxyapiPriority, 10);
});
});
describe("getUpstreamProxyConfig", () => {
it("should return null for non-existent provider", () => {
assert.equal(getConfig(testDb, "ghost"), null);
});
it("should return config for provider", () => {
upsert(testDb, { providerId: "claude", mode: "fallback" });
assert.equal(getConfig(testDb, "claude").mode, "fallback");
});
});
describe("getUpstreamProxyConfigs", () => {
it("should return all ordered by provider_id", () => {
upsert(testDb, { providerId: "zebra" });
upsert(testDb, { providerId: "alpha" });
const configs = getAllConfigs(testDb);
assert.equal(configs.length, 2);
assert.equal(configs[0].providerId, "alpha");
assert.equal(configs[1].providerId, "zebra");
});
it("should return empty array", () => {
assert.equal(getAllConfigs(testDb).length, 0);
});
});
describe("updateUpstreamProxyConfig", () => {
it("should update individual fields", () => {
upsert(testDb, { providerId: "u", mode: "native" });
testDb
.prepare(
"UPDATE upstream_proxy_config SET mode = ?, updated_at = datetime('now') WHERE provider_id = ?"
)
.run("cliproxyapi", "u");
assert.equal(getConfig(testDb, "u").mode, "cliproxyapi");
});
it("should update model mapping", () => {
upsert(testDb, { providerId: "u2" });
testDb
.prepare(
"UPDATE upstream_proxy_config SET cliproxyapi_model_mapping = ?, updated_at = datetime('now') WHERE provider_id = ?"
)
.run(JSON.stringify({ k: "v" }), "u2");
assert.deepEqual(getConfig(testDb, "u2").cliproxyapiModelMapping, { k: "v" });
});
it("should update multiple fields", () => {
upsert(testDb, { providerId: "m" });
testDb
.prepare(
"UPDATE upstream_proxy_config SET mode = ?, native_priority = ?, enabled = ?, updated_at = datetime('now') WHERE provider_id = ?"
)
.run("fallback", 3, 0, "m");
const config = getConfig(testDb, "m");
assert.equal(config.mode, "fallback");
assert.equal(config.nativePriority, 3);
assert.equal(config.enabled, false);
});
it("should set model mapping to null", () => {
upsert(testDb, { providerId: "n", cliproxyapiModelMapping: { a: 1 } });
testDb
.prepare(
"UPDATE upstream_proxy_config SET cliproxyapi_model_mapping = NULL WHERE provider_id = ?"
)
.run("n");
assert.equal(getConfig(testDb, "n").cliproxyapiModelMapping, null);
});
});
describe("deleteUpstreamProxyConfig", () => {
it("should delete existing config", () => {
upsert(testDb, { providerId: "del" });
assert.equal(
testDb.prepare("DELETE FROM upstream_proxy_config WHERE provider_id = ?").run("del")
.changes,
1
);
assert.equal(getConfig(testDb, "del"), null);
});
it("should return 0 for non-existent", () => {
assert.equal(
testDb.prepare("DELETE FROM upstream_proxy_config WHERE provider_id = ?").run("ghost")
.changes,
0
);
});
});
describe("getProvidersByMode", () => {
it("should filter by mode and enabled", () => {
upsert(testDb, { providerId: "p1", mode: "fallback", enabled: true });
upsert(testDb, { providerId: "p2", mode: "fallback", enabled: true });
upsert(testDb, { providerId: "p3", mode: "native", enabled: true });
upsert(testDb, { providerId: "p4", mode: "fallback", enabled: false });
const results = getProvidersByMode(testDb, "fallback");
assert.equal(results.length, 2);
assert.ok(results.every((r) => r.enabled));
});
it("should return empty for no matches", () => {
assert.equal(getProvidersByMode(testDb, "cliproxyapi").length, 0);
});
});
describe("getFallbackChainForProvider", () => {
it("should return empty for non-existent provider", () => {
assert.deepEqual(getFallbackChain(testDb, "ghost"), []);
});
it("should return native-only for native mode", () => {
upsert(testDb, { providerId: "native-only", mode: "native" });
const chain = getFallbackChain(testDb, "native-only");
assert.equal(chain.length, 1);
assert.equal(chain[0].executor, "native");
});
it("should return native+cliproxyapi for fallback mode", () => {
upsert(testDb, {
providerId: "fb",
mode: "fallback",
nativePriority: 1,
cliproxyapiPriority: 2,
});
const chain = getFallbackChain(testDb, "fb");
assert.equal(chain.length, 2);
assert.equal(chain[0].executor, "native");
assert.equal(chain[1].executor, "cliproxyapi");
});
it("should return both for cliproxyapi mode", () => {
upsert(testDb, { providerId: "cpa", mode: "cliproxyapi" });
const chain = getFallbackChain(testDb, "cpa");
assert.equal(chain.length, 2);
});
it("should sort by priority", () => {
upsert(testDb, {
providerId: "rev",
mode: "fallback",
nativePriority: 10,
cliproxyapiPriority: 1,
});
const chain = getFallbackChain(testDb, "rev");
assert.equal(chain[0].executor, "cliproxyapi");
assert.equal(chain[0].priority, 1);
assert.equal(chain[1].executor, "native");
assert.equal(chain[1].priority, 10);
});
it("should return empty chain when disabled", () => {
upsert(testDb, { providerId: "dis-fb", mode: "fallback", enabled: false });
assert.deepEqual(getFallbackChain(testDb, "dis-fb"), []);
});
});
});

View File

@@ -0,0 +1,368 @@
import { describe, it, beforeEach, afterEach, after } from "node:test";
import assert from "node:assert/strict";
import path from "node:path";
import fs from "node:fs";
import os from "node:os";
import Database from "better-sqlite3";
const fileTmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "omni-dbvm-test-"));
const SCHEMA = `
CREATE TABLE IF NOT EXISTS version_manager (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tool TEXT NOT NULL UNIQUE,
current_version TEXT,
installed_version TEXT,
pinned_version TEXT,
binary_path TEXT,
status TEXT NOT NULL DEFAULT 'not_installed',
pid INTEGER,
port INTEGER DEFAULT 8317,
api_key TEXT,
management_key TEXT,
auto_update INTEGER NOT NULL DEFAULT 1,
auto_start INTEGER NOT NULL DEFAULT 0,
last_health_check TEXT,
last_update_check TEXT,
health_status TEXT DEFAULT 'unknown',
config_overrides TEXT,
error_message TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
`;
let testDb;
let testDbPath;
beforeEach(() => {
testDbPath = path.join(fileTmpDir, `test-${Date.now()}.db`);
testDb = new Database(testDbPath);
testDb.exec(SCHEMA);
});
afterEach(() => {
testDb.close();
try {
fs.unlinkSync(testDbPath);
} catch {}
});
after(() => {
if (fs.existsSync(fileTmpDir)) fs.rmSync(fileTmpDir, { recursive: true, force: true });
});
function upsertTool(db, data) {
db.prepare(
`
INSERT INTO version_manager (
tool, current_version, installed_version, pinned_version, binary_path,
status, pid, port, api_key, management_key, auto_update, auto_start,
health_status, config_overrides, error_message, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
ON CONFLICT(tool) DO UPDATE SET
current_version = excluded.current_version,
installed_version = excluded.installed_version,
pinned_version = excluded.pinned_version,
binary_path = excluded.binary_path,
status = excluded.status,
pid = excluded.pid,
port = excluded.port,
api_key = excluded.api_key,
management_key = excluded.management_key,
auto_update = excluded.auto_update,
auto_start = excluded.auto_start,
health_status = excluded.health_status,
config_overrides = excluded.config_overrides,
error_message = excluded.error_message,
updated_at = datetime('now')
`
).run(
data.tool,
data.currentVersion ?? null,
data.installedVersion ?? null,
data.pinnedVersion ?? null,
data.binaryPath ?? null,
data.status ?? "not_installed",
data.pid ?? null,
data.port ?? 8317,
data.apiKey ?? null,
data.managementKey ?? null,
data.autoUpdate !== undefined ? (data.autoUpdate ? 1 : 0) : 1,
data.autoStart !== undefined ? (data.autoStart ? 1 : 0) : 0,
data.healthStatus ?? "unknown",
data.configOverrides !== undefined
? data.configOverrides
? JSON.stringify(data.configOverrides)
: null
: null,
data.errorMessage ?? null
);
return db.prepare("SELECT * FROM version_manager WHERE tool = ?").get(data.tool);
}
function parseConfigOverrides(value) {
if (!value || typeof value !== "string" || value.trim() === "") return null;
try {
const parsed = JSON.parse(value);
return typeof parsed === "object" && parsed !== null ? parsed : null;
} catch {
return null;
}
}
function toTool(row) {
if (!row) return null;
return {
id: row.id,
tool: row.tool,
currentVersion: row.current_version,
installedVersion: row.installed_version,
pinnedVersion: row.pinned_version,
binaryPath: row.binary_path,
status: row.status,
pid: row.pid,
port: row.port,
apiKey: row.api_key,
managementKey: row.management_key,
autoUpdate: row.auto_update === 1,
autoStart: row.auto_start === 1,
lastHealthCheck: row.last_health_check,
lastUpdateCheck: row.last_update_check,
healthStatus: row.health_status,
configOverrides: parseConfigOverrides(row.config_overrides),
errorMessage: row.error_message,
createdAt: row.created_at,
updatedAt: row.updated_at,
};
}
describe("db/versionManager (logic)", () => {
describe("upsertVersionManagerTool", () => {
it("should insert a new tool and read it back", () => {
const tool = toTool(
upsertTool(testDb, {
tool: "cliproxyapi",
installedVersion: "6.9.7",
binaryPath: "/tmp/bin/cliproxyapi",
status: "installed",
})
);
assert.equal(tool.tool, "cliproxyapi");
assert.equal(tool.installedVersion, "6.9.7");
assert.equal(tool.binaryPath, "/tmp/bin/cliproxyapi");
assert.equal(tool.status, "installed");
assert.equal(tool.port, 8317);
assert.equal(tool.autoUpdate, true);
assert.equal(tool.autoStart, false);
assert.equal(tool.healthStatus, "unknown");
assert.ok(tool.id > 0);
});
it("should update on conflict (upsert)", () => {
upsertTool(testDb, { tool: "test-tool", status: "installed" });
const tool = toTool(
upsertTool(testDb, { tool: "test-tool", installedVersion: "7.0.0", status: "running" })
);
assert.equal(tool.installedVersion, "7.0.0");
assert.equal(tool.status, "running");
});
it("should store boolean fields as 0/1", () => {
const tool = toTool(
upsertTool(testDb, { tool: "bool-test", autoUpdate: false, autoStart: true })
);
assert.equal(tool.autoUpdate, false);
assert.equal(tool.autoStart, true);
});
it("should store config overrides as JSON", () => {
const tool = toTool(upsertTool(testDb, { tool: "cfg", configOverrides: { port: 9999 } }));
assert.deepEqual(tool.configOverrides, { port: 9999 });
});
it("should handle null config overrides", () => {
const tool = toTool(upsertTool(testDb, { tool: "null-cfg" }));
assert.equal(tool.configOverrides, null);
});
it("should handle pid and error message", () => {
const tool = toTool(
upsertTool(testDb, { tool: "pid-test", pid: 12345, errorMessage: "err" })
);
assert.equal(tool.pid, 12345);
assert.equal(tool.errorMessage, "err");
});
});
describe("getVersionManagerTool", () => {
it("should return null for non-existent tool", () => {
assert.equal(
toTool(testDb.prepare("SELECT * FROM version_manager WHERE tool = ?").get("ghost")),
null
);
});
it("should return tool by name", () => {
upsertTool(testDb, { tool: "findme", installedVersion: "1.0.0" });
const tool = toTool(
testDb.prepare("SELECT * FROM version_manager WHERE tool = ?").get("findme")
);
assert.equal(tool.installedVersion, "1.0.0");
});
});
describe("getVersionManagerStatus", () => {
it("should return all tools", () => {
upsertTool(testDb, { tool: "a" });
upsertTool(testDb, { tool: "b" });
assert.equal(testDb.prepare("SELECT * FROM version_manager").all().length, 2);
});
it("should return empty when no tools", () => {
assert.equal(testDb.prepare("SELECT * FROM version_manager").all().length, 0);
});
});
describe("updateVersionManagerTool (partial)", () => {
it("should update pinnedVersion and autoUpdate", () => {
upsertTool(testDb, { tool: "upd", status: "installed" });
testDb
.prepare(
"UPDATE version_manager SET pinned_version = ?, auto_update = ?, updated_at = datetime('now') WHERE tool = ?"
)
.run("6.8.0", 0, "upd");
const tool = toTool(
testDb.prepare("SELECT * FROM version_manager WHERE tool = ?").get("upd")
);
assert.equal(tool.pinnedVersion, "6.8.0");
assert.equal(tool.autoUpdate, false);
assert.equal(tool.status, "installed");
});
it("should set fields to null", () => {
upsertTool(testDb, { tool: "nulls", installedVersion: "1.0.0", pid: 42 });
testDb
.prepare("UPDATE version_manager SET installed_version = NULL, pid = NULL WHERE tool = ?")
.run("nulls");
const tool = toTool(
testDb.prepare("SELECT * FROM version_manager WHERE tool = ?").get("nulls")
);
assert.equal(tool.installedVersion, null);
assert.equal(tool.pid, null);
});
});
describe("deleteVersionManagerTool", () => {
it("should delete existing tool", () => {
upsertTool(testDb, { tool: "del" });
assert.equal(
testDb.prepare("DELETE FROM version_manager WHERE tool = ?").run("del").changes,
1
);
assert.equal(
testDb.prepare("SELECT * FROM version_manager WHERE tool = ?").get("del"),
undefined
);
});
it("should return 0 changes for non-existent", () => {
assert.equal(
testDb.prepare("DELETE FROM version_manager WHERE tool = ?").run("ghost").changes,
0
);
});
});
describe("updateToolHealth", () => {
it("should update health_status and last_health_check", () => {
upsertTool(testDb, { tool: "h" });
const r = testDb
.prepare(
"UPDATE version_manager SET health_status = ?, last_health_check = datetime('now') WHERE tool = ?"
)
.run("healthy", "h");
assert.equal(r.changes, 1);
const tool = toTool(testDb.prepare("SELECT * FROM version_manager WHERE tool = ?").get("h"));
assert.equal(tool.healthStatus, "healthy");
assert.ok(tool.lastHealthCheck);
});
it("should return 0 changes for non-existent", () => {
assert.equal(
testDb
.prepare("UPDATE version_manager SET health_status = ? WHERE tool = ?")
.run("healthy", "ghost").changes,
0
);
});
});
describe("updateToolVersion", () => {
it("should update current_version", () => {
upsertTool(testDb, { tool: "v1" });
testDb
.prepare("UPDATE version_manager SET current_version = ? WHERE tool = ?")
.run("7.0.0", "v1");
assert.equal(
testDb.prepare("SELECT * FROM version_manager WHERE tool = ?").get("v1").current_version,
"7.0.0"
);
});
it("should update installed_version", () => {
upsertTool(testDb, { tool: "v2" });
testDb
.prepare("UPDATE version_manager SET installed_version = ? WHERE tool = ?")
.run("7.0.0", "v2");
assert.equal(
testDb.prepare("SELECT * FROM version_manager WHERE tool = ?").get("v2").installed_version,
"7.0.0"
);
});
});
describe("setToolStatus", () => {
it("should update status with pid", () => {
upsertTool(testDb, { tool: "s1" });
testDb
.prepare("UPDATE version_manager SET status = ?, pid = ?, error_message = ? WHERE tool = ?")
.run("running", 9999, "ok", "s1");
const row = testDb.prepare("SELECT * FROM version_manager WHERE tool = ?").get("s1");
assert.equal(row.status, "running");
assert.equal(row.pid, 9999);
assert.equal(row.error_message, "ok");
});
it("should update status without pid", () => {
upsertTool(testDb, { tool: "s2" });
testDb
.prepare("UPDATE version_manager SET status = ?, error_message = ? WHERE tool = ?")
.run("error", "crashed", "s2");
const row = testDb.prepare("SELECT * FROM version_manager WHERE tool = ?").get("s2");
assert.equal(row.status, "error");
});
it("should return 0 for non-existent", () => {
assert.equal(
testDb
.prepare("UPDATE version_manager SET status = ? WHERE tool = ?")
.run("running", "ghost").changes,
0
);
});
});
describe("parseConfigOverrides", () => {
it("should parse valid JSON", () => {
assert.deepEqual(parseConfigOverrides('{"key":"val"}'), { key: "val" });
});
it("should return null for invalid JSON", () => {
assert.equal(parseConfigOverrides("not-json"), null);
assert.equal(parseConfigOverrides(""), null);
assert.equal(parseConfigOverrides(null), null);
assert.equal(parseConfigOverrides("123"), null);
});
});
});