| "use server"; |
|
|
| import { NextResponse } from "next/server"; |
| import fs from "fs/promises"; |
| import path from "path"; |
| import os from "os"; |
| import { exec } from "child_process"; |
| import { promisify } from "util"; |
| import { parseTOML, stringifyTOML } from "confbox"; |
|
|
| const execAsync = promisify(exec); |
|
|
| const getJcodeConfigDir = () => path.join(os.homedir(), ".jcode"); |
| const getConfigPath = () => path.join(getJcodeConfigDir(), "config.toml"); |
|
|
| const getProviderEnvPath = () => { |
| const configDir = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"); |
| return path.join(configDir, "jcode", "provider-9router.env"); |
| }; |
|
|
| const checkJcodeInstalled = async () => { |
| try { |
| const isWindows = os.platform() === "win32"; |
| const command = isWindows ? "where jcode" : "which jcode"; |
| await execAsync(command, { windowsHide: true }); |
| return true; |
| } catch { |
| try { |
| await fs.access(getJcodeConfigDir()); |
| return true; |
| } catch { |
| return false; |
| } |
| } |
| }; |
|
|
| const readConfig = async () => { |
| try { |
| const configPath = getConfigPath(); |
| const content = await fs.readFile(configPath, "utf-8"); |
| return parseTOML(content); |
| } catch (error) { |
| return { providers: {} }; |
| } |
| }; |
|
|
| const has9RouterConfig = (config) => { |
| if (!config || !config.providers) return false; |
|
|
| const providers = config.providers; |
|
|
| if (providers["9router"]) return true; |
|
|
| for (const [name, provider] of Object.entries(providers)) { |
| if (provider.base_url && provider.base_url.includes("localhost:20128")) { |
| return true; |
| } |
| } |
|
|
| return false; |
| }; |
|
|
| const writeConfig = async (config) => { |
| const configPath = getConfigPath(); |
| const content = stringifyTOML(config); |
| await fs.writeFile(configPath, content, "utf-8"); |
| }; |
|
|
| const readProviderEnv = async () => { |
| try { |
| const envPath = getProviderEnvPath(); |
| const content = await fs.readFile(envPath, "utf-8"); |
| const env = {}; |
|
|
| for (const line of content.split("\n")) { |
| const trimmed = line.trim(); |
| if (!trimmed || trimmed.startsWith("#")) continue; |
|
|
| const eqIndex = trimmed.indexOf("="); |
| if (eqIndex > 0) { |
| const key = trimmed.slice(0, eqIndex).trim(); |
| let value = trimmed.slice(eqIndex + 1).trim(); |
|
|
| if ((value.startsWith('"') && value.endsWith('"')) || |
| (value.startsWith("'") && value.endsWith("'"))) { |
| value = value.slice(1, -1); |
| } |
|
|
| env[key] = value; |
| } |
| } |
|
|
| return env; |
| } catch { |
| return {}; |
| } |
| }; |
|
|
| const writeProviderEnv = async (env) => { |
| const envPath = getProviderEnvPath(); |
| let content = "# jcode provider environment variables\n"; |
|
|
| for (const [key, value] of Object.entries(env)) { |
| content += `${key}="${value}"\n`; |
| } |
|
|
| await fs.writeFile(envPath, content, "utf-8"); |
| }; |
|
|
| export async function GET() { |
| const isInstalled = await checkJcodeInstalled(); |
|
|
| if (!isInstalled) { |
| return NextResponse.json({ |
| installed: false, |
| message: "jcode not installed. Install via: curl -fsSL https://raw.githubusercontent.com/1jehuang/jcode/master/scripts/install.sh | bash", |
| }); |
| } |
|
|
| const config = await readConfig(); |
| const has9Router = has9RouterConfig(config); |
|
|
| return NextResponse.json({ |
| installed: true, |
| config, |
| has9Router, |
| configPath: getConfigPath(), |
| }); |
| } |
|
|
| export async function POST(request) { |
| try { |
| const { baseUrl, apiKey, models } = await request.json(); |
|
|
| if (!baseUrl || !apiKey) { |
| return NextResponse.json( |
| { error: "baseUrl and apiKey are required" }, |
| { status: 400 } |
| ); |
| } |
|
|
| const normalizedBaseUrl = baseUrl.endsWith("/v1") |
| ? baseUrl |
| : `${baseUrl}/v1`; |
|
|
| let config = await readConfig(); |
|
|
| if (!config.providers) { |
| config.providers = {}; |
| } |
|
|
| config.providers["9router"] = { |
| type: "openai-compatible", |
| base_url: normalizedBaseUrl, |
| auth: "bearer", |
| api_key_env: "JCODE_9ROUTER_API_KEY", |
| env_file: "provider-9router.env", |
| default_model: models && models.length > 0 ? models[0] : "cc/claude-opus-4-7", |
| requires_api_key: true, |
| }; |
|
|
| const configDir = getJcodeConfigDir(); |
| await fs.mkdir(configDir, { recursive: true }); |
|
|
| await writeConfig(config); |
|
|
| const xdgConfigDir = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"); |
| const jcodeConfigDir = path.join(xdgConfigDir, "jcode"); |
| await fs.mkdir(jcodeConfigDir, { recursive: true }); |
|
|
| const env = await readProviderEnv(); |
| env.JCODE_9ROUTER_API_KEY = apiKey; |
| await writeProviderEnv(env); |
|
|
| return NextResponse.json({ |
| success: true, |
| message: "jcode configured successfully. Use: jcode --provider-profile 9router", |
| configPath: getConfigPath(), |
| }); |
| } catch (error) { |
| console.error("Error configuring jcode:", error); |
| return NextResponse.json( |
| { error: error.message }, |
| { status: 500 } |
| ); |
| } |
| } |
|
|
| export async function DELETE() { |
| try { |
| const config = await readConfig(); |
|
|
| if (!config.providers) { |
| return NextResponse.json({ success: true, message: "No configuration to remove" }); |
| } |
|
|
| delete config.providers["9router"]; |
|
|
| await writeConfig(config); |
|
|
| const env = await readProviderEnv(); |
| delete env.JCODE_9ROUTER_API_KEY; |
| await writeProviderEnv(env); |
|
|
| return NextResponse.json({ |
| success: true, |
| message: "9router configuration removed from jcode", |
| }); |
| } catch (error) { |
| console.error("Error removing jcode configuration:", error); |
| return NextResponse.json( |
| { error: error.message }, |
| { status: 500 } |
| ); |
| } |
| } |
|
|