Files
memind/scripts/dev.mjs

209 lines
6.3 KiB
JavaScript
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
// Compatibility entrypoint for the old full-stack startup.
// Prefer `pnpm dev` for the core app and `pnpm dev:plaza` for Plaza alone.
import { spawn, execSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
const defaultPlazaDir = path.join(root, '../memind_plaza');
const plazaDir = process.env.PLAZA_APP_DIR ?? defaultPlazaDir;
const opsDir = path.join(root, 'ops');
const portalPort = Number(process.env.H5_PORT ?? 8081);
const vitePort = Number(process.env.VITE_PORT ?? 5173);
const plazaPort = Number(process.env.PLAZA_PORT ?? 3001);
const opsPort = Number(process.env.OPS_PORT ?? 3002);
const adminPort = Number(process.env.ADMIN_PORT ?? 8082);
const portalUrl = `http://127.0.0.1:${portalPort}`;
const adminUrl = `http://127.0.0.1:${adminPort}`;
function loadEnvFile(filePath) {
if (!fs.existsSync(filePath)) return;
for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const eq = trimmed.indexOf('=');
if (eq < 0) continue;
const key = trimmed.slice(0, eq).trim();
const value = trimmed.slice(eq + 1).trim();
if (!process.env[key]) process.env[key] = value;
}
}
loadEnvFile(path.join(root, '../../.env.local'));
loadEnvFile(path.join(root, '.env'));
function freePort(port) {
try {
execSync(`lsof -ti TCP:${port} -sTCP:LISTEN | xargs kill -9`, { stdio: 'ignore' });
} catch {
// port already free
}
}
function spawnChild(command, args, label, cwd = root, extraEnv = {}) {
const child = spawn(command, args, {
cwd,
env: { ...process.env, ...extraEnv },
stdio: 'inherit',
});
child.on('exit', (code, signal) => {
if (signal) return;
if (code && code !== 0) {
console.error(`[${label}] exited with code ${code}`);
shutdown(code ?? 1);
}
});
return child;
}
let server;
let admin;
let plaza;
let ops;
let vite;
let stopping = false;
function shutdown(code = 0) {
if (stopping) return;
stopping = true;
server?.kill('SIGTERM');
admin?.kill('SIGTERM');
plaza?.kill('SIGTERM');
ops?.kill('SIGTERM');
vite?.kill('SIGTERM');
setTimeout(() => process.exit(code), 300);
}
process.on('SIGINT', () => shutdown(0));
process.on('SIGTERM', () => shutdown(0));
async function waitFor(url, check, label, retries = 60) {
for (let i = 0; i < retries; i += 1) {
try {
if (await check(url)) return;
} catch {
// still starting
}
await new Promise((resolve) => setTimeout(resolve, 500));
}
throw new Error(`${label} 未在 ${url} 启动`);
}
const mindSpacePublicBase = (
process.env.H5_PUBLIC_BASE_URL ?? `http://127.0.0.1:${vitePort}`
).replace(/\/$/, '');
const plazaPublicBase = (
process.env.PLAZA_PUBLIC_BASE ?? `http://127.0.0.1:${plazaPort}`
).replace(/\/$/, '');
const plazaEnv = {
PLAZA_API_PROXY: portalUrl,
PLAZA_API_BASE: portalUrl,
PLAZA_PUBLIC_BASE: plazaPublicBase,
NEXT_PUBLIC_API_BASE: plazaPublicBase,
NEXT_PUBLIC_SITE_BASE: plazaPublicBase,
NEXT_PUBLIC_MINDSPACE_BASE: mindSpacePublicBase,
NEXT_PUBLIC_PLAZA_BASE: '/plaza',
};
const viteEnv = {
VITE_PLAZA_BASE: plazaPublicBase,
VITE_MINDSPACE_BASE: mindSpacePublicBase,
};
const opsEnv = {
OPS_API_PROXY: portalUrl,
OPS_ADMIN_PROXY: adminUrl,
VITE_PLAZA_BASE: plazaPublicBase,
VITE_MINDSPACE_BASE: mindSpacePublicBase,
};
function ensurePlazaApp() {
if (!fs.existsSync(path.join(plazaDir, 'package.json'))) {
throw new Error(`未找到 Plaza Next.js${plazaDir}。请设置 PLAZA_APP_DIR 指向本地 Plaza 源码,例如 ../memind_plaza`);
}
}
console.log(`==> 清理端口 ${portalPort} / ${adminPort} / ${vitePort} / ${plazaPort} / ${opsPort}...`);
freePort(portalPort);
freePort(adminPort);
freePort(vitePort);
freePort(plazaPort);
freePort(opsPort);
await new Promise((resolve) => setTimeout(resolve, 500));
ensurePlazaApp();
console.log('==> 启动 portal (server.mjs)...');
server = spawnChild('node', ['server.mjs'], 'portal');
try {
await waitFor(portalUrl, async (url) => (await fetch(`${url}/auth/status`)).ok, 'Portal');
console.log(`==> Portal 就绪: ${portalUrl}`);
console.log('==> 启动 memind_adm (admin-server.mjs)...');
admin = spawnChild('node', ['admin-server.mjs'], 'admin');
await waitFor(adminUrl, async (url) => (await fetch(`${url}/healthz`)).ok, 'Admin');
console.log(`==> memind_adm 就绪: ${adminUrl}`);
console.log(`==> 启动 Plaza Next.js @ ${plazaPublicBase}/plaza`);
plaza = spawnChild(
'npm',
['run', 'dev', '--', '-H', '0.0.0.0', '-p', String(plazaPort)],
'plaza',
plazaDir,
plazaEnv,
);
await waitFor(
`http://127.0.0.1:${plazaPort}`,
async (url) => {
const res = await fetch(`${url}/plaza`);
return res.ok || res.status === 307 || res.status === 308;
},
'Plaza',
80,
);
console.log(`==> Plaza 就绪: http://127.0.0.1:${plazaPort}/plaza`);
console.log(`==> 启动 Ops 后台 @ http://127.0.0.1:${opsPort}/ops/`);
ops = spawnChild('npm', ['run', 'dev'], 'ops', opsDir, opsEnv);
await waitFor(
`http://127.0.0.1:${opsPort}`,
async (url) => (await fetch(`${url}/ops/`)).ok,
'Ops',
40,
);
console.log(`==> Ops 就绪: http://127.0.0.1:${opsPort}/ops/`);
console.log(`==> 启动 Vite @ http://127.0.0.1:${vitePort}`);
vite = spawnChild('npx', ['vite'], 'vite', root, viteEnv);
await waitFor(
`http://127.0.0.1:${vitePort}`,
async (url) => {
const [rootRes, mainRes] = await Promise.all([
fetch(`${url}/`),
fetch(`${url}/src/main.tsx`),
]);
return rootRes.ok && mainRes.ok;
},
'Vite',
);
console.log(`==> Vite 就绪: http://127.0.0.1:${vitePort}`);
console.log('');
console.log('本地服务:');
console.log(` MindSpace UI http://127.0.0.1:${vitePort}/?preview=mindspace`);
console.log(` Plaza 广场 http://127.0.0.1:${plazaPort}/plaza`);
console.log(` Ops 审核后台 http://127.0.0.1:${opsPort}/ops/`);
console.log(` API / Portal ${portalUrl}`);
console.log(` memind_adm ${adminUrl}`);
console.log('');
console.log('首次使用 Ops:先登录 MindSpace,再执行 node scripts/grant-ops-role.mjs admin ops_admin');
} catch (err) {
console.error(err instanceof Error ? err.message : err);
shutdown(1);
}