feat: update configuration for standalone server and add assembly script

This commit is contained in:
Xi Zhang
2026-06-01 02:14:25 +01:00
parent 53537258ba
commit 795645deb9
4 changed files with 176 additions and 6 deletions
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env node
import { spawn, execSync } from "child_process";
import { dirname, join, resolve } from "path";
import { fileURLToPath } from "url";
import { readFileSync } from "fs";
const __dirname = dirname(fileURLToPath(import.meta.url));
const pkgDir = resolve(__dirname, "..");
const pkg = JSON.parse(readFileSync(join(pkgDir, "package.json"), "utf-8"));
const serverEntry = join(pkgDir, "dist", "server.js");
const args = process.argv.slice(2);
if (["-v", "--version", "version"].includes(args[0])) {
console.log(pkg.version);
process.exit(0);
}
if (["-h", "--help", "help"].includes(args[0])) {
console.log(`
${pkg.name} v${pkg.version}
Usage: evoscientist-webui [--port <port>]
Starts the EvoScientist Web UI and opens it in your browser.
Then enter your EvoScientist deployment URL (default http://127.0.0.1:6174)
from \`EvoSci deploy\` to connect.
Options:
--port <port> Port to run on (default 4716)
-v, --version Show version
-h, --help Show this help
`);
process.exit(0);
}
// Resolve port: --port flag > PORT env > default.
let port = 4716;
const portIdx = args.indexOf("--port");
if (portIdx !== -1 && args[portIdx + 1]) {
port = parseInt(args[portIdx + 1], 10);
} else if (process.env.PORT && !Number.isNaN(parseInt(process.env.PORT, 10))) {
port = parseInt(process.env.PORT, 10);
}
const url = `http://localhost:${port}`;
console.log(` ⏳ Starting EvoScientist Web UI on ${url} …`);
const child = spawn(process.execPath, [serverEntry], {
stdio: "inherit",
env: {
...process.env,
PORT: String(port),
HOSTNAME: process.env.HOSTNAME || "127.0.0.1",
NODE_ENV: "production",
},
});
child.on("error", (err) => {
console.error(` ✗ Failed to start: ${err.message}`);
process.exit(1);
});
// Poll until the server responds, then open the browser once.
let opened = false;
let waited = 0;
function openBrowser() {
if (opened) return;
opened = true;
const cmd =
process.platform === "darwin"
? `open "${url}"`
: process.platform === "win32"
? `start "" "${url}"`
: `xdg-open "${url}"`;
try {
execSync(cmd, { stdio: "ignore" });
} catch {
// Browser couldn't be opened automatically — the URL is printed above.
}
}
function poll() {
fetch(url)
.then(() => {
console.log(` ✓ EvoScientist Web UI is running → ${url}`);
openBrowser();
})
.catch(() => {
waited += 500;
if (waited < 30000) setTimeout(poll, 500);
});
}
setTimeout(poll, 800);
child.on("exit", (code) => process.exit(code ?? 0));
process.on("SIGINT", () => child.kill("SIGINT"));
process.on("SIGTERM", () => child.kill("SIGTERM"));
+3 -1
View File
@@ -1,7 +1,9 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
// Self-contained server bundle for the npm package (the bin launcher runs
// dist/server.js). Needed because /api/skills is a server route.
output: "standalone",
};
export default nextConfig;
+29 -5
View File
@@ -1,17 +1,41 @@
{
"name": "evoscientist-webui",
"version": "0.1.0",
"name": "@evoscientist/webui",
"version": "0.0.1",
"description": "Web UI for EvoScientist — a self-evolving AI scientist built on DeepAgents/LangGraph.",
"private": true,
"type": "module",
"license": "Apache-2.0",
"keywords": [
"evoscientist",
"ai-scientist",
"research",
"deepagents",
"langgraph",
"chat-ui",
"webui"
],
"bin": {
"evoscientist-webui": "./bin/evoscientist-webui.mjs"
},
"files": [
"bin/",
"dist/"
],
"engines": {
"node": ">=20"
},
"publishConfig": {
"access": "public"
},
"scripts": {
"dev": "next dev --turbopack --port 4716",
"build": "next build",
"build": "next build && node scripts/assemble-standalone.mjs",
"start": "next start --port 4716",
"start:dist": "node dist/server.js",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write .",
"format:check": "prettier --check ."
"format:check": "prettier --check .",
"prepack": "npm run build"
},
"dependencies": {
"@langchain/core": "1.1.19",
+47
View File
@@ -0,0 +1,47 @@
// Assemble a self-contained server into dist/ from the Next standalone output.
// `next build` with output:"standalone" produces:
// .next/standalone/ (server.js + traced node_modules + .next/server)
// .next/static/ (NOT inside standalone — must be copied)
// We copy everything into dist/ so the published package ships one folder that
// `node dist/server.js` can run.
import { cp, rm, readdir } from "fs/promises";
import { existsSync } from "fs";
const STANDALONE = ".next/standalone";
const STATIC = ".next/static";
const PUBLIC = "public";
const OUT = "dist";
if (!existsSync(STANDALONE)) {
console.error(
`✗ ${STANDALONE} not found. Did "next build" run with output:"standalone"?`
);
process.exit(1);
}
await rm(OUT, { recursive: true, force: true });
await cp(STANDALONE, OUT, { recursive: true });
await cp(STATIC, `${OUT}/.next/static`, { recursive: true });
if (existsSync(PUBLIC)) {
await cp(PUBLIC, `${OUT}/public`, { recursive: true });
}
// Next copies the whole project root into the standalone bundle. Prune it down
// to just the runtime essentials (drops src/, configs, and local notes like
// CLAUDE.md/AGENTS.md so they never get published).
const KEEP = new Set([
"server.js",
".next",
"node_modules",
"package.json",
"public",
]);
for (const entry of await readdir(OUT)) {
if (!KEEP.has(entry)) {
await rm(`${OUT}/${entry}`, { recursive: true, force: true });
}
}
console.log(
`✓ Assembled standalone server into ${OUT}/ (run: node ${OUT}/server.js)`
);