diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..86b7318 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,17 @@ +.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 291dafe..6efc5ae 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,8 @@ release tmp release-local logs -.opencat \ No newline at end of file +.opencat +test-results +playwright-report +blob-report +.tmp \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..147c7ce --- /dev/null +++ b/Dockerfile @@ -0,0 +1,54 @@ +ARG NODE_IMAGE=node:22-bookworm + +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 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 nginx \ + && rm -rf /var/lib/apt/lists/* \ + && rm -f /etc/nginx/sites-enabled/default /etc/nginx/conf.d/default.conf + +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 + +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 3f5ce1e..89ee179 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -
diff --git a/README_zh.md b/README_zh.md
index cd79161..dc0b436 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -1,4 +1,4 @@
-
diff --git a/build/build.mjs b/build/build.mjs
index a07412b..ba43565 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 } from "./esbuild.config.mjs";
+import { buildBrowserRenderer, buildMain, buildRenderer, buildStyles, buildTrayRenderer, buildWebClientBridge, cleanDist, copyAppAssets, copyBrowserRendererHtml, copyMarketplacePlugins, copyModelCatalog, copyRendererHtml, copyTrayRendererHtml, syncUiRendererToRuntimeDists } from "./esbuild.config.mjs";
const mode = process.argv.includes("--dev") ? "development" : "production";
@@ -19,4 +19,6 @@ await Promise.all([
buildStyles({ minify: mode === "production" })
]);
-console.log(`Built Electron app assets in ${mode} mode.`);
+syncUiRendererToRuntimeDists();
+
+console.log(`Built monorepo package assets in ${mode} mode.`);
diff --git a/build/dev.mjs b/build/dev.mjs
index a7cf8fd..b91a341 100644
--- a/build/dev.mjs
+++ b/build/dev.mjs
@@ -5,12 +5,14 @@ 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,
@@ -21,12 +23,12 @@ import {
createRendererBuildOptions,
createTrayRendererBuildOptions,
createWebClientBridgeBuildOptions,
- cssInput,
- cssOutput,
appAssetsInput,
modelCatalogInput,
projectRoot,
+ rendererRoot,
rendererHtmlInput,
+ syncUiRendererToRuntimeDists,
trayRendererHtmlInput,
watchPlugin
} from "./esbuild.config.mjs";
@@ -37,7 +39,12 @@ 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,
@@ -46,6 +53,32 @@ 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}`);
@@ -57,6 +90,7 @@ function relativePath(file) {
function readyState() {
return Object.entries(ready)
+ .filter(([name]) => activeReadyNames.has(name))
.map(([name, value]) => `${name}:${value ? "ready" : "pending"}`)
.join(" ");
}
@@ -163,7 +197,63 @@ function handleWatchedInput(label, watchedPath, eventType, filename, options, on
}
onChange();
- scheduleRestart(reason);
+ 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);
+ }
}
function markReady(name, reason = `${name} esbuild completed`) {
@@ -171,7 +261,7 @@ function markReady(name, reason = `${name} esbuild completed`) {
ready[name] = true;
}
logDev(`build ready: ${reason}; ${readyState()}`);
- if (ready.browser && ready.cli && ready.main && ready.renderer && ready.tray && ready.webBridge) {
+ if (enabled.electron && Array.from(activeReadyNames).every((readyName) => ready[readyName])) {
scheduleRestart(reason);
}
}
@@ -221,114 +311,153 @@ function restartElectron() {
});
}
-logDev("starting dev build");
+logDev(`starting dev build target=${devTarget} ui=${enabled.ui ? "on" : "off"} cli=${enabled.cli ? "on" : "off"} electron=${enabled.electron ? "on" : "off"}`);
cleanDist();
-copyAppAssets();
-copyMarketplacePlugins();
-copyModelCatalog();
+if (enabled.electron) {
+ copyAppAssets();
+}
+if (enabled.cli || enabled.electron) {
+ copyMarketplacePlugins();
+ copyModelCatalog();
+}
copyBrowserRendererHtml();
copyRendererHtml();
copyTrayRendererHtml();
await buildStyles({ minify: false });
-
-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"}`);
-});
+syncUiRendererToRuntimeDists();
rememberWatchSignature("home html", rendererHtmlInput);
rememberWatchSignature("browser html", browserRendererHtmlInput);
rememberWatchSignature("tray html", trayRendererHtmlInput);
-rememberWatchSignature("app assets", appAssetsInput);
-if (existsSync(modelCatalogInput)) {
+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("model catalog", modelCatalogInput);
}
const htmlWatcher = watch(rendererHtmlInput, { persistent: true }, (eventType, filename) => {
- handleWatchedInput("home html", rendererHtmlInput, eventType, filename, undefined, copyRendererHtml);
+ handleWatchedInput("home html", rendererHtmlInput, eventType, filename, undefined, () => {
+ copyRendererHtml();
+ syncUiRendererToRuntimeDists();
+ });
});
const browserHtmlWatcher = watch(browserRendererHtmlInput, { persistent: true }, (eventType, filename) => {
- handleWatchedInput("browser html", browserRendererHtmlInput, eventType, filename, undefined, copyBrowserRendererHtml);
+ handleWatchedInput("browser html", browserRendererHtmlInput, eventType, filename, undefined, () => {
+ copyBrowserRendererHtml();
+ syncUiRendererToRuntimeDists();
+ });
});
const trayHtmlWatcher = watch(trayRendererHtmlInput, { persistent: true }, (eventType, filename) => {
- handleWatchedInput("tray html", trayRendererHtmlInput, eventType, filename, undefined, copyTrayRendererHtml);
+ handleWatchedInput("tray html", trayRendererHtmlInput, eventType, filename, undefined, () => {
+ copyTrayRendererHtml();
+ syncUiRendererToRuntimeDists();
+ });
});
-const appAssetsWatcher = watch(appAssetsInput, { persistent: true }, (eventType, filename) => {
- handleWatchedInput("app assets", appAssetsInput, eventType, filename, { isDirectory: true }, copyAppAssets);
-});
+const stylePoller = setInterval(pollStyleWatchRoots, stylePollIntervalMs);
-const modelCatalogWatcher = existsSync(modelCatalogInput)
+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)
? watch(modelCatalogInput, { persistent: true }, (eventType, filename) => {
handleWatchedInput("model catalog", modelCatalogInput, eventType, filename, undefined, copyModelCatalog);
})
: { close: () => undefined };
-const mainContext = await esbuild.context(
- createMainBuildOptions({
- mode: "development",
- plugins: [watchPlugin("main", (name) => markReady(name))]
- })
-);
+const contexts = [];
-const cliContext = await esbuild.context(
- createCliBuildOptions({
- mode: "development",
- plugins: [watchPlugin("cli", (name) => markReady(name))]
- })
-);
-
-const rendererContext = await esbuild.context(
- createRendererBuildOptions({
- mode: "development",
- plugins: [
- watchPlugin("renderer", (name) => {
- copyRendererHtml();
- markReady(name);
+if (enabled.electron) {
+ contexts.push(
+ await esbuild.context(
+ createMainBuildOptions({
+ mode: "development",
+ plugins: [watchPlugin("main", (name) => markReady(name))]
})
- ]
- })
-);
+ )
+ );
+}
-const trayRendererContext = await esbuild.context(
- createTrayRendererBuildOptions({
- mode: "development",
- plugins: [
- watchPlugin("tray", (name) => {
- copyTrayRendererHtml();
- markReady(name);
+if (enabled.cli) {
+ contexts.push(
+ await esbuild.context(
+ createCliBuildOptions({
+ mode: "development",
+ plugins: [
+ watchPlugin("cli", (name) => {
+ if (enabled.electron) {
+ copyCliRuntimeToElectronDist();
+ }
+ markReady(name);
+ })
+ ]
})
- ]
- })
-);
+ )
+ );
+}
-const browserRendererContext = await esbuild.context(
- createBrowserRendererBuildOptions({
- mode: "development",
- plugins: [
- watchPlugin("browser", (name) => {
- copyBrowserRendererHtml();
- markReady(name);
+if (enabled.ui) {
+ contexts.push(
+ await esbuild.context(
+ createRendererBuildOptions({
+ mode: "development",
+ plugins: [
+ watchPlugin("renderer", (name) => {
+ copyRendererHtml();
+ syncUiRendererToRuntimeDists();
+ 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);
+ })
+ ]
+ })
+ )
+ );
+}
-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()]);
+await Promise.all(contexts.map((context) => context.watch()));
logDev("watchers are active");
async function shutdown() {
@@ -340,13 +469,16 @@ async function shutdown() {
if (electronProcess) {
electronProcess.kill();
}
- tailwindProcess.kill();
+ if (styleBuildTimer) {
+ clearTimeout(styleBuildTimer);
+ }
htmlWatcher.close();
browserHtmlWatcher.close();
trayHtmlWatcher.close();
+ clearInterval(stylePoller);
appAssetsWatcher.close();
modelCatalogWatcher.close();
- await Promise.all([mainContext.dispose(), cliContext.dispose(), rendererContext.dispose(), trayRendererContext.dispose(), browserRendererContext.dispose(), webClientBridgeContext.dispose()]);
+ await Promise.all(contexts.map((context) => context.dispose()));
process.exit(0);
}
diff --git a/build/docker-build.mjs b/build/docker-build.mjs
new file mode 100644
index 0000000..75d4ed1
--- /dev/null
+++ b/build/docker-build.mjs
@@ -0,0 +1,37 @@
+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 bcf3a1a..86ad8b0 100644
--- a/build/esbuild.config.mjs
+++ b/build/esbuild.config.mjs
@@ -8,16 +8,43 @@ import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export const projectRoot = path.resolve(__dirname, "..");
-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 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 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 rendererAssetsDir = path.join(rendererOutDir, "assets");
-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 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 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");
@@ -30,8 +57,10 @@ export const webClientBridgeOutput = path.join(rendererAssetsDir, "web-client-br
const lightweightMcpBundleNames = ["browser-web-search-proxy-mcp.js", "fusion-vision-mcp.js", "fusion-tool-fallback-mcp.js"];
const lightweightMcpBundleMaxBytes = 128 * 1024;
const forbiddenLightweightMcpInputs = [
- { 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: "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: "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" }
];
@@ -45,15 +74,26 @@ const nodeExternals = [
];
export function cleanDist() {
- rmSync(distDir, { force: true, recursive: true });
+ 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 });
ensureDist();
}
export function ensureDist() {
- mkdirSync(mainOutDir, { recursive: true });
+ mkdirSync(cliMainOutDir, { recursive: true });
+ mkdirSync(coreMainOutDir, { recursive: true });
+ mkdirSync(electronMainOutDir, { recursive: true });
mkdirSync(appAssetsDir, { recursive: true });
- mkdirSync(marketplacePluginsDir, { recursive: true });
+ mkdirSync(cliMarketplacePluginsDir, { recursive: true });
+ mkdirSync(coreMarketplacePluginsDir, { recursive: true });
+ mkdirSync(electronMarketplacePluginsDir, { 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 });
@@ -69,12 +109,16 @@ export function copyAppAssets() {
export function copyModelCatalog() {
ensureDist();
if (existsSync(modelCatalogInput)) {
- cpSync(modelCatalogInput, modelCatalogOutput);
+ cpSync(modelCatalogInput, cliModelCatalogOutput);
+ cpSync(modelCatalogInput, coreModelCatalogOutput);
+ cpSync(modelCatalogInput, electronModelCatalogOutput);
}
}
export function copyRendererHtml() {
- copyRendererPageHtml(rendererHtmlInput, rendererHtmlOutput, "main.js");
+ copyRendererPageHtml(rendererHtmlInput, rendererHtmlOutput, "main.js", {
+ beforeModuleScriptTags: [' ']
+ });
}
export function copyTrayRendererHtml() {
@@ -89,14 +133,25 @@ 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, target);
+ cpSync(source, path.join(cliMarketplacePluginsDir, filename));
+ cpSync(source, path.join(coreMarketplacePluginsDir, filename));
+ cpSync(source, path.join(electronMarketplacePluginsDir, filename));
}
}
}
-function copyRendererPageHtml(input, output, scriptName) {
+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 = {}) {
ensureDist();
const source = readFileSync(input, "utf8");
const styleTag = ' ';
@@ -105,6 +160,12 @@ function copyRendererPageHtml(input, output, scriptName) {
? source.replace(' ', scriptTag)
: source.replace("