diff --git a/.dockerignore b/.dockerignore
deleted file mode 100644
index 86b7318..0000000
--- a/.dockerignore
+++ /dev/null
@@ -1,17 +0,0 @@
-.git
-.DS_Store
-.env
-.env.*
-node_modules
-dist
-packages/*/dist
-release
-release-local
-logs
-tmp
-test-results
-playwright-report
-blob-report
-docs/node_modules
-docs/dist
-docs/.astro
diff --git a/.gitignore b/.gitignore
index 6efc5ae..291dafe 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,8 +13,4 @@ release
tmp
release-local
logs
-.opencat
-test-results
-playwright-report
-blob-report
-.tmp
\ No newline at end of file
+.opencat
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index 4c0c868..0000000
--- a/Dockerfile
+++ /dev/null
@@ -1,75 +0,0 @@
-ARG NODE_IMAGE=node:22-bookworm
-ARG RUNTIME_NODE_IMAGE=node:22-bookworm-slim
-
-FROM ${NODE_IMAGE} AS build
-WORKDIR /app
-
-COPY package.json package-lock.json ./
-COPY packages/cli/package.json packages/cli/package.json
-COPY packages/core/package.json packages/core/package.json
-COPY packages/electron/package.json packages/electron/package.json
-COPY packages/ui/package.json packages/ui/package.json
-RUN npm ci
-
-COPY . .
-RUN npm run build:docker
-
-FROM ${NODE_IMAGE} AS production-deps
-WORKDIR /app
-ENV NODE_ENV=production
-
-COPY package.json package-lock.json ./
-COPY packages/core/package.json packages/core/package.json
-RUN npm ci --omit=dev --workspace=@claude-code-router/core --include-workspace-root=false \
- && npm cache clean --force
-
-FROM ${RUNTIME_NODE_IMAGE} AS runtime
-ENV NODE_ENV=production \
- CCR_DATA_DIR=/data \
- CCR_WEB_HOST=127.0.0.1 \
- CCR_WEB_PORT=3459 \
- CCR_NGINX_PORT=8080 \
- CCR_GATEWAY_HOST=127.0.0.1 \
- CCR_GATEWAY_PORT=3456 \
- CCR_GATEWAY_CORE_PORT=3457 \
- CCR_PUBLIC_HOST=127.0.0.1 \
- CCR_PUBLIC_PORT=3458
-
-WORKDIR /app
-
-RUN apt-get update \
- && apt-get install -y --no-install-recommends ca-certificates libstdc++6 nginx \
- && rm -rf /var/lib/apt/lists/* \
- && rm -f /etc/nginx/sites-enabled/default /etc/nginx/conf.d/default.conf \
- && rm -rf \
- /opt/yarn-* \
- /usr/local/bin/corepack \
- /usr/local/bin/npm \
- /usr/local/bin/npx \
- /usr/local/bin/yarn \
- /usr/local/bin/yarnpkg \
- /usr/local/include/node \
- /usr/local/lib/node_modules/corepack \
- /usr/local/lib/node_modules/npm \
- /usr/local/share/doc \
- /usr/local/share/man
-
-COPY package.json package-lock.json ./
-COPY packages/core/package.json packages/core/package.json
-COPY --from=production-deps /app/node_modules node_modules
-
-COPY --from=build /app/packages/core/dist packages/core/dist
-COPY --from=build /app/packages/ui/dist/renderer /usr/share/nginx/html
-COPY docker/entrypoint.sh /usr/local/bin/ccr-docker-entrypoint
-COPY docker/pm2.config.cjs docker/pm2.config.cjs
-
-RUN chmod +x /usr/local/bin/ccr-docker-entrypoint \
- && mkdir -p /data /run/nginx /var/lib/nginx /var/log/nginx
-
-VOLUME ["/data"]
-EXPOSE 8080
-
-HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
- CMD node -e "fetch('http://127.0.0.1:' + (process.env.CCR_NGINX_PORT || '8080') + '/').then((r) => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"
-
-ENTRYPOINT ["ccr-docker-entrypoint"]
diff --git a/README.md b/README.md
index f32ddf1..95847ab 100644
--- a/README.md
+++ b/README.md
@@ -1,52 +1,19 @@
-
Claude Code Router
+Claude Code Router Desktop
-
-
-
-
-
-
-
-
-
-
-
- Kimi Code Subscription
- ·
- API Global
- ·
- API China
-
-
-
-
-
-
- Thanks to Kimi for sponsoring this project! Kimi K2.7 Code is an open-source, coding-focused agentic model developed by Moonshot AI, with substantial gains on real-world long-horizon coding tasks and higher end-to-end success across complex software engineering workflows. It also cuts thinking-token usage by approximately 30% compared with K2.6. Inside CCR, Kimi ships as built-in provider presets: import the pay-as-you-go API or the Kimi Code subscription in one click and route your coding agent's requests to Kimi, the subscription endpoint passes straight through natively with no protocol conversion, API endpoints are adapted automatically, and your balance and subscription usage show up right in the CCR dashboard.
-
-
- CCR already supports Kimi. Visit the Kimi Open Platform (中文站 | Global ) to try the API, or explore the cost-effective Coding Plan .
-
-
-
-
-
-
-
-Claude Code Router Desktop is a local gateway and desktop control panel for routing agent requests from Claude Code, Codex, ZCode, and compatible clients to the model provider you actually want to use.
-
+Claude Code Router Desktop is a local gateway and desktop control panel for routing agent requests from Claude Code, Codex, ZCode, and compatible clients to the model provider you actually want to use.
+
## Why Use CCR
- Use one local endpoint for multiple agent tools instead of configuring every client separately.
@@ -202,43 +169,6 @@ Codex support is powered by [musistudio/codexl](https://github.com/musistudio/co
RunAPI
-
-
-
-
- TeamoRouter
-
-
-
-
-
-
- code0.ai
-
-
-
-
-
-
- claudeapi
-
-
-
-
-
-
-
-
- Qiniu Cloud AI
-
-
-
-
-
-
- Fenno.ai
-
-
diff --git a/README_zh.md b/README_zh.md
index 7065c54..15bdb68 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -1,52 +1,19 @@
-Claude Code Router
+Claude Code Router Desktop
-
-
-
-
-
-
-
-
-
-
-
- Kimi Code 订阅
- ·
- API 中文站
- ·
- API Global
-
-
-
-
-
-
- 感谢 Kimi 赞助本项目! Kimi K2.7 Code 是 Moonshot AI 推出的编程专用开源智能体模型,在真实长程编程与复杂软件工程工作流中显著提升端到端任务成功率,同时优化推理效率,相比 K2.6 平均减少约 30% 的推理 token 消耗。在 CCR 中,Kimi 已作为内置供应商预设开箱即用:无论按量付费 API 还是 Kimi Code 订阅,一键导入即可把你的编程 Agent 请求路由到 Kimi,订阅端点原生直通、无需协议转换,API 端点自动适配,账户余额与订阅用量也能直接在 CCR 面板中查看。
-
-
- CCR 已内置 Kimi 供应商预设。前往 Kimi 开放平台(中文站 |Global )体验 API,或了解高性价比 Coding Plan 套餐。
-
-
-
-
-
-
-
-Claude Code Router Desktop 是一个本地网关和桌面控制台,用来把 Claude Code、Codex、ZCode 以及兼容客户端的 Agent 请求路由到你真正想使用的模型服务。
-
+Claude Code Router Desktop 是一个本地网关和桌面控制台,用来把 Claude Code、Codex、ZCode 以及兼容客户端的 Agent 请求路由到你真正想使用的模型服务。
+
## 为什么使用 CCR
- 用一个本地入口连接多个 Agent 工具,不需要在每个客户端里重复配置 Provider。
@@ -201,43 +168,6 @@ CCR 可以完全通过桌面 UI 完成配置。首次使用建议按下面顺序
RunAPI
-
-
-
-
- TeamoRouter
-
-
-
-
-
-
- code0.ai
-
-
-
-
-
-
- claudeapi
-
-
-
-
-
-
-
-
- 七牛云 AI
-
-
-
-
-
-
- Fenno.ai
-
-
diff --git a/packages/electron/assets/logo.png b/assets/logo.png
similarity index 100%
rename from packages/electron/assets/logo.png
rename to assets/logo.png
diff --git a/packages/electron/assets/tray-cyan.png b/assets/tray-cyan.png
similarity index 100%
rename from packages/electron/assets/tray-cyan.png
rename to assets/tray-cyan.png
diff --git a/packages/electron/assets/tray-orange.png b/assets/tray-orange.png
similarity index 100%
rename from packages/electron/assets/tray-orange.png
rename to assets/tray-orange.png
diff --git a/packages/electron/assets/tray-violet.png b/assets/tray-violet.png
similarity index 100%
rename from packages/electron/assets/tray-violet.png
rename to assets/tray-violet.png
diff --git a/packages/electron/assets/tray.png b/assets/tray.png
similarity index 100%
rename from packages/electron/assets/tray.png
rename to assets/tray.png
diff --git a/build/build.mjs b/build/build.mjs
index ba43565..a07412b 100644
--- a/build/build.mjs
+++ b/build/build.mjs
@@ -1,4 +1,4 @@
-import { buildBrowserRenderer, buildMain, buildRenderer, buildStyles, buildTrayRenderer, buildWebClientBridge, cleanDist, copyAppAssets, copyBrowserRendererHtml, copyMarketplacePlugins, copyModelCatalog, copyRendererHtml, copyTrayRendererHtml, syncUiRendererToRuntimeDists } from "./esbuild.config.mjs";
+import { buildBrowserRenderer, buildMain, buildRenderer, buildStyles, buildTrayRenderer, buildWebClientBridge, cleanDist, copyAppAssets, copyBrowserRendererHtml, copyMarketplacePlugins, copyModelCatalog, copyRendererHtml, copyTrayRendererHtml } from "./esbuild.config.mjs";
const mode = process.argv.includes("--dev") ? "development" : "production";
@@ -19,6 +19,4 @@ await Promise.all([
buildStyles({ minify: mode === "production" })
]);
-syncUiRendererToRuntimeDists();
-
-console.log(`Built monorepo package assets in ${mode} mode.`);
+console.log(`Built Electron app assets in ${mode} mode.`);
diff --git a/build/dev.mjs b/build/dev.mjs
index b91a341..a7cf8fd 100644
--- a/build/dev.mjs
+++ b/build/dev.mjs
@@ -5,14 +5,12 @@ import { spawn } from "node:child_process";
import { existsSync, readdirSync, readFileSync, statSync, watch } from "node:fs";
import path from "node:path";
import {
+ binPath,
buildStyles,
cleanDist,
browserRendererHtmlInput,
- cliSourceRoot,
- coreSourceRoot,
copyAppAssets,
copyBrowserRendererHtml,
- copyCliRuntimeToElectronDist,
copyMarketplacePlugins,
copyModelCatalog,
copyRendererHtml,
@@ -23,12 +21,12 @@ import {
createRendererBuildOptions,
createTrayRendererBuildOptions,
createWebClientBridgeBuildOptions,
+ cssInput,
+ cssOutput,
appAssetsInput,
modelCatalogInput,
projectRoot,
- rendererRoot,
rendererHtmlInput,
- syncUiRendererToRuntimeDists,
trayRendererHtmlInput,
watchPlugin
} from "./esbuild.config.mjs";
@@ -39,12 +37,7 @@ let pendingRestartReasons = [];
const watchSignatures = new Map();
let shuttingDown = false;
const restartDelayMs = 160;
-const styleBuildDelayMs = 160;
-const stylePollIntervalMs = 1000;
const ignoredSignatureEntries = new Set([".DS_Store"]);
-let styleBuildTimer = null;
-let styleBuildInFlight = false;
-let queuedStyleBuildReason = null;
const ready = {
browser: false,
cli: false,
@@ -53,32 +46,6 @@ const ready = {
tray: false,
webBridge: false
};
-const devTarget = parseDevTarget(process.argv.slice(2));
-const enabled = {
- cli: devTarget === "cli" || devTarget === "electron",
- electron: devTarget === "electron",
- ui: true
-};
-const coreSharedSourceRoot = path.join(coreSourceRoot, "shared");
-const styleWatchRoots = [rendererRoot, coreSharedSourceRoot].filter((watchRoot) => existsSync(watchRoot));
-const activeReadyNames = new Set([
- ...(enabled.ui ? ["browser", "renderer", "tray", "webBridge"] : []),
- ...(enabled.cli ? ["cli"] : []),
- ...(enabled.electron ? ["main"] : [])
-]);
-
-function parseDevTarget(args) {
- const target = args[0] ?? "electron";
- if (target === "--help" || target === "-h") {
- console.log("Usage: node build/dev.mjs [ui|cli|electron]");
- process.exit(0);
- }
- if (target === "ui" || target === "cli" || target === "electron") {
- return target;
- }
- console.error(`Unknown dev target "${target}". Expected ui, cli, or electron.`);
- process.exit(2);
-}
function logDev(message) {
console.log(`[dev] ${new Date().toISOString()} ${message}`);
@@ -90,7 +57,6 @@ function relativePath(file) {
function readyState() {
return Object.entries(ready)
- .filter(([name]) => activeReadyNames.has(name))
.map(([name, value]) => `${name}:${value ? "ready" : "pending"}`)
.join(" ");
}
@@ -197,63 +163,7 @@ function handleWatchedInput(label, watchedPath, eventType, filename, options, on
}
onChange();
- if (enabled.electron && options?.restart !== false) {
- scheduleRestart(reason);
- }
-}
-
-function scheduleStyleBuild(reason) {
- queuedStyleBuildReason = reason;
- if (styleBuildTimer) {
- clearTimeout(styleBuildTimer);
- }
- styleBuildTimer = setTimeout(() => {
- styleBuildTimer = null;
- void rebuildStyles(queuedStyleBuildReason ?? reason);
- }, styleBuildDelayMs);
-}
-
-async function rebuildStyles(reason) {
- if (styleBuildInFlight) {
- queuedStyleBuildReason = reason;
- return;
- }
-
- styleBuildInFlight = true;
- queuedStyleBuildReason = null;
- try {
- logDev(`rebuilding styles: ${reason}`);
- await buildStyles({ minify: false });
- syncUiRendererToRuntimeDists();
- if (enabled.electron) {
- scheduleRestart(`styles rebuilt: ${reason}`);
- }
- } catch (error) {
- const message = error instanceof Error ? error.message : String(error);
- logDev(`style rebuild failed: ${message}`);
- } finally {
- styleBuildInFlight = false;
- if (queuedStyleBuildReason) {
- const queuedReason = queuedStyleBuildReason;
- queuedStyleBuildReason = null;
- scheduleStyleBuild(queuedReason);
- }
- }
-}
-
-function pollStyleWatchRoots() {
- for (const styleWatchRoot of styleWatchRoots) {
- const label = `styles ${relativePath(styleWatchRoot)}`;
- const signature = contentSignature(styleWatchRoot);
- const previousSignature = watchSignatures.get(label);
- if (previousSignature === signature.key) {
- continue;
- }
-
- watchSignatures.set(label, signature.key);
- logDev(`watch event: ${label}; ${signature.summary}; content=changed`);
- scheduleStyleBuild(label);
- }
+ scheduleRestart(reason);
}
function markReady(name, reason = `${name} esbuild completed`) {
@@ -261,7 +171,7 @@ function markReady(name, reason = `${name} esbuild completed`) {
ready[name] = true;
}
logDev(`build ready: ${reason}; ${readyState()}`);
- if (enabled.electron && Array.from(activeReadyNames).every((readyName) => ready[readyName])) {
+ if (ready.browser && ready.cli && ready.main && ready.renderer && ready.tray && ready.webBridge) {
scheduleRestart(reason);
}
}
@@ -311,153 +221,114 @@ function restartElectron() {
});
}
-logDev(`starting dev build target=${devTarget} ui=${enabled.ui ? "on" : "off"} cli=${enabled.cli ? "on" : "off"} electron=${enabled.electron ? "on" : "off"}`);
+logDev("starting dev build");
cleanDist();
-if (enabled.electron) {
- copyAppAssets();
-}
-if (enabled.cli || enabled.electron) {
- copyMarketplacePlugins();
- copyModelCatalog();
-}
+copyAppAssets();
+copyMarketplacePlugins();
+copyModelCatalog();
copyBrowserRendererHtml();
copyRendererHtml();
copyTrayRendererHtml();
await buildStyles({ minify: false });
-syncUiRendererToRuntimeDists();
+
+const tailwindProcess = spawn(binPath("tailwindcss"), ["-i", cssInput, "-o", cssOutput, "--watch"], {
+ cwd: projectRoot,
+ stdio: "inherit",
+ shell: process.platform === "win32"
+});
+logDev(`Tailwind watcher started pid=${tailwindProcess.pid ?? "unknown"} input=${relativePath(cssInput)} output=${relativePath(cssOutput)}`);
+tailwindProcess.on("exit", (code, signal) => {
+ logDev(`Tailwind watcher exited code=${code ?? "null"} signal=${signal ?? "null"}`);
+});
rememberWatchSignature("home html", rendererHtmlInput);
rememberWatchSignature("browser html", browserRendererHtmlInput);
rememberWatchSignature("tray html", trayRendererHtmlInput);
-for (const styleWatchRoot of styleWatchRoots) {
- rememberWatchSignature(`styles ${relativePath(styleWatchRoot)}`, styleWatchRoot);
-}
-if (enabled.electron) {
- rememberWatchSignature("app assets", appAssetsInput);
-}
-if ((enabled.cli || enabled.electron) && existsSync(modelCatalogInput)) {
+rememberWatchSignature("app assets", appAssetsInput);
+if (existsSync(modelCatalogInput)) {
rememberWatchSignature("model catalog", modelCatalogInput);
}
const htmlWatcher = watch(rendererHtmlInput, { persistent: true }, (eventType, filename) => {
- handleWatchedInput("home html", rendererHtmlInput, eventType, filename, undefined, () => {
- copyRendererHtml();
- syncUiRendererToRuntimeDists();
- });
+ handleWatchedInput("home html", rendererHtmlInput, eventType, filename, undefined, copyRendererHtml);
});
const browserHtmlWatcher = watch(browserRendererHtmlInput, { persistent: true }, (eventType, filename) => {
- handleWatchedInput("browser html", browserRendererHtmlInput, eventType, filename, undefined, () => {
- copyBrowserRendererHtml();
- syncUiRendererToRuntimeDists();
- });
+ handleWatchedInput("browser html", browserRendererHtmlInput, eventType, filename, undefined, copyBrowserRendererHtml);
});
const trayHtmlWatcher = watch(trayRendererHtmlInput, { persistent: true }, (eventType, filename) => {
- handleWatchedInput("tray html", trayRendererHtmlInput, eventType, filename, undefined, () => {
- copyTrayRendererHtml();
- syncUiRendererToRuntimeDists();
- });
+ handleWatchedInput("tray html", trayRendererHtmlInput, eventType, filename, undefined, copyTrayRendererHtml);
});
-const stylePoller = setInterval(pollStyleWatchRoots, stylePollIntervalMs);
+const appAssetsWatcher = watch(appAssetsInput, { persistent: true }, (eventType, filename) => {
+ handleWatchedInput("app assets", appAssetsInput, eventType, filename, { isDirectory: true }, copyAppAssets);
+});
-const appAssetsWatcher = enabled.electron
- ? watch(appAssetsInput, { persistent: true }, (eventType, filename) => {
- handleWatchedInput("app assets", appAssetsInput, eventType, filename, { isDirectory: true }, copyAppAssets);
- })
- : { close: () => undefined };
-
-const modelCatalogWatcher = (enabled.cli || enabled.electron) && existsSync(modelCatalogInput)
+const modelCatalogWatcher = existsSync(modelCatalogInput)
? watch(modelCatalogInput, { persistent: true }, (eventType, filename) => {
handleWatchedInput("model catalog", modelCatalogInput, eventType, filename, undefined, copyModelCatalog);
})
: { close: () => undefined };
-const contexts = [];
+const mainContext = await esbuild.context(
+ createMainBuildOptions({
+ mode: "development",
+ plugins: [watchPlugin("main", (name) => markReady(name))]
+ })
+);
-if (enabled.electron) {
- contexts.push(
- await esbuild.context(
- createMainBuildOptions({
- mode: "development",
- plugins: [watchPlugin("main", (name) => markReady(name))]
- })
- )
- );
-}
+const cliContext = await esbuild.context(
+ createCliBuildOptions({
+ mode: "development",
+ plugins: [watchPlugin("cli", (name) => markReady(name))]
+ })
+);
-if (enabled.cli) {
- contexts.push(
- await esbuild.context(
- createCliBuildOptions({
- mode: "development",
- plugins: [
- watchPlugin("cli", (name) => {
- if (enabled.electron) {
- copyCliRuntimeToElectronDist();
- }
- markReady(name);
- })
- ]
+const rendererContext = await esbuild.context(
+ createRendererBuildOptions({
+ mode: "development",
+ plugins: [
+ watchPlugin("renderer", (name) => {
+ copyRendererHtml();
+ markReady(name);
})
- )
- );
-}
+ ]
+ })
+);
-if (enabled.ui) {
- contexts.push(
- await esbuild.context(
- createRendererBuildOptions({
- mode: "development",
- plugins: [
- watchPlugin("renderer", (name) => {
- copyRendererHtml();
- syncUiRendererToRuntimeDists();
- markReady(name);
- })
- ]
+const trayRendererContext = await esbuild.context(
+ createTrayRendererBuildOptions({
+ mode: "development",
+ plugins: [
+ watchPlugin("tray", (name) => {
+ copyTrayRendererHtml();
+ markReady(name);
})
- ),
- await esbuild.context(
- createTrayRendererBuildOptions({
- mode: "development",
- plugins: [
- watchPlugin("tray", (name) => {
- copyTrayRendererHtml();
- syncUiRendererToRuntimeDists();
- markReady(name);
- })
- ]
- })
- ),
- await esbuild.context(
- createBrowserRendererBuildOptions({
- mode: "development",
- plugins: [
- watchPlugin("browser", (name) => {
- copyBrowserRendererHtml();
- syncUiRendererToRuntimeDists();
- markReady(name);
- })
- ]
- })
- ),
- await esbuild.context(
- createWebClientBridgeBuildOptions({
- mode: "development",
- plugins: [
- watchPlugin("webBridge", (name) => {
- syncUiRendererToRuntimeDists();
- markReady(name);
- })
- ]
- })
- )
- );
-}
+ ]
+ })
+);
-await Promise.all(contexts.map((context) => context.watch()));
+const browserRendererContext = await esbuild.context(
+ createBrowserRendererBuildOptions({
+ mode: "development",
+ plugins: [
+ watchPlugin("browser", (name) => {
+ copyBrowserRendererHtml();
+ markReady(name);
+ })
+ ]
+ })
+);
+
+const webClientBridgeContext = await esbuild.context(
+ createWebClientBridgeBuildOptions({
+ mode: "development",
+ plugins: [watchPlugin("webBridge", (name) => markReady(name))]
+ })
+);
+
+await Promise.all([mainContext.watch(), cliContext.watch(), rendererContext.watch(), trayRendererContext.watch(), browserRendererContext.watch(), webClientBridgeContext.watch()]);
logDev("watchers are active");
async function shutdown() {
@@ -469,16 +340,13 @@ async function shutdown() {
if (electronProcess) {
electronProcess.kill();
}
- if (styleBuildTimer) {
- clearTimeout(styleBuildTimer);
- }
+ tailwindProcess.kill();
htmlWatcher.close();
browserHtmlWatcher.close();
trayHtmlWatcher.close();
- clearInterval(stylePoller);
appAssetsWatcher.close();
modelCatalogWatcher.close();
- await Promise.all(contexts.map((context) => context.dispose()));
+ await Promise.all([mainContext.dispose(), cliContext.dispose(), rendererContext.dispose(), trayRendererContext.dispose(), browserRendererContext.dispose(), webClientBridgeContext.dispose()]);
process.exit(0);
}
diff --git a/build/docker-build.mjs b/build/docker-build.mjs
deleted file mode 100644
index 75d4ed1..0000000
--- a/build/docker-build.mjs
+++ /dev/null
@@ -1,37 +0,0 @@
-import {
- buildBrowserRenderer,
- buildCoreServer,
- buildRenderer,
- buildStyles,
- buildTrayRenderer,
- buildWebClientBridge,
- cleanDist,
- copyBrowserRendererHtml,
- copyMarketplacePlugins,
- copyModelCatalog,
- copyRendererHtml,
- copyTrayRendererHtml,
- syncUiRendererToRuntimeDists
-} from "./esbuild.config.mjs";
-
-const mode = process.argv.includes("--dev") ? "development" : "production";
-
-cleanDist();
-copyMarketplacePlugins();
-copyModelCatalog();
-copyBrowserRendererHtml();
-copyRendererHtml();
-copyTrayRendererHtml();
-
-await Promise.all([
- buildCoreServer({ mode }),
- buildBrowserRenderer({ mode }),
- buildRenderer({ mode }),
- buildTrayRenderer({ mode }),
- buildWebClientBridge({ mode }),
- buildStyles({ minify: mode === "production" })
-]);
-
-syncUiRendererToRuntimeDists();
-
-console.log(`Built Docker core server and UI assets in ${mode} mode.`);
diff --git a/build/esbuild.config.mjs b/build/esbuild.config.mjs
index e4a05f6..9bdaba7 100644
--- a/build/esbuild.config.mjs
+++ b/build/esbuild.config.mjs
@@ -1,63 +1,23 @@
import esbuild from "esbuild";
import { spawn } from "node:child_process";
-import { chmodSync, cpSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
-import { builtinModules, createRequire } from "node:module";
+import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
+import { builtinModules } from "node:module";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
-const requireFromHere = createRequire(import.meta.url);
export const projectRoot = path.resolve(__dirname, "..");
-export const packagesRoot = path.join(projectRoot, "packages");
-export const cliRoot = path.join(packagesRoot, "cli");
-export const coreRoot = path.join(packagesRoot, "core");
-export const electronRoot = path.join(packagesRoot, "electron");
-export const uiRoot = path.join(packagesRoot, "ui");
-export const cliSourceRoot = path.join(cliRoot, "src");
-export const coreSourceRoot = path.join(coreRoot, "src");
-export const electronSourceRoot = path.join(electronRoot, "src");
-export const uiSourceRoot = path.join(uiRoot, "src");
-export const legacyDistDir = path.join(projectRoot, "dist");
-export const cliDistDir = path.join(cliRoot, "dist");
-export const coreDistDir = path.join(coreRoot, "dist");
-export const electronDistDir = path.join(electronRoot, "dist");
-export const uiDistDir = path.join(uiRoot, "dist");
-export const distDir = electronDistDir;
-export const cliMainOutDir = path.join(cliDistDir, "main");
-export const coreMainOutDir = path.join(coreDistDir, "main");
-export const electronMainOutDir = path.join(electronDistDir, "main");
-export const mainOutDir = electronMainOutDir;
-export const gatewayPackageRoot = path.dirname(requireFromHere.resolve("@the-next-ai/ai-gateway/package.json"));
-export const gatewayRuntimeInput = path.join(gatewayPackageRoot, "bin", "next-ai-gateway.js");
-export const electronGatewayRuntimeOutput = path.join(electronMainOutDir, "next-ai-gateway.js");
-export const botGatewaySdkPackageRoot = path.dirname(requireFromHere.resolve("@the-next-ai/bot-gateway-sdk/package.json"));
-export const botGatewaySdkEntryInput = path.join(botGatewaySdkPackageRoot, "dist", "index.js");
-export const botGatewaySdkRunnerInput = path.join(botGatewaySdkPackageRoot, "bin", "bot-gateway-stdio.mjs");
-export const electronBotGatewaySdkRootDir = path.join(electronMainOutDir, "bot-gateway-sdk");
-export const electronBotGatewaySdkDistDir = path.join(electronBotGatewaySdkRootDir, "dist");
-export const electronBotGatewaySdkBinDir = path.join(electronBotGatewaySdkRootDir, "bin");
-export const electronBotGatewaySdkPackageOutput = path.join(electronBotGatewaySdkRootDir, "package.json");
-export const electronBotGatewaySdkEntryOutput = path.join(electronBotGatewaySdkDistDir, "index.js");
-export const electronBotGatewaySdkRunnerOutput = path.join(electronBotGatewaySdkBinDir, "bot-gateway-stdio.mjs");
-export const rendererOutDir = path.join(uiDistDir, "renderer");
-export const cliRendererOutDir = path.join(cliDistDir, "renderer");
-export const coreRendererOutDir = path.join(coreDistDir, "renderer");
-export const electronRendererOutDir = path.join(electronDistDir, "renderer");
-export const runtimeRendererOutDirs = [cliRendererOutDir, coreRendererOutDir, electronRendererOutDir];
-export const appAssetsDir = path.join(electronDistDir, "assets");
+export const distDir = path.join(projectRoot, "dist");
+export const mainOutDir = path.join(distDir, "main");
+export const rendererOutDir = path.join(distDir, "renderer");
+export const appAssetsDir = path.join(distDir, "assets");
export const rendererAssetsDir = path.join(rendererOutDir, "assets");
-export const cliMarketplacePluginsDir = path.join(cliDistDir, "marketplace", "plugins");
-export const coreMarketplacePluginsDir = path.join(coreDistDir, "marketplace", "plugins");
-export const electronMarketplacePluginsDir = path.join(electronDistDir, "marketplace", "plugins");
-export const marketplacePluginsDir = electronMarketplacePluginsDir;
-export const appAssetsInput = path.join(electronRoot, "assets");
-export const modelCatalogInput = path.join(coreRoot, "models.json");
-export const cliModelCatalogOutput = path.join(cliDistDir, "models.json");
-export const coreModelCatalogOutput = path.join(coreDistDir, "models.json");
-export const electronModelCatalogOutput = path.join(electronDistDir, "models.json");
-export const modelCatalogOutput = electronModelCatalogOutput;
-export const rendererRoot = uiSourceRoot;
+export const marketplacePluginsDir = path.join(distDir, "marketplace", "plugins");
+export const appAssetsInput = path.join(projectRoot, "assets");
+export const modelCatalogInput = path.join(projectRoot, "models.json");
+export const modelCatalogOutput = path.join(distDir, "models.json");
+export const rendererRoot = path.join(projectRoot, "src", "renderer");
export const rendererHtmlInput = path.join(rendererRoot, "pages", "home", "index.html");
export const rendererHtmlOutput = path.join(rendererOutDir, "pages", "home", "index.html");
export const browserRendererHtmlInput = path.join(rendererRoot, "pages", "browser", "index.html");
@@ -67,14 +27,11 @@ export const trayRendererHtmlOutput = path.join(rendererOutDir, "pages", "tray",
export const cssInput = path.join(rendererRoot, "styles", "globals.css");
export const cssOutput = path.join(rendererAssetsDir, "main.css");
export const webClientBridgeOutput = path.join(rendererAssetsDir, "web-client-bridge.js");
-export const electronUndiciProxyAgentInput = path.join(coreSourceRoot, "proxy", "undici-proxy-agent.ts");
-const lightweightMcpBundleNames = ["browser-web-search-proxy-mcp.js", "fusion-vision-mcp.js", "fusion-tool-fallback-mcp.js"];
+const lightweightMcpBundleNames = ["fusion-vision-mcp.js", "fusion-tool-fallback-mcp.js"];
const lightweightMcpBundleMaxBytes = 128 * 1024;
const forbiddenLightweightMcpInputs = [
- { prefix: "packages/core/src/config/", reason: "config modules can pull in native storage side effects" },
- { prefix: "packages/core/src/storage/", reason: "native SQLite storage is not allowed in lightweight MCP subprocesses" },
- { prefix: "packages/electron/src/", reason: "Electron runtime modules are not allowed in lightweight MCP subprocesses" },
- { prefix: "packages/ui/src/", reason: "UI modules do not belong in stdio MCP subprocesses" },
+ { prefix: "src/main/", reason: "main-process modules can pull in config, Electron, or native storage side effects" },
+ { prefix: "src/renderer/", reason: "renderer modules do not belong in stdio MCP subprocesses" },
{ prefix: "node_modules/better-sqlite3/", reason: "native SQLite is not allowed in lightweight MCP subprocesses" },
{ prefix: "node_modules/electron/", reason: "Electron runtime modules are not allowed in lightweight MCP subprocesses" }
];
@@ -88,28 +45,15 @@ const nodeExternals = [
];
export function cleanDist() {
- rmSync(legacyDistDir, { force: true, recursive: true });
- rmSync(cliDistDir, { force: true, recursive: true });
- rmSync(coreDistDir, { force: true, recursive: true });
- rmSync(electronDistDir, { force: true, recursive: true });
- rmSync(uiDistDir, { force: true, recursive: true });
+ rmSync(distDir, { force: true, recursive: true });
ensureDist();
}
export function ensureDist() {
- mkdirSync(cliMainOutDir, { recursive: true });
- mkdirSync(coreMainOutDir, { recursive: true });
- mkdirSync(electronMainOutDir, { recursive: true });
- mkdirSync(electronBotGatewaySdkDistDir, { recursive: true });
- mkdirSync(electronBotGatewaySdkBinDir, { recursive: true });
+ mkdirSync(mainOutDir, { recursive: true });
mkdirSync(appAssetsDir, { recursive: true });
- mkdirSync(cliMarketplacePluginsDir, { recursive: true });
- mkdirSync(coreMarketplacePluginsDir, { recursive: true });
- mkdirSync(electronMarketplacePluginsDir, { recursive: true });
+ mkdirSync(marketplacePluginsDir, { recursive: true });
mkdirSync(rendererAssetsDir, { recursive: true });
- for (const outputDir of runtimeRendererOutDirs) {
- mkdirSync(path.join(outputDir, "assets"), { recursive: true });
- }
mkdirSync(path.dirname(rendererHtmlOutput), { recursive: true });
mkdirSync(path.dirname(browserRendererHtmlOutput), { recursive: true });
mkdirSync(path.dirname(trayRendererHtmlOutput), { recursive: true });
@@ -125,16 +69,12 @@ export function copyAppAssets() {
export function copyModelCatalog() {
ensureDist();
if (existsSync(modelCatalogInput)) {
- cpSync(modelCatalogInput, cliModelCatalogOutput);
- cpSync(modelCatalogInput, coreModelCatalogOutput);
- cpSync(modelCatalogInput, electronModelCatalogOutput);
+ cpSync(modelCatalogInput, modelCatalogOutput);
}
}
export function copyRendererHtml() {
- copyRendererPageHtml(rendererHtmlInput, rendererHtmlOutput, "main.js", {
- beforeModuleScriptTags: [' ']
- });
+ copyRendererPageHtml(rendererHtmlInput, rendererHtmlOutput, "main.js");
}
export function copyTrayRendererHtml() {
@@ -149,25 +89,14 @@ export function copyMarketplacePlugins() {
ensureDist();
for (const filename of ["claude-design-plugin.cjs", "cursor-proxy-plugin.cjs"]) {
const source = path.join(projectRoot, "examples", "plugins", filename);
+ const target = path.join(marketplacePluginsDir, filename);
if (existsSync(source)) {
- cpSync(source, path.join(cliMarketplacePluginsDir, filename));
- cpSync(source, path.join(coreMarketplacePluginsDir, filename));
- cpSync(source, path.join(electronMarketplacePluginsDir, filename));
+ cpSync(source, target);
}
}
}
-export function syncUiRendererToRuntimeDists() {
- ensureDist();
- for (const outputDir of runtimeRendererOutDirs) {
- rmSync(outputDir, { force: true, recursive: true });
- if (existsSync(rendererOutDir)) {
- cpSync(rendererOutDir, outputDir, { recursive: true });
- }
- }
-}
-
-function copyRendererPageHtml(input, output, scriptName, options = {}) {
+function copyRendererPageHtml(input, output, scriptName) {
ensureDist();
const source = readFileSync(input, "utf8");
const styleTag = ' ';
@@ -176,12 +105,6 @@ function copyRendererPageHtml(input, output, scriptName, options = {}) {
? source.replace(' ', scriptTag)
: source.replace("