Files
memind/scripts/dev-core-daemon.mjs
T

217 lines
5.9 KiB
JavaScript
Raw 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
/**
* LaunchAgent / 后台常驻版 dev 栈:portal + admin + ops + vite。
* 单个子进程异常退出时会自动重启;启动阶段失败则整体退出,由 launchd KeepAlive 重试。
*/
import { spawn, execSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
applyMemindRuntimeProfile,
describeMemindRuntimeProfile,
loadMemindEnvFiles,
} from './memind-runtime-profile.mjs';
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
const opsDir = path.join(root, 'ops');
const nodeBin = process.execPath;
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}`;
const plazaPublicBase = (
process.env.PLAZA_PUBLIC_BASE ?? `http://127.0.0.1:${plazaPort}`
).replace(/\/$/, '');
const logFile =
process.env.MEMIND_DEV_LOG ?? path.join(os.homedir(), 'Library/Logs/memind-dev.log');
function log(message) {
const line = `[${new Date().toISOString()}] ${message}\n`;
try {
fs.appendFileSync(logFile, line);
} catch {
process.stdout.write(line);
}
}
loadMemindEnvFiles(root);
applyMemindRuntimeProfile({ rootDir: root });
log(`Runtime profile: ${describeMemindRuntimeProfile()}`);
function freePort(port) {
try {
execSync(`lsof -ti TCP:${port} -sTCP:LISTEN | xargs kill -9`, { stdio: 'ignore' });
} catch {
// port already free
}
}
let fullyStarted = false;
let stopping = false;
const children = { server: null, admin: null, ops: null, vite: null };
function shutdown(code = 0) {
if (stopping) return;
stopping = true;
for (const child of Object.values(children)) {
child?.kill('SIGTERM');
}
setTimeout(() => process.exit(code), 300);
}
process.on('SIGINT', () => shutdown(0));
process.on('SIGTERM', () => shutdown(0));
function attachExitHandler(label, child, respawn) {
child.on('exit', (code, signal) => {
if (stopping || signal === 'SIGTERM') return;
if (!code || code === 0) return;
if (!fullyStarted) {
log(`[${label}] 启动阶段退出 code=${code}`);
shutdown(code);
return;
}
log(`[${label}] 异常退出 code=${code}3 秒后重启...`);
setTimeout(() => {
if (stopping) return;
respawn();
}, 3000);
});
}
function spawnPortal() {
const child = spawn(nodeBin, [path.join(root, 'server.mjs')], {
cwd: root,
env: process.env,
stdio: 'inherit',
});
children.server = child;
attachExitHandler('portal', child, spawnPortal);
return child;
}
function spawnAdmin() {
const child = spawn(nodeBin, [path.join(root, 'admin-server.mjs')], {
cwd: root,
env: process.env,
stdio: 'inherit',
});
children.admin = child;
attachExitHandler('admin', child, spawnAdmin);
return child;
}
function spawnOps() {
const child = spawn('npm', ['run', 'dev'], {
cwd: opsDir,
env: { ...process.env, ...opsEnv },
stdio: 'inherit',
});
children.ops = child;
attachExitHandler('ops', child, spawnOps);
return child;
}
function spawnVite() {
const child = spawn('npx', ['vite'], {
cwd: root,
env: { ...process.env, ...viteEnv },
stdio: 'inherit',
});
children.vite = child;
attachExitHandler('vite', child, spawnVite);
return child;
}
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 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,
};
log(`清理端口 ${portalPort} / ${adminPort} / ${vitePort} / ${opsPort}...`);
freePort(portalPort);
freePort(adminPort);
freePort(vitePort);
freePort(opsPort);
await new Promise((resolve) => setTimeout(resolve, 500));
log('启动 portal (server.mjs)...');
spawnPortal();
try {
await waitFor(portalUrl, async (url) => (await fetch(`${url}/auth/status`)).ok, 'Portal');
log(`Portal 就绪: ${portalUrl}`);
log('启动 memind_adm (admin-server.mjs)...');
spawnAdmin();
await waitFor(adminUrl, async (url) => (await fetch(`${url}/healthz`)).ok, 'Admin');
log(`memind_adm 就绪: ${adminUrl}`);
log(`启动 Ops 后台 @ http://127.0.0.1:${opsPort}/ops/`);
spawnOps();
await waitFor(
`http://127.0.0.1:${opsPort}`,
async (url) => (await fetch(`${url}/ops/`)).ok,
'Ops',
40,
);
log(`Ops 就绪: http://127.0.0.1:${opsPort}/ops/`);
log(`启动 Vite @ http://127.0.0.1:${vitePort}`);
spawnVite();
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',
);
log(`Vite 就绪: http://127.0.0.1:${vitePort}`);
fullyStarted = true;
log('');
log('本地服务(LaunchAgent 常驻):');
log(` MindSpace UI http://127.0.0.1:${vitePort}/?preview=mindspace`);
log(` Ops 审核后台 http://127.0.0.1:${opsPort}/ops/`);
log(` API / Portal ${portalUrl}`);
log(` memind_adm ${adminUrl}`);
log('');
log('Plaza 未包含在此 Agent 内;需要时请单独运行 pnpm dev:plaza');
} catch (err) {
log(err instanceof Error ? err.message : String(err));
shutdown(1);
}