fix(cli): write jcode settings as [providers.omniroute] in config.toml (#11484)

Validated in a combined sub-batch worktree off release/v3.8.51 tip. Its second commit reimplemented wsPath.ts's sanitizeLiveWsPort/resolveLiveWsUrl to fix a build break it hit — that exact break is already fixed on the current tip via #11502 (the original #11388 implementation restored verbatim). Conflicted against that; resolved by keeping the tip's established implementation and dropping the redundant reimplementation (both are functionally equivalent; the tip's carries the original #11331 doc comments and precedence contract). Pushed the same resolution to this branch.
- typecheck:core, file-size, changelog-integrity, complexity, cognitive-complexity — all OK
- Full-repo lint: 228 pre-existing dashboard react-hooks/* findings, unrelated to this diff
- Focused test: cli-settings-jcode.test.ts (integration) — part of sub-batch's 165/165 run

Thanks for the real end-to-end verification (production server, real ~/.jcode/config.toml, jcode --provider-profile omniroute serving a completion, jcode provider-doctor passing) — the managed-block approach (marker-delimited, preserves user-authored config, 409s instead of corrupting on a hand-written conflicting table) is exactly the right shape for this integration.
This commit is contained in:
Justin Hong
2026-08-25 17:11:50 -07:00
committed by GitHub
parent be6cbe7de5
commit b84ae072ea
3 changed files with 134 additions and 89 deletions

View File

@@ -3,6 +3,7 @@
import { NextResponse } from "next/server";
import fs from "fs/promises";
import path from "path";
import { parse as parseToml } from "smol-toml";
import { requireCliToolsAuth } from "@/lib/api/requireCliToolsAuth";
import {
ensureCliConfigWriteAllowed,
@@ -18,28 +19,61 @@ import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
const TOOL_ID = "jcode";
/**
* jcode reads named provider profiles from `[providers.<name>]` tables in
* ~/.jcode/config.toml (TOML, not JSON — the previous revision of this route
* wrote a ~/.jcode/config.json that jcode never reads). Reference:
* https://github.com/1jehuang/jcode#openai-compatible-providers
*
* The OmniRoute-managed profile is kept inside a marker-delimited block so
* apply/reset round-trips without disturbing the rest of the user's config.
*/
const MANAGED_BEGIN = "# >>> managed by OmniRoute (jcode provider profile) >>>";
const MANAGED_END = "# <<< managed by OmniRoute <<<";
const getJcodeConfigPath = (): string =>
getCliPrimaryConfigPath(TOOL_ID) ?? path.join(process.env.HOME ?? "~", ".jcode", "config.json");
getCliPrimaryConfigPath(TOOL_ID) ?? path.join(process.env.HOME ?? "~", ".jcode", "config.toml");
const getJcodeDir = () => path.dirname(getJcodeConfigPath());
/**
* Check if the config file contains OmniRoute settings.
*/
const hasOmniRouteConfig = (settings: Record<string, unknown> | null): boolean => {
if (!settings) return false;
return (
typeof settings.baseUrl === "string" &&
settings.baseUrl.length > 0 &&
settings._managedBy === "omniroute"
);
};
const tomlString = (value: string): string => JSON.stringify(String(value));
// Read current config.json
const readConfig = async (): Promise<Record<string, unknown> | null> => {
/**
* Render the managed `[providers.omniroute]` block. The API key is stored
* inline via jcode's `api_key` field; `requires_api_key = false` keeps a
* keyless local gateway working.
*/
function renderManagedBlock(baseUrl: string, apiKey: string, model: string): string {
const normalizedBaseUrl = baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`;
const lines = [
MANAGED_BEGIN,
"[providers.omniroute]",
'type = "openai-compatible"',
`base_url = ${tomlString(normalizedBaseUrl)}`,
];
if (apiKey) lines.push(`api_key = ${tomlString(apiKey)}`);
lines.push(`default_model = ${tomlString(model)}`, "requires_api_key = false", MANAGED_END);
return lines.join("\n");
}
const hasOmniRouteConfig = (content: string | null): boolean =>
Boolean(content && content.includes(MANAGED_BEGIN));
/** Strip the managed block (including surrounding blank padding) from config text. */
function stripManagedBlock(content: string): string {
const begin = content.indexOf(MANAGED_BEGIN);
if (begin === -1) return content;
const endMarker = content.indexOf(MANAGED_END, begin);
const end = endMarker === -1 ? content.length : endMarker + MANAGED_END.length;
const before = content.slice(0, begin).replace(/\n+$/, "\n");
const after = content.slice(end).replace(/^\n+/, "\n");
return (before + after).replace(/^\n+/, "");
}
// Read current config.toml
const readConfig = async (): Promise<string | null> => {
try {
const content = await fs.readFile(getJcodeConfigPath(), "utf-8");
return JSON.parse(content) as Record<string, unknown>;
return await fs.readFile(getJcodeConfigPath(), "utf-8");
} catch (err) {
if ((err as NodeJS.ErrnoException).code === "ENOENT") return null;
throw err;
@@ -84,14 +118,11 @@ export async function GET(request: Request) {
configPath: getJcodeConfigPath(),
});
} catch (err) {
return NextResponse.json(
{ error: { message: sanitizeErrorMessage(err) } },
{ status: 500 }
);
return NextResponse.json({ error: { message: sanitizeErrorMessage(err) } }, { status: 500 });
}
}
// POST — write OmniRoute settings to jcode config.json
// POST — write the OmniRoute provider profile into jcode's config.toml
export async function POST(request: Request) {
const authError = await requireCliToolsAuth(request);
if (authError) return authError;
@@ -100,10 +131,7 @@ export async function POST(request: Request) {
try {
rawBody = await request.json();
} catch {
return NextResponse.json(
{ error: { message: "Invalid JSON body" } },
{ status: 400 }
);
return NextResponse.json({ error: { message: "Invalid JSON body" } }, { status: 400 });
}
try {
@@ -131,26 +159,50 @@ export async function POST(request: Request) {
// Backup current config before modifying
await createBackup(TOOL_ID, configPath);
// Read existing config or start fresh
let existing: Record<string, unknown> = {};
// Read existing config (TOML text) or start fresh
let existing = "";
try {
const raw = await fs.readFile(configPath, "utf-8");
existing = JSON.parse(raw) as Record<string, unknown>;
existing = await fs.readFile(configPath, "utf-8");
} catch {
/* No existing config */
}
// Merge OmniRoute settings (jcode uses OpenAI-compatible config)
const normalizedBaseUrl = baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`;
const updated: Record<string, unknown> = {
...existing,
baseUrl: normalizedBaseUrl,
apiKey,
model,
_managedBy: "omniroute",
};
// Refuse to double-define the table if the user hand-wrote a
// [providers.omniroute] profile outside our managed block — duplicate
// TOML tables would make the whole config unparseable for jcode.
const unmanaged = stripManagedBlock(existing);
try {
if (unmanaged.trim()) {
const parsed = parseToml(unmanaged) as { providers?: Record<string, unknown> };
if (parsed.providers && Object.hasOwn(parsed.providers, "omniroute")) {
return NextResponse.json(
{
error: {
message:
"config.toml already defines [providers.omniroute] outside the OmniRoute-managed block; remove it or manage it manually",
},
},
{ status: 409 }
);
}
}
} catch {
return NextResponse.json(
{
error: {
message:
"existing ~/.jcode/config.toml is not valid TOML; fix it before applying OmniRoute settings",
},
},
{ status: 409 }
);
}
await fs.writeFile(configPath, JSON.stringify(updated, null, 2), "utf-8");
const base = unmanaged.trimEnd();
const block = renderManagedBlock(baseUrl, apiKey ?? "", model);
const updated = base ? `${base}\n\n${block}\n` : `${block}\n`;
await fs.writeFile(configPath, updated, "utf-8");
// Persist last-configured timestamp
try {
@@ -161,18 +213,16 @@ export async function POST(request: Request) {
return NextResponse.json({
success: true,
message: "jcode settings applied successfully!",
message:
"jcode settings applied! Start jcode with `jcode --provider-profile omniroute` or pick the profile with /model.",
configPath,
});
} catch (err) {
return NextResponse.json(
{ error: { message: sanitizeErrorMessage(err) } },
{ status: 500 }
);
return NextResponse.json({ error: { message: sanitizeErrorMessage(err) } }, { status: 500 });
}
}
// DELETE — remove OmniRoute settings from jcode config
// DELETE — remove the OmniRoute-managed block from jcode's config.toml
export async function DELETE(request: Request) {
const authError = await requireCliToolsAuth(request);
if (authError) return authError;
@@ -188,11 +238,9 @@ export async function DELETE(request: Request) {
// Backup before modifying
await createBackup(TOOL_ID, configPath);
// Read existing config
let existing: Record<string, unknown> = {};
let existing: string;
try {
const raw = await fs.readFile(configPath, "utf-8");
existing = JSON.parse(raw) as Record<string, unknown>;
existing = await fs.readFile(configPath, "utf-8");
} catch (err) {
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
return NextResponse.json({ success: true, message: "No config file to reset" });
@@ -200,16 +248,12 @@ export async function DELETE(request: Request) {
throw err;
}
// Remove OmniRoute-managed fields
delete existing.baseUrl;
delete existing.apiKey;
delete existing.model;
delete existing._managedBy;
const remaining = stripManagedBlock(existing);
if (Object.keys(existing).length === 0) {
if (!remaining.trim()) {
await fs.rm(configPath, { force: true });
} else {
await fs.writeFile(configPath, JSON.stringify(existing, null, 2), "utf-8");
await fs.writeFile(configPath, remaining, "utf-8");
}
// Clear last-configured timestamp
@@ -221,9 +265,6 @@ export async function DELETE(request: Request) {
return NextResponse.json({ success: true, message: "jcode OmniRoute settings removed" });
} catch (err) {
return NextResponse.json(
{ error: { message: sanitizeErrorMessage(err) } },
{ status: 500 }
);
return NextResponse.json({ error: { message: sanitizeErrorMessage(err) } }, { status: 500 });
}
}

View File

@@ -242,7 +242,7 @@ const CLI_TOOLS: Record<string, any> = {
requiresBinary: true,
healthcheckTimeoutMs: 8000,
paths: {
config: ".jcode/config.json",
config: ".jcode/config.toml",
},
},
"prime-agent": {

View File

@@ -16,9 +16,7 @@ process.env.JWT_SECRET = "test-jwt-secret-jcode";
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/jcode-settings/route.ts"
);
const { GET, POST, DELETE } = await import("../../src/app/api/cli-tools/jcode-settings/route.ts");
async function resetStorage() {
delete process.env.INITIAL_PASSWORD;
@@ -82,9 +80,9 @@ test("jcode-settings POST: 400 when model is missing", async () => {
assert.equal(res.status, 400, `Expected 400, got ${res.status}`);
});
// ── Test 4: POST with valid body → writes config.json ───────────────────────
// ── Test 4: POST with valid body → writes provider profile into config.toml ──
test("jcode-settings POST: writes config.json with valid body", async () => {
test("jcode-settings POST: writes [providers.omniroute] into config.toml", async () => {
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "jcode-home-"));
const origHome = process.env.HOME;
process.env.HOME = tmpHome;
@@ -101,19 +99,18 @@ test("jcode-settings POST: writes config.json with valid body", async () => {
}),
})
);
assert.ok(
[200, 403, 500].includes(res.status),
`Unexpected status ${res.status}`
);
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 configPath = path.join(tmpHome, ".jcode", "config.json");
const configPath = path.join(tmpHome, ".jcode", "config.toml");
if (fs.existsSync(configPath)) {
const written = JSON.parse(fs.readFileSync(configPath, "utf-8"));
assert.equal(written._managedBy, "omniroute");
assert.ok(written.baseUrl.includes("localhost:20128"));
assert.equal(written.model, "gpt-5.4-mini");
const written = fs.readFileSync(configPath, "utf-8");
assert.ok(written.includes("managed by OmniRoute"));
assert.ok(written.includes("[providers.omniroute]"));
assert.ok(written.includes('type = "openai-compatible"'));
assert.ok(written.includes("localhost:20128/v1"));
assert.ok(written.includes('default_model = "gpt-5.4-mini"'));
}
}
} finally {
@@ -124,7 +121,7 @@ test("jcode-settings POST: writes config.json with valid body", async () => {
// ── Test 5: DELETE → removes OmniRoute fields ────────────────────────────────
test("jcode-settings DELETE: removes OmniRoute fields from existing config", async () => {
test("jcode-settings DELETE: removes only the OmniRoute-managed block", async () => {
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "jcode-home-del-"));
const origHome = process.env.HOME;
process.env.HOME = tmpHome;
@@ -132,26 +129,33 @@ test("jcode-settings DELETE: removes OmniRoute fields from existing config", asy
try {
const jcodeDir = path.join(tmpHome, ".jcode");
fs.mkdirSync(jcodeDir, { recursive: true });
fs.writeFileSync(
path.join(jcodeDir, "config.json"),
JSON.stringify({
_managedBy: "omniroute",
baseUrl: "http://localhost:20128",
apiKey: "sk-test",
model: "gpt-5",
})
);
const userSection = '[provider]\ndefault_model = "claude-opus-4-8"\n';
const managedBlock = [
"# >>> managed by OmniRoute (jcode provider profile) >>>",
"[providers.omniroute]",
'type = "openai-compatible"',
'base_url = "http://localhost:20128/v1"',
'api_key = "sk-test"',
'default_model = "gpt-5"',
"requires_api_key = false",
"# <<< managed by OmniRoute <<<",
].join("\n");
fs.writeFileSync(path.join(jcodeDir, "config.toml"), `${userSection}\n${managedBlock}\n`);
const res = await DELETE(
new Request("http://localhost/api/cli-tools/jcode-settings", { method: "DELETE" })
);
assert.ok(
[200, 403, 500].includes(res.status),
`Expected 200/403/500, got ${res.status}`
);
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);
const configPath = path.join(jcodeDir, "config.toml");
if (fs.existsSync(configPath)) {
const remaining = fs.readFileSync(configPath, "utf-8");
assert.ok(!remaining.includes("managed by OmniRoute"));
assert.ok(!remaining.includes("[providers.omniroute]"));
assert.ok(remaining.includes('default_model = "claude-opus-4-8"'));
}
}
} finally {
process.env.HOME = origHome;