mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 20:00:47 +00:00
feat: v0.4.41 - cli-tools UI redesign + jcode integration
- Add jcode CLI tool (#1047) with auto-configuration - Redesign CLI Tools dashboard: grid 1/2/3 cols + per-tool detail page - Sync DeepSeek TUI card style + resize icon 1024->128 - Add official logos: amp, jcode, qwen - Bump version 0.4.39 -> 0.4.41 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import { GET as copilotGet } from "../copilot-settings/route";
|
||||
import { GET as clineGet } from "../cline-settings/route";
|
||||
import { GET as kiloGet } from "../kilo-settings/route";
|
||||
import { GET as deepseekTuiGet } from "../deepseek-tui-settings/route";
|
||||
import { GET as jcodeGet } from "../jcode-settings/route";
|
||||
|
||||
const STATUS_GETTERS = {
|
||||
claude: claudeGet,
|
||||
@@ -25,6 +26,7 @@ const STATUS_GETTERS = {
|
||||
cline: clineGet,
|
||||
kilo: kiloGet,
|
||||
"deepseek-tui": deepseekTuiGet,
|
||||
jcode: jcodeGet,
|
||||
};
|
||||
|
||||
// Batch endpoint: gather all CLI tool statuses in one round-trip
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
"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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user