| "use server";
|
|
|
| import { NextResponse } from "next/server";
|
| import fs from "fs/promises";
|
| import pino from "pino";
|
|
|
| import { buildErrorBody } from "@omniroute/open-sse/utils/error.ts";
|
|
|
| import { requireCliToolsAuth } from "@/lib/api/requireCliToolsAuth";
|
| import { CLI_TOOLS } from "@/shared/constants/cliTools";
|
| import { getCliRuntimeStatus, getCliPrimaryConfigPath } from "@/shared/services/cliRuntime";
|
| import { getAllCliToolLastConfigured } from "@/lib/db/cliToolState";
|
| import { checkToolConfigStatus } from "@/lib/cliTools/checkToolConfigStatus";
|
| import { getCached, setCached } from "@/lib/cliTools/batchStatusCache";
|
| import type { ToolBatchStatus, ToolBatchStatusMap } from "@/shared/types/cliBatchStatus";
|
|
|
| const logger = pino({ name: "cli-tools-all-statuses-api" });
|
|
|
| const TOOL_CHECK_TIMEOUT_MS = 5000;
|
|
|
| |
| |
| |
|
|
| async function extractEndpointFromConfig(
|
| toolId: string,
|
| configPath: string
|
| ): Promise<string | null> {
|
| try {
|
| const content = await fs.readFile(configPath, "utf-8");
|
|
|
|
|
| if (toolId === "codex") {
|
| const match = content.match(/base_url\s*=\s*["']([^"'\n]+)["']/i);
|
| return match ? match[1] : null;
|
| }
|
|
|
| const config = JSON.parse(content) as Record<string, unknown>;
|
|
|
| switch (toolId) {
|
| case "claude": {
|
| const env = config.env as Record<string, unknown> | undefined;
|
| return (env?.ANTHROPIC_BASE_URL as string | undefined) ?? null;
|
| }
|
| case "qwen": {
|
| const mp = config.modelProviders as Record<string, unknown>[] | undefined;
|
| if (!Array.isArray(mp)) return null;
|
| for (const provider of mp) {
|
| const baseUrl = (provider as Record<string, unknown>).apiBase as string | undefined;
|
| if (baseUrl) return baseUrl;
|
| }
|
| return null;
|
| }
|
| case "cline":
|
| return (config.openAiBaseUrl as string | undefined) ?? null;
|
| case "droid":
|
| case "openclaw":
|
| case "kilo": {
|
|
|
| for (const key of ["baseUrl", "apiBase", "openaiBaseUrl", "baseURL", "endpoint"]) {
|
| const value = config[key];
|
| if (typeof value === "string" && value.startsWith("http")) return value;
|
| }
|
| return null;
|
| }
|
| case "hermes": {
|
|
|
| const match = content.match(/base_url\s*=\s*["']([^"'\n]+)["']/i);
|
| return match ? match[1] : null;
|
| }
|
| default:
|
| return null;
|
| }
|
| } catch {
|
| return null;
|
| }
|
| }
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| export async function GET(request: Request): Promise<Response> {
|
| const authError = await requireCliToolsAuth(request);
|
| if (authError) return authError;
|
|
|
| try {
|
| const toolIds = Object.keys(CLI_TOOLS);
|
| const statuses: ToolBatchStatusMap = {};
|
|
|
|
|
| const mtimesMap: Record<string, number> = {};
|
| await Promise.allSettled(
|
| toolIds.map(async (toolId) => {
|
| const configPath = getCliPrimaryConfigPath(toolId);
|
| if (!configPath) {
|
| mtimesMap[toolId] = 0;
|
| return;
|
| }
|
| try {
|
| const stat = await fs.stat(configPath);
|
| mtimesMap[toolId] = stat.mtimeMs;
|
| } catch {
|
| mtimesMap[toolId] = 0;
|
| }
|
| })
|
| );
|
|
|
|
|
| await Promise.allSettled(
|
| toolIds.map(async (toolId) => {
|
| const mtimeMs = mtimesMap[toolId] ?? 0;
|
| const cached = getCached(toolId, mtimeMs);
|
|
|
| if (cached) {
|
| statuses[toolId] = cached;
|
| return;
|
| }
|
|
|
| try {
|
| const runtimePromise = Promise.race<Awaited<ReturnType<typeof getCliRuntimeStatus>>>([
|
| getCliRuntimeStatus(toolId),
|
| new Promise<never>((_, reject) =>
|
| setTimeout(() => reject(new Error("Timeout")), TOOL_CHECK_TIMEOUT_MS)
|
| ),
|
| ]);
|
|
|
| const configStatusPromise = checkToolConfigStatus(toolId);
|
|
|
| const [runtimeResult, configStatusResult] = await Promise.allSettled([
|
| runtimePromise,
|
| configStatusPromise,
|
| ]);
|
|
|
| const runtime =
|
| runtimeResult.status === "fulfilled"
|
| ? runtimeResult.value
|
| : { installed: false, runnable: false, reason: "Timeout" };
|
|
|
| const configStatus =
|
| configStatusResult.status === "fulfilled" ? configStatusResult.value : "unknown";
|
|
|
|
|
| const effectiveConfigStatus =
|
| !runtime.installed || !runtime.runnable ? "not_installed" : configStatus;
|
|
|
|
|
| const configPath = getCliPrimaryConfigPath(toolId);
|
| const endpoint = configPath
|
| ? await extractEndpointFromConfig(toolId, configPath)
|
| : null;
|
|
|
| const result: ToolBatchStatus = {
|
| detection: {
|
| installed: runtime.installed,
|
| runnable: runtime.runnable,
|
| command: runtime.command ?? undefined,
|
| commandPath: (runtime as Record<string, unknown>).commandPath as string | undefined,
|
| reason: runtime.reason ?? undefined,
|
| },
|
| config: {
|
| status: effectiveConfigStatus,
|
| endpoint: endpoint ?? null,
|
| },
|
| };
|
|
|
| setCached(toolId, mtimeMs, result);
|
| statuses[toolId] = result;
|
| } catch (toolErr) {
|
| const errMsg =
|
| toolErr instanceof Error && toolErr.message === "Timeout" ? "Timeout" : "Check failed";
|
| logger.warn({ toolId, err: toolErr }, "Failed to check CLI tool status");
|
|
|
| const result: ToolBatchStatus = {
|
| detection: { installed: false, runnable: false, reason: errMsg },
|
| config: { status: "unknown" },
|
| error: errMsg,
|
| };
|
| statuses[toolId] = result;
|
| }
|
| })
|
| );
|
|
|
|
|
| try {
|
| const lastConfigured = getAllCliToolLastConfigured();
|
| for (const [toolId, timestamp] of Object.entries(lastConfigured)) {
|
| if (statuses[toolId]) {
|
| statuses[toolId].config.lastConfiguredAt = timestamp;
|
| }
|
| }
|
| } catch (dbErr) {
|
| logger.warn({ err: dbErr }, "Failed to fetch lastConfiguredAt timestamps");
|
| }
|
|
|
| return NextResponse.json(statuses);
|
| } catch (err) {
|
| logger.error({ err }, "Unexpected error in /api/cli-tools/all-statuses");
|
| return NextResponse.json(buildErrorBody(500, err instanceof Error ? err.message : String(err)), {
|
| status: 500,
|
| });
|
| }
|
| }
|
|
|