From 795645deb9411e4a7e863af1146e0dda4be2e093 Mon Sep 17 00:00:00 2001 From: Xi Zhang Date: Mon, 1 Jun 2026 02:14:25 +0100 Subject: [PATCH] feat: update configuration for standalone server and add assembly script --- bin/evoscientist-webui.mjs | 97 +++++++++++++++++++++++++++++++++ next.config.ts | 4 +- package.json | 34 ++++++++++-- scripts/assemble-standalone.mjs | 47 ++++++++++++++++ 4 files changed, 176 insertions(+), 6 deletions(-) create mode 100755 bin/evoscientist-webui.mjs create mode 100644 scripts/assemble-standalone.mjs diff --git a/bin/evoscientist-webui.mjs b/bin/evoscientist-webui.mjs new file mode 100755 index 0000000..2e49a57 --- /dev/null +++ b/bin/evoscientist-webui.mjs @@ -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 ] + +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 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")); diff --git a/next.config.ts b/next.config.ts index e9ffa30..eed90f5 100644 --- a/next.config.ts +++ b/next.config.ts @@ -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; diff --git a/package.json b/package.json index d75b7bf..20bdd28 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/assemble-standalone.mjs b/scripts/assemble-standalone.mjs new file mode 100644 index 0000000..b2be1fd --- /dev/null +++ b/scripts/assemble-standalone.mjs @@ -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)` +);