Merge F3 into parent: settings handlers for new custom configType tools (plan 14)

This commit is contained in:
diegosouzapw
2026-05-27 22:43:21 -03:00
11 changed files with 2125 additions and 0 deletions

View File

@@ -0,0 +1,206 @@
"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 = "deepseek-tui";
const getDeepseekTuiConfigPath = (): string =>
getCliPrimaryConfigPath(TOOL_ID) ??
path.join(process.env.HOME ?? "~", ".config", "deepseek-tui", "config.toml");
const getDeepseekTuiDir = () => path.dirname(getDeepseekTuiConfigPath());
/**
* Render the OmniRoute config block in DeepSeek TUI TOML format.
* DeepSeek TUI reads OPENAI_BASE_URL and OPENAI_API_KEY from its config.
* Reference: https://github.com/hunterbown/deepseek-tui
*/
function renderDeepseekTuiConfig(baseUrl: string, apiKey: string, model: string): string {
return [
"# DeepSeek TUI 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
const readConfig = async (): Promise<string | null> => {
try {
return await fs.readFile(getDeepseekTuiConfigPath(), "utf-8");
} catch (err: any) {
if (err.code === "ENOENT") return null;
throw err;
}
};
// GET — check deepseek-tui 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
? "DeepSeek TUI is installed but not runnable"
: "DeepSeek TUI 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: getDeepseekTuiConfigPath(),
});
} catch (err) {
return NextResponse.json(
{ error: { message: sanitizeErrorMessage(err) } },
{ status: 500 }
);
}
}
// POST — write OmniRoute settings to DeepSeek TUI config.toml
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 configPath = getDeepseekTuiConfigPath();
const configDir = getDeepseekTuiDir();
// Ensure directory exists
await fs.mkdir(configDir, { recursive: true });
// Backup current config before modifying
await createBackup(TOOL_ID, configPath);
// Write new config (full replace — simple TOML file)
const content = renderDeepseekTuiConfig(baseUrl, apiKey, model);
await fs.writeFile(configPath, content, "utf-8");
// Persist last-configured timestamp
try {
saveCliToolLastConfigured(TOOL_ID);
} catch {
/* non-critical */
}
return NextResponse.json({
success: true,
message: "DeepSeek TUI settings applied successfully!",
configPath,
});
} catch (err) {
return NextResponse.json(
{ error: { message: sanitizeErrorMessage(err) } },
{ status: 500 }
);
}
}
// DELETE — remove DeepSeek TUI OmniRoute config
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 configPath = getDeepseekTuiConfigPath();
// Backup before removing
await createBackup(TOOL_ID, configPath);
await fs.rm(configPath, { force: true });
// Clear last-configured timestamp
try {
deleteCliToolLastConfigured(TOOL_ID);
} catch {
/* non-critical */
}
return NextResponse.json({
success: true,
message: "DeepSeek TUI settings removed successfully",
});
} catch (err) {
return NextResponse.json(
{ error: { message: sanitizeErrorMessage(err) } },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,204 @@
"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 = "forge";
const getForgeConfigPath = (): string =>
getCliPrimaryConfigPath(TOOL_ID) ?? path.join(process.env.HOME ?? "~", ".forge", "config.toml");
const getForgeDir = () => path.dirname(getForgeConfigPath());
/**
* Render the OmniRoute provider block in Forge TOML format.
* Forge uses a TOML config at ~/.forge/config.toml with an [openai] section.
* Reference: https://github.com/antinomyhq/forge
*/
function renderForgeConfig(baseUrl: string, apiKey: string, model: string): string {
const normalizedBaseUrl = baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`;
return [
"# Forge config — managed by OmniRoute (plan 14)",
"",
"[openai]",
`api_key = "${apiKey}"`,
`base_url = "${normalizedBaseUrl}"`,
`model = "${model}"`,
"",
].join("\n");
}
/**
* Check if the config file contains OmniRoute settings.
* Looks for the managed-by-OmniRoute marker comment.
*/
const hasOmniRouteConfig = (content: string | null): boolean => {
if (!content) return false;
return content.includes("managed by OmniRoute");
};
// Read current config.toml
const readConfig = async (): Promise<string | null> => {
try {
return await fs.readFile(getForgeConfigPath(), "utf-8");
} catch (err: any) {
if (err.code === "ENOENT") return null;
throw err;
}
};
// GET — check forge 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
? "Forge CLI is installed but not runnable"
: "Forge CLI 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: getForgeConfigPath(),
});
} catch (err) {
return NextResponse.json(
{ error: { message: sanitizeErrorMessage(err) } },
{ status: 500 }
);
}
}
// POST — write OmniRoute settings to Forge config.toml
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 configPath = getForgeConfigPath();
const forgeDir = getForgeDir();
// Ensure directory exists
await fs.mkdir(forgeDir, { recursive: true });
// Backup current config before modifying
await createBackup(TOOL_ID, configPath);
// Write new config (full replace — Forge config is simple)
const content = renderForgeConfig(baseUrl, apiKey, model);
await fs.writeFile(configPath, content, "utf-8");
// Persist last-configured timestamp
try {
saveCliToolLastConfigured(TOOL_ID);
} catch {
/* non-critical */
}
return NextResponse.json({
success: true,
message: "Forge settings applied successfully!",
configPath,
});
} catch (err) {
return NextResponse.json(
{ error: { message: sanitizeErrorMessage(err) } },
{ status: 500 }
);
}
}
// DELETE — remove Forge OmniRoute config
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 configPath = getForgeConfigPath();
// Backup before removing
await createBackup(TOOL_ID, configPath);
await fs.rm(configPath, { force: true });
// Clear last-configured timestamp
try {
deleteCliToolLastConfigured(TOOL_ID);
} catch {
/* non-critical */
}
return NextResponse.json({ success: true, message: "Forge settings removed successfully" });
} catch (err) {
return NextResponse.json(
{ error: { message: sanitizeErrorMessage(err) } },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,229 @@
"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 = "jcode";
const getJcodeConfigPath = (): string =>
getCliPrimaryConfigPath(TOOL_ID) ?? path.join(process.env.HOME ?? "~", ".jcode", "config.json");
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"
);
};
// Read current config.json
const readConfig = async (): Promise<Record<string, unknown> | null> => {
try {
const content = await fs.readFile(getJcodeConfigPath(), "utf-8");
return JSON.parse(content) as Record<string, unknown>;
} catch (err: any) {
if (err.code === "ENOENT") return null;
throw err;
}
};
// GET — check jcode 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
? "jcode CLI is installed but not runnable"
: "jcode CLI 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: getJcodeConfigPath(),
});
} catch (err) {
return NextResponse.json(
{ error: { message: sanitizeErrorMessage(err) } },
{ status: 500 }
);
}
}
// POST — write OmniRoute settings to jcode config.json
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 configPath = getJcodeConfigPath();
const jcodeDir = getJcodeDir();
// Ensure directory exists
await fs.mkdir(jcodeDir, { recursive: true });
// Backup current config before modifying
await createBackup(TOOL_ID, configPath);
// Read existing config or start fresh
let existing: Record<string, unknown> = {};
try {
const raw = await fs.readFile(configPath, "utf-8");
existing = JSON.parse(raw) as Record<string, unknown>;
} 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",
};
await fs.writeFile(configPath, JSON.stringify(updated, null, 2), "utf-8");
// Persist last-configured timestamp
try {
saveCliToolLastConfigured(TOOL_ID);
} catch {
/* non-critical */
}
return NextResponse.json({
success: true,
message: "jcode settings applied successfully!",
configPath,
});
} catch (err) {
return NextResponse.json(
{ error: { message: sanitizeErrorMessage(err) } },
{ status: 500 }
);
}
}
// DELETE — remove OmniRoute settings from jcode config
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 configPath = getJcodeConfigPath();
// Backup before modifying
await createBackup(TOOL_ID, configPath);
// Read existing config
let existing: Record<string, unknown> = {};
try {
const raw = await fs.readFile(configPath, "utf-8");
existing = JSON.parse(raw) as Record<string, unknown>;
} catch (err: any) {
if (err.code === "ENOENT") {
return NextResponse.json({ success: true, message: "No config file to reset" });
}
throw err;
}
// Remove OmniRoute-managed fields
delete existing.baseUrl;
delete existing.apiKey;
delete existing.model;
delete existing._managedBy;
if (Object.keys(existing).length === 0) {
await fs.rm(configPath, { force: true });
} else {
await fs.writeFile(configPath, JSON.stringify(existing, null, 2), "utf-8");
}
// Clear last-configured timestamp
try {
deleteCliToolLastConfigured(TOOL_ID);
} catch {
/* non-critical */
}
return NextResponse.json({ success: true, message: "jcode OmniRoute settings removed" });
} catch (err) {
return NextResponse.json(
{ error: { message: sanitizeErrorMessage(err) } },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,229 @@
"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 = "pi";
const getPiConfigPath = (): string =>
getCliPrimaryConfigPath(TOOL_ID) ?? path.join(process.env.HOME ?? "~", ".pi", "config.json");
const getPiDir = () => path.dirname(getPiConfigPath());
/**
* 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"
);
};
// Read current config.json
const readConfig = async (): Promise<Record<string, unknown> | null> => {
try {
const content = await fs.readFile(getPiConfigPath(), "utf-8");
return JSON.parse(content) as Record<string, unknown>;
} catch (err: any) {
if (err.code === "ENOENT") return null;
throw err;
}
};
// GET — check pi 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
? "Pi CLI is installed but not runnable"
: "Pi CLI 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: getPiConfigPath(),
});
} catch (err) {
return NextResponse.json(
{ error: { message: sanitizeErrorMessage(err) } },
{ status: 500 }
);
}
}
// POST — write OmniRoute settings to Pi config.json
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 configPath = getPiConfigPath();
const piDir = getPiDir();
// Ensure directory exists
await fs.mkdir(piDir, { recursive: true });
// Backup current config before modifying
await createBackup(TOOL_ID, configPath);
// Read existing config or start fresh
let existing: Record<string, unknown> = {};
try {
const raw = await fs.readFile(configPath, "utf-8");
existing = JSON.parse(raw) as Record<string, unknown>;
} catch {
/* No existing config */
}
// Merge OmniRoute settings (pi uses OpenAI-compatible config)
const normalizedBaseUrl = baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`;
const updated: Record<string, unknown> = {
...existing,
baseUrl: normalizedBaseUrl,
apiKey,
model,
_managedBy: "omniroute",
};
await fs.writeFile(configPath, JSON.stringify(updated, null, 2), "utf-8");
// Persist last-configured timestamp
try {
saveCliToolLastConfigured(TOOL_ID);
} catch {
/* non-critical */
}
return NextResponse.json({
success: true,
message: "Pi settings applied successfully!",
configPath,
});
} catch (err) {
return NextResponse.json(
{ error: { message: sanitizeErrorMessage(err) } },
{ status: 500 }
);
}
}
// DELETE — remove OmniRoute settings from Pi config
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 configPath = getPiConfigPath();
// Backup before modifying
await createBackup(TOOL_ID, configPath);
// Read existing config
let existing: Record<string, unknown> = {};
try {
const raw = await fs.readFile(configPath, "utf-8");
existing = JSON.parse(raw) as Record<string, unknown>;
} catch (err: any) {
if (err.code === "ENOENT") {
return NextResponse.json({ success: true, message: "No config file to reset" });
}
throw err;
}
// Remove OmniRoute-managed fields
delete existing.baseUrl;
delete existing.apiKey;
delete existing.model;
delete existing._managedBy;
if (Object.keys(existing).length === 0) {
await fs.rm(configPath, { force: true });
} else {
await fs.writeFile(configPath, JSON.stringify(existing, null, 2), "utf-8");
}
// Clear last-configured timestamp
try {
deleteCliToolLastConfigured(TOOL_ID);
} catch {
/* non-critical */
}
return NextResponse.json({ success: true, message: "Pi OmniRoute settings removed" });
} catch (err) {
return NextResponse.json(
{ error: { message: sanitizeErrorMessage(err) } },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,229 @@
"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 = "smelt";
const getSmeltConfigPath = (): string =>
getCliPrimaryConfigPath(TOOL_ID) ?? path.join(process.env.HOME ?? "~", ".smelt", "config.json");
const getSmeltDir = () => path.dirname(getSmeltConfigPath());
/**
* 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"
);
};
// Read current config.json
const readConfig = async (): Promise<Record<string, unknown> | null> => {
try {
const content = await fs.readFile(getSmeltConfigPath(), "utf-8");
return JSON.parse(content) as Record<string, unknown>;
} catch (err: any) {
if (err.code === "ENOENT") return null;
throw err;
}
};
// GET — check smelt 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
? "Smelt CLI is installed but not runnable"
: "Smelt CLI 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: getSmeltConfigPath(),
});
} catch (err) {
return NextResponse.json(
{ error: { message: sanitizeErrorMessage(err) } },
{ status: 500 }
);
}
}
// POST — write OmniRoute settings to Smelt config.json
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 configPath = getSmeltConfigPath();
const smeltDir = getSmeltDir();
// Ensure directory exists
await fs.mkdir(smeltDir, { recursive: true });
// Backup current config before modifying
await createBackup(TOOL_ID, configPath);
// Read existing config or start fresh
let existing: Record<string, unknown> = {};
try {
const raw = await fs.readFile(configPath, "utf-8");
existing = JSON.parse(raw) as Record<string, unknown>;
} catch {
/* No existing config */
}
// Merge OmniRoute settings (smelt uses OpenAI-compatible config)
const normalizedBaseUrl = baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`;
const updated: Record<string, unknown> = {
...existing,
baseUrl: normalizedBaseUrl,
apiKey,
model,
_managedBy: "omniroute",
};
await fs.writeFile(configPath, JSON.stringify(updated, null, 2), "utf-8");
// Persist last-configured timestamp
try {
saveCliToolLastConfigured(TOOL_ID);
} catch {
/* non-critical */
}
return NextResponse.json({
success: true,
message: "Smelt settings applied successfully!",
configPath,
});
} catch (err) {
return NextResponse.json(
{ error: { message: sanitizeErrorMessage(err) } },
{ status: 500 }
);
}
}
// DELETE — remove OmniRoute settings from Smelt config
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 configPath = getSmeltConfigPath();
// Backup before modifying
await createBackup(TOOL_ID, configPath);
// Read existing config
let existing: Record<string, unknown> = {};
try {
const raw = await fs.readFile(configPath, "utf-8");
existing = JSON.parse(raw) as Record<string, unknown>;
} catch (err: any) {
if (err.code === "ENOENT") {
return NextResponse.json({ success: true, message: "No config file to reset" });
}
throw err;
}
// Remove OmniRoute-managed fields
delete existing.baseUrl;
delete existing.apiKey;
delete existing.model;
delete existing._managedBy;
if (Object.keys(existing).length === 0) {
await fs.rm(configPath, { force: true });
} else {
await fs.writeFile(configPath, JSON.stringify(existing, null, 2), "utf-8");
}
// Clear last-configured timestamp
try {
deleteCliToolLastConfigured(TOOL_ID);
} catch {
/* non-critical */
}
return NextResponse.json({ success: true, message: "Smelt OmniRoute settings removed" });
} catch (err) {
return NextResponse.json(
{ error: { message: sanitizeErrorMessage(err) } },
{ status: 500 }
);
}
}

View File

@@ -188,6 +188,52 @@ const CLI_TOOLS: Record<string, any> = {
settings: ".gemini/settings.json",
},
},
// ── Plan 14 — new "custom" configType tools ───────────────────────────────
forge: {
defaultCommand: "forge",
envBinKey: "CLI_FORGE_BIN",
requiresBinary: true,
healthcheckTimeoutMs: 8000,
paths: {
config: ".forge/config.toml",
},
},
jcode: {
defaultCommand: "jcode",
envBinKey: "CLI_JCODE_BIN",
requiresBinary: true,
healthcheckTimeoutMs: 8000,
paths: {
config: ".jcode/config.json",
},
},
"deepseek-tui": {
defaultCommand: "deepseek-tui",
envBinKey: "CLI_DEEPSEEK_TUI_BIN",
requiresBinary: true,
healthcheckTimeoutMs: 8000,
paths: {
config: ".config/deepseek-tui/config.toml",
},
},
smelt: {
defaultCommand: "smelt",
envBinKey: "CLI_SMELT_BIN",
requiresBinary: true,
healthcheckTimeoutMs: 8000,
paths: {
config: ".smelt/config.json",
},
},
pi: {
defaultCommand: "pi",
envBinKey: "CLI_PI_BIN",
requiresBinary: true,
healthcheckTimeoutMs: 8000,
paths: {
config: ".pi/config.json",
},
},
};
const isWindows = () => process.platform === "win32";

View File

@@ -0,0 +1,193 @@
/**
* Integration tests for /api/cli-tools/deepseek-tui-settings
* Plan 14 F3 — settings handler for DeepSeek TUI (configType: "custom")
*/
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-deepseek-tui-settings-")
);
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = "test-api-key-secret-deepseek-tui";
process.env.JWT_SECRET = "test-jwt-secret-deepseek-tui";
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/deepseek-tui-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("deepseek-tui-settings GET: returns 401 when auth required and no token", async () => {
await enableAuth();
const res = await GET(new Request("http://localhost/api/cli-tools/deepseek-tui-settings"));
assert.equal(res.status, 401, `Expected 401, got ${res.status}`);
});
// ── Test 2: GET without auth requirement → 200 ───────────────────────────────
test("deepseek-tui-settings GET: returns 200 when auth not required", async () => {
const res = await GET(new Request("http://localhost/api/cli-tools/deepseek-tui-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("deepseek-tui-settings POST: 400 when baseUrl is missing", async () => {
const res = await POST(
new Request("http://localhost/api/cli-tools/deepseek-tui-settings", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ apiKey: "sk-test", model: "deepseek-coder" }),
})
);
assert.equal(res.status, 400, `Expected 400, got ${res.status}`);
const body = await res.json();
assert.ok(body.error !== undefined);
});
test("deepseek-tui-settings POST: 400 when model is missing", async () => {
const res = await POST(
new Request("http://localhost/api/cli-tools/deepseek-tui-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 config.toml ───────────────────────
test("deepseek-tui-settings POST: writes config.toml with valid body", async () => {
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "deepseek-tui-home-"));
const origHome = process.env.HOME;
process.env.HOME = tmpHome;
try {
const res = await POST(
new Request("http://localhost/api/cli-tools/deepseek-tui-settings", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
baseUrl: "http://localhost:20128",
apiKey: "sk-test-deepseek-key",
model: "deepseek-coder-v2",
}),
})
);
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, ".config", "deepseek-tui", "config.toml");
if (fs.existsSync(configPath)) {
const content = fs.readFileSync(configPath, "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");
}
}
} finally {
process.env.HOME = origHome;
fs.rmSync(tmpHome, { recursive: true, force: true });
}
});
// ── Test 5: DELETE → removes config file ─────────────────────────────────────
test("deepseek-tui-settings DELETE: removes config file", async () => {
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "deepseek-tui-home-del-"));
const origHome = process.env.HOME;
process.env.HOME = tmpHome;
try {
const configDir = path.join(tmpHome, ".config", "deepseek-tui");
fs.mkdirSync(configDir, { recursive: true });
fs.writeFileSync(
path.join(configDir, "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/deepseek-tui-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);
}
} finally {
process.env.HOME = origHome;
fs.rmSync(tmpHome, { recursive: true, force: true });
}
});
// ── Test 6: Error sanitization (Hard Rule #12) ───────────────────────────────
test("deepseek-tui-settings: error responses do not leak stack traces", async () => {
const badReq = new Request("http://localhost/api/cli-tools/deepseek-tui-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 7: Hard Rule #13 (no exec/spawn) ────────────────────────────────────
test("deepseek-tui-settings route.ts: does not call exec() or spawn() directly", () => {
const routePath = path.resolve(
import.meta.dirname,
"../../src/app/api/cli-tools/deepseek-tui-settings/route.ts"
);
const content = fs.readFileSync(routePath, "utf-8");
assert.ok(!content.match(/\bexec\s*\(/), "Handler must not use exec()");
assert.ok(!content.match(/\bspawn\s*\(/), "Handler must not use spawn()");
});
test.after(async () => {
await resetStorage();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
delete process.env.DATA_DIR;
delete process.env.API_KEY_SECRET;
delete process.env.JWT_SECRET;
});

View File

@@ -0,0 +1,201 @@
/**
* Integration tests for /api/cli-tools/forge-settings
* Plan 14 F3 — settings handler for ForgeCode (configType: "custom")
*/
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";
import { makeManagementSessionRequest } from "../helpers/managementSession.ts";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-forge-settings-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = "test-api-key-secret-forge";
process.env.JWT_SECRET = "test-jwt-secret-forge";
// Import DB reset helpers (must be before route import)
const core = await import("../../src/lib/db/core.ts");
const localDb = await import("../../src/lib/localDb.ts");
// Import route handlers
const { GET, POST, DELETE } = await import(
"../../src/app/api/cli-tools/forge-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 when auth is required → 401 ────────────────────
test("forge-settings GET: returns 401 when auth required and no token", async () => {
await enableAuth();
const res = await GET(new Request("http://localhost/api/cli-tools/forge-settings"));
assert.equal(res.status, 401, `Expected 401, got ${res.status}`);
});
// ── Test 2: GET with valid auth → 200 ────────────────────────────────────────
test("forge-settings GET: returns 200 with valid auth (forge not installed on CI)", async () => {
// No auth required in default test state (no INITIAL_PASSWORD, no requireLogin)
const res = await GET(new Request("http://localhost/api/cli-tools/forge-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("forge-settings POST: 400 when baseUrl is missing", async () => {
const res = await POST(
new Request("http://localhost/api/cli-tools/forge-settings", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ apiKey: "sk-test", model: "gpt-5" }), // missing baseUrl
})
);
assert.equal(res.status, 400, `Expected 400 for missing baseUrl, got ${res.status}`);
const body = await res.json();
assert.ok(body.error !== undefined, "Response should have error field");
});
test("forge-settings POST: 400 when model is missing", async () => {
const res = await POST(
new Request("http://localhost/api/cli-tools/forge-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 for missing model, got ${res.status}`);
});
// ── Test 4: POST with valid body → writes config.toml ──────────────────────
test("forge-settings POST: writes config.toml with valid body", async () => {
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "forge-home-"));
const origHome = process.env.HOME;
process.env.HOME = tmpHome;
try {
const res = await POST(
new Request("http://localhost/api/cli-tools/forge-settings", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
baseUrl: "http://localhost:20128",
apiKey: "sk-test-forge-key",
model: "gpt-5.4-mini",
}),
})
);
// 200 = success; 403 = write guard active (test env); 500 = backup dir issue
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, "success should be true on 200");
const configPath = path.join(tmpHome, ".forge", "config.toml");
if (fs.existsSync(configPath)) {
const content = fs.readFileSync(configPath, "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");
}
}
} finally {
process.env.HOME = origHome;
fs.rmSync(tmpHome, { recursive: true, force: true });
}
});
// ── Test 5: DELETE → removes config file ─────────────────────────────────────
test("forge-settings DELETE: removes config file when it exists", async () => {
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "forge-home-del-"));
const origHome = process.env.HOME;
process.env.HOME = tmpHome;
try {
// Pre-create a config file
const forgeDir = path.join(tmpHome, ".forge");
fs.mkdirSync(forgeDir, { recursive: true });
fs.writeFileSync(
path.join(forgeDir, "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/forge-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);
}
} finally {
process.env.HOME = origHome;
fs.rmSync(tmpHome, { recursive: true, force: true });
}
});
// ── Test 6: Error sanitization (Hard Rule #12) ───────────────────────────────
test("forge-settings: error responses do not leak stack traces", async () => {
const badReq = new Request("http://localhost/api/cli-tools/forge-settings", {
method: "POST",
headers: { "content-type": "application/json" },
body: "{ this is not 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 7: Hard Rule #13 (no exec/spawn) ────────────────────────────────────
test("forge-settings route.ts: does not call exec() or spawn() directly", () => {
const routePath = path.resolve(
import.meta.dirname,
"../../src/app/api/cli-tools/forge-settings/route.ts"
);
const content = fs.readFileSync(routePath, "utf-8");
assert.ok(!content.match(/\bexec\s*\(/), "Handler must not use exec()");
assert.ok(!content.match(/\bspawn\s*\(/), "Handler must not use spawn()");
});
test.after(async () => {
await resetStorage();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
delete process.env.DATA_DIR;
delete process.env.API_KEY_SECRET;
delete process.env.JWT_SECRET;
});

View File

@@ -0,0 +1,196 @@
/**
* Integration tests for /api/cli-tools/jcode-settings
* Plan 14 F3 — settings handler for jcode (configType: "custom")
*/
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-jcode-settings-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = "test-api-key-secret-jcode";
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"
);
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("jcode-settings GET: returns 401 when auth required and no token", async () => {
await enableAuth();
const res = await GET(new Request("http://localhost/api/cli-tools/jcode-settings"));
assert.equal(res.status, 401, `Expected 401, got ${res.status}`);
});
// ── Test 2: GET without auth requirement → 200 ───────────────────────────────
test("jcode-settings GET: returns 200 when auth not required", async () => {
const res = await GET(new Request("http://localhost/api/cli-tools/jcode-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("jcode-settings POST: 400 when baseUrl is missing", async () => {
const res = await POST(
new Request("http://localhost/api/cli-tools/jcode-settings", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ apiKey: "sk-test", model: "gpt-5" }),
})
);
assert.equal(res.status, 400, `Expected 400, got ${res.status}`);
const body = await res.json();
assert.ok(body.error !== undefined);
});
test("jcode-settings POST: 400 when model is missing", async () => {
const res = await POST(
new Request("http://localhost/api/cli-tools/jcode-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 config.json ───────────────────────
test("jcode-settings POST: writes config.json with valid body", async () => {
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "jcode-home-"));
const origHome = process.env.HOME;
process.env.HOME = tmpHome;
try {
const res = await POST(
new Request("http://localhost/api/cli-tools/jcode-settings", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
baseUrl: "http://localhost:20128",
apiKey: "sk-test-jcode-key",
model: "gpt-5.4-mini",
}),
})
);
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");
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");
}
}
} finally {
process.env.HOME = origHome;
fs.rmSync(tmpHome, { recursive: true, force: true });
}
});
// ── Test 5: DELETE → removes OmniRoute fields ────────────────────────────────
test("jcode-settings DELETE: removes OmniRoute fields from existing config", async () => {
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "jcode-home-del-"));
const origHome = process.env.HOME;
process.env.HOME = tmpHome;
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 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}`
);
if (res.status === 200) {
const body = await res.json();
assert.equal(body.success, true);
}
} finally {
process.env.HOME = origHome;
fs.rmSync(tmpHome, { recursive: true, force: true });
}
});
// ── Test 6: Error sanitization (Hard Rule #12) ───────────────────────────────
test("jcode-settings: error responses do not leak stack traces", async () => {
const badReq = new Request("http://localhost/api/cli-tools/jcode-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 7: Hard Rule #13 (no exec/spawn) ────────────────────────────────────
test("jcode-settings route.ts: does not call exec() or spawn() directly", () => {
const routePath = path.resolve(
import.meta.dirname,
"../../src/app/api/cli-tools/jcode-settings/route.ts"
);
const content = fs.readFileSync(routePath, "utf-8");
assert.ok(!content.match(/\bexec\s*\(/), "Handler must not use exec()");
assert.ok(!content.match(/\bspawn\s*\(/), "Handler must not use spawn()");
});
test.after(async () => {
await resetStorage();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
delete process.env.DATA_DIR;
delete process.env.API_KEY_SECRET;
delete process.env.JWT_SECRET;
});

View File

@@ -0,0 +1,196 @@
/**
* Integration tests for /api/cli-tools/pi-settings
* Plan 14 F3 — settings handler for Pi (configType: "custom")
*/
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-pi-settings-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = "test-api-key-secret-pi";
process.env.JWT_SECRET = "test-jwt-secret-pi";
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/pi-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("pi-settings GET: returns 401 when auth required and no token", async () => {
await enableAuth();
const res = await GET(new Request("http://localhost/api/cli-tools/pi-settings"));
assert.equal(res.status, 401, `Expected 401, got ${res.status}`);
});
// ── Test 2: GET without auth requirement → 200 ───────────────────────────────
test("pi-settings GET: returns 200 when auth not required", async () => {
const res = await GET(new Request("http://localhost/api/cli-tools/pi-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("pi-settings POST: 400 when baseUrl is missing", async () => {
const res = await POST(
new Request("http://localhost/api/cli-tools/pi-settings", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ apiKey: "sk-test", model: "gpt-5" }),
})
);
assert.equal(res.status, 400, `Expected 400, got ${res.status}`);
const body = await res.json();
assert.ok(body.error !== undefined);
});
test("pi-settings POST: 400 when model is missing", async () => {
const res = await POST(
new Request("http://localhost/api/cli-tools/pi-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 config.json ───────────────────────
test("pi-settings POST: writes config.json with valid body", async () => {
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "pi-home-"));
const origHome = process.env.HOME;
process.env.HOME = tmpHome;
try {
const res = await POST(
new Request("http://localhost/api/cli-tools/pi-settings", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
baseUrl: "http://localhost:20128",
apiKey: "sk-test-pi-key",
model: "gpt-5.4-mini",
}),
})
);
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, ".pi", "config.json");
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");
}
}
} finally {
process.env.HOME = origHome;
fs.rmSync(tmpHome, { recursive: true, force: true });
}
});
// ── Test 5: DELETE → removes OmniRoute fields ────────────────────────────────
test("pi-settings DELETE: removes OmniRoute fields from existing config", async () => {
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "pi-home-del-"));
const origHome = process.env.HOME;
process.env.HOME = tmpHome;
try {
const piDir = path.join(tmpHome, ".pi");
fs.mkdirSync(piDir, { recursive: true });
fs.writeFileSync(
path.join(piDir, "config.json"),
JSON.stringify({
_managedBy: "omniroute",
baseUrl: "http://localhost:20128",
apiKey: "sk-test",
model: "gpt-5",
})
);
const res = await DELETE(
new Request("http://localhost/api/cli-tools/pi-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);
}
} finally {
process.env.HOME = origHome;
fs.rmSync(tmpHome, { recursive: true, force: true });
}
});
// ── Test 6: Error sanitization (Hard Rule #12) ───────────────────────────────
test("pi-settings: error responses do not leak stack traces", async () => {
const badReq = new Request("http://localhost/api/cli-tools/pi-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 7: Hard Rule #13 (no exec/spawn) ────────────────────────────────────
test("pi-settings route.ts: does not call exec() or spawn() directly", () => {
const routePath = path.resolve(
import.meta.dirname,
"../../src/app/api/cli-tools/pi-settings/route.ts"
);
const content = fs.readFileSync(routePath, "utf-8");
assert.ok(!content.match(/\bexec\s*\(/), "Handler must not use exec()");
assert.ok(!content.match(/\bspawn\s*\(/), "Handler must not use spawn()");
});
test.after(async () => {
await resetStorage();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
delete process.env.DATA_DIR;
delete process.env.API_KEY_SECRET;
delete process.env.JWT_SECRET;
});

View File

@@ -0,0 +1,196 @@
/**
* Integration tests for /api/cli-tools/smelt-settings
* Plan 14 F3 — settings handler for Smelt (configType: "custom")
*/
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-smelt-settings-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = "test-api-key-secret-smelt";
process.env.JWT_SECRET = "test-jwt-secret-smelt";
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/smelt-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("smelt-settings GET: returns 401 when auth required and no token", async () => {
await enableAuth();
const res = await GET(new Request("http://localhost/api/cli-tools/smelt-settings"));
assert.equal(res.status, 401, `Expected 401, got ${res.status}`);
});
// ── Test 2: GET without auth requirement → 200 ───────────────────────────────
test("smelt-settings GET: returns 200 when auth not required", async () => {
const res = await GET(new Request("http://localhost/api/cli-tools/smelt-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("smelt-settings POST: 400 when baseUrl is missing", async () => {
const res = await POST(
new Request("http://localhost/api/cli-tools/smelt-settings", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ apiKey: "sk-test", model: "gpt-5" }),
})
);
assert.equal(res.status, 400, `Expected 400, got ${res.status}`);
const body = await res.json();
assert.ok(body.error !== undefined);
});
test("smelt-settings POST: 400 when model is missing", async () => {
const res = await POST(
new Request("http://localhost/api/cli-tools/smelt-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 config.json ───────────────────────
test("smelt-settings POST: writes config.json with valid body", async () => {
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "smelt-home-"));
const origHome = process.env.HOME;
process.env.HOME = tmpHome;
try {
const res = await POST(
new Request("http://localhost/api/cli-tools/smelt-settings", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
baseUrl: "http://localhost:20128",
apiKey: "sk-test-smelt-key",
model: "gpt-5.4-mini",
}),
})
);
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, ".smelt", "config.json");
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");
}
}
} finally {
process.env.HOME = origHome;
fs.rmSync(tmpHome, { recursive: true, force: true });
}
});
// ── Test 5: DELETE → removes OmniRoute fields ────────────────────────────────
test("smelt-settings DELETE: removes OmniRoute fields from existing config", async () => {
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "smelt-home-del-"));
const origHome = process.env.HOME;
process.env.HOME = tmpHome;
try {
const smeltDir = path.join(tmpHome, ".smelt");
fs.mkdirSync(smeltDir, { recursive: true });
fs.writeFileSync(
path.join(smeltDir, "config.json"),
JSON.stringify({
_managedBy: "omniroute",
baseUrl: "http://localhost:20128",
apiKey: "sk-test",
model: "gpt-5",
})
);
const res = await DELETE(
new Request("http://localhost/api/cli-tools/smelt-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);
}
} finally {
process.env.HOME = origHome;
fs.rmSync(tmpHome, { recursive: true, force: true });
}
});
// ── Test 6: Error sanitization (Hard Rule #12) ───────────────────────────────
test("smelt-settings: error responses do not leak stack traces", async () => {
const badReq = new Request("http://localhost/api/cli-tools/smelt-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 7: Hard Rule #13 (no exec/spawn) ────────────────────────────────────
test("smelt-settings route.ts: does not call exec() or spawn() directly", () => {
const routePath = path.resolve(
import.meta.dirname,
"../../src/app/api/cli-tools/smelt-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;
});