mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
Merge F3 into parent: settings handlers for new custom configType tools (plan 14)
This commit is contained in:
206
src/app/api/cli-tools/deepseek-tui-settings/route.ts
Normal file
206
src/app/api/cli-tools/deepseek-tui-settings/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
204
src/app/api/cli-tools/forge-settings/route.ts
Normal file
204
src/app/api/cli-tools/forge-settings/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
229
src/app/api/cli-tools/jcode-settings/route.ts
Normal file
229
src/app/api/cli-tools/jcode-settings/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
229
src/app/api/cli-tools/pi-settings/route.ts
Normal file
229
src/app/api/cli-tools/pi-settings/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
229
src/app/api/cli-tools/smelt-settings/route.ts
Normal file
229
src/app/api/cli-tools/smelt-settings/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
193
tests/integration/cli-settings-deepseek-tui.test.ts
Normal file
193
tests/integration/cli-settings-deepseek-tui.test.ts
Normal 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;
|
||||
});
|
||||
201
tests/integration/cli-settings-forge.test.ts
Normal file
201
tests/integration/cli-settings-forge.test.ts
Normal 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;
|
||||
});
|
||||
196
tests/integration/cli-settings-jcode.test.ts
Normal file
196
tests/integration/cli-settings-jcode.test.ts
Normal 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;
|
||||
});
|
||||
196
tests/integration/cli-settings-pi.test.ts
Normal file
196
tests/integration/cli-settings-pi.test.ts
Normal 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;
|
||||
});
|
||||
196
tests/integration/cli-settings-smelt.test.ts
Normal file
196
tests/integration/cli-settings-smelt.test.ts
Normal 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;
|
||||
});
|
||||
Reference in New Issue
Block a user