feat: add UI build to build process

- Created separate build script to handle both CLI and UI building
- Added automatic UI dependency installation
- Copy built UI artifacts to dist directory

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
musistudio
2025-07-30 11:15:05 +08:00
parent 31db041084
commit 112d7ef8f9
57 changed files with 13581 additions and 365 deletions

View File

@@ -2,9 +2,9 @@
import { run } from "./index";
import { showStatus } from "./utils/status";
import { executeCodeCommand } from "./utils/codeCommand";
import { cleanupPidFile, isServiceRunning } from "./utils/processCheck";
import { cleanupPidFile, isServiceRunning, getServiceInfo } from "./utils/processCheck";
import { version } from "../package.json";
import { spawn } from "child_process";
import { spawn, exec } from "child_process";
import { PID_FILE, REFERENCE_COUNT_FILE } from "./constants";
import fs, { existsSync, readFileSync } from "fs";
import {join} from "path";
@@ -20,12 +20,14 @@ Commands:
restart Restart server
status Show server status
code Execute claude command
ui Open the web UI in browser
-v, version Show version information
-h, help Show help information
Example:
ccr start
ccr code "Write a Hello World"
ccr ui
`;
async function waitForService(
@@ -117,6 +119,61 @@ async function main() {
executeCodeCommand(process.argv.slice(3));
}
break;
case "ui":
// Check if service is running
if (!isServiceRunning()) {
console.log("Service not running, starting service...");
const cliPath = join(__dirname, "cli.js");
const startProcess = spawn("node", [cliPath, "start"], {
detached: true,
stdio: "ignore",
});
startProcess.on("error", (error) => {
console.error("Failed to start service:", error.message);
process.exit(1);
});
startProcess.unref();
if (!(await waitForService())) {
console.error(
"Service startup timeout, please manually run `ccr start` to start the service"
);
process.exit(1);
}
}
// Get service info and open UI
const serviceInfo = await getServiceInfo();
const uiUrl = `${serviceInfo.endpoint}/ui/`;
console.log(`Opening UI at ${uiUrl}`);
// Open URL in browser based on platform
const platform = process.platform;
let openCommand = "";
if (platform === "win32") {
// Windows
openCommand = `start ${uiUrl}`;
} else if (platform === "darwin") {
// macOS
openCommand = `open ${uiUrl}`;
} else if (platform === "linux") {
// Linux
openCommand = `xdg-open ${uiUrl}`;
} else {
console.error("Unsupported platform for opening browser");
process.exit(1);
}
exec(openCommand, (error) => {
if (error) {
console.error("Failed to open browser:", error.message);
process.exit(1);
}
});
break;
case "-v":
case "version":
console.log(`claude-code-router version: ${version}`);

View File

@@ -94,9 +94,11 @@ async function run(options: RunOptions = {}) {
},
});
server.addHook("preHandler", apiKeyAuth(config));
server.addHook("preHandler", async (req, reply) =>
router(req, reply, config)
);
server.addHook("preHandler", async (req, reply) => {
if(req.url.startsWith("/v1/messages")) {
router(req, reply, config)
}
});
server.start();
}

View File

@@ -3,7 +3,7 @@ import { FastifyRequest, FastifyReply } from "fastify";
export const apiKeyAuth =
(config: any) =>
(req: FastifyRequest, reply: FastifyReply, done: () => void) => {
if (["/", "/health"].includes(req.url)) {
if (["/", "/health"].includes(req.url) || req.url.startsWith("/ui")) {
return done();
}
const apiKey = config.APIKEY;

View File

@@ -1,6 +1,47 @@
import Server from "@musistudio/llms";
import { readConfigFile, writeConfigFile } from "./utils";
import { CONFIG_FILE } from "./constants";
import { join } from "path";
import { readFileSync } from "fs";
import fastifyStatic from "@fastify/static";
export const createServer = (config: any): Server => {
const server = new Server(config);
// Add endpoint to read config.json
server.app.get("/api/config", async () => {
return await readConfigFile();
});
// Add endpoint to save config.json
server.app.post("/api/config", async (req) => {
const newConfig = req.body;
await writeConfigFile(newConfig);
return { success: true, message: "Config saved successfully" };
});
// Add endpoint to restart the service
server.app.post("/api/restart", async (_, reply) => {
reply.send({ success: true, message: "Service restart initiated" });
// Restart the service after a short delay to allow response to be sent
setTimeout(() => {
const { spawn } = require('child_process');
spawn('ccr', ['restart'], { detached: true, stdio: 'ignore' });
}, 1000);
});
// Register static file serving with caching
server.app.register(fastifyStatic, {
root: join(__dirname, "..", "dist"),
prefix: "/ui/",
maxAge: "1h"
});
// Redirect /ui to /ui/ for proper static file serving
server.app.get("/ui", async (_, reply) => {
return reply.redirect("/ui/");
});
return server;
};

View File

@@ -87,8 +87,7 @@ export const readConfigFile = async () => {
export const writeConfigFile = async (config: any) => {
await ensureDir(HOME_DIR);
// Add a comment to indicate JSON5 support
const configWithComment = `// This config file supports JSON5 format (comments, trailing commas, etc.)\n${JSON5.stringify(config, null, 2)}`;
const configWithComment = `${JSON.stringify(config, null, 2)}`;
await fs.writeFile(CONFIG_FILE, configWithComment);
};