Files
memind/scripts/build-mindspace-service-runtime.mjs
T
john 67d529c81e fix(mindspace): keep standalone service data paths under MindSpace root
Prevent Portal .env from overriding MindSpace storage/users roots, and
set launchd env plus runtime docs to use /Users/john/MindSpace paths.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-12 18:06:16 +08:00

177 lines
5.9 KiB
JavaScript

#!/usr/bin/env node
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const root = path.join(__dirname, '..');
const runtimeRoot = path.join(root, '.runtime', 'mindspace-service');
const serviceSourceRoot = path.join(root, 'mindspace-service');
const nodeModulesDir = path.join(root, 'node_modules');
const skipNodeModules = process.argv.includes('--skip-node-modules');
const excludeTopLevel = new Set([
'.git',
'.claude',
'.cursor',
'.local',
'.runtime',
'node_modules',
'dist',
'MindSpace',
'data',
'users',
'logs',
]);
async function exists(targetPath) {
try {
await fs.access(targetPath);
return true;
} catch {
return false;
}
}
async function remove(targetPath) {
await fs.rm(targetPath, { recursive: true, force: true });
}
async function copyDir(source, target) {
await fs.mkdir(path.dirname(target), { recursive: true });
await fs.cp(source, target, { recursive: true, dereference: false });
}
function shouldCopyMemindSource(source) {
const relative = path.relative(root, source);
if (!relative) return true;
const parts = relative.split(path.sep);
const top = parts[0];
const last = parts[parts.length - 1];
if (excludeTopLevel.has(top)) return false;
if (last.endsWith('.pid')) return false;
if (last === '.DS_Store' || last === '.env' || last === '.env.local') return false;
if (top === 'public' && parts[1] === 'plaza-covers') return false;
return true;
}
async function rewriteNodeModulesSymlinks(runtimeNodeModulesDir, sourceNodeModulesDir) {
console.log('==> 重写 node_modules 顶层符号链接为 runtime 内相对路径');
const localNodeModulesRoot = `${sourceNodeModulesDir}${path.sep}`;
async function visit(dir) {
const entries = await fs.readdir(dir);
for (const entryName of entries) {
const fullPath = path.join(dir, entryName);
const stats = await fs.lstat(fullPath);
if (stats.isSymbolicLink()) {
const linkTarget = await fs.readlink(fullPath);
const normalizedTarget = path.normalize(linkTarget);
if (!normalizedTarget.startsWith(localNodeModulesRoot)) continue;
const relativeInsideNodeModules = normalizedTarget.slice(localNodeModulesRoot.length);
const runtimeTarget = path.join(runtimeNodeModulesDir, relativeInsideNodeModules);
const replacement = path.relative(path.dirname(fullPath), runtimeTarget);
await fs.unlink(fullPath);
await fs.symlink(replacement, fullPath);
continue;
}
if (stats.isDirectory()) {
await visit(fullPath);
}
}
}
await visit(runtimeNodeModulesDir);
}
async function copyServiceSource() {
console.log('==> 拷贝 MindSpace service 源文件');
await remove(runtimeRoot);
await fs.mkdir(runtimeRoot, { recursive: true });
await copyDir(serviceSourceRoot, runtimeRoot);
await remove(path.join(runtimeRoot, 'mindspace-rpc-server.test.mjs'));
}
async function copyMemindSourceSnapshot() {
console.log('==> 拷贝 Memind 受控源码快照');
const targetRoot = path.join(runtimeRoot, 'memind-source');
await fs.mkdir(targetRoot, { recursive: true });
const entries = await fs.readdir(root);
for (const entry of entries) {
if (excludeTopLevel.has(entry)) continue;
if (entry.endsWith('.pid')) continue;
if (entry === '.DS_Store' || entry === '.env' || entry === '.env.local') continue;
const sourcePath = path.join(root, entry);
if (!shouldCopyMemindSource(sourcePath)) continue;
await fs.cp(sourcePath, path.join(targetRoot, entry), {
recursive: true,
filter: shouldCopyMemindSource,
});
}
}
async function copyNodeModules() {
if (skipNodeModules) return;
if (!(await exists(nodeModulesDir))) {
throw new Error(`缺少 node_modules 目录: ${nodeModulesDir}`);
}
const resolvedNodeModulesDir = await fs.realpath(nodeModulesDir);
console.log('==> 拷贝生产运行依赖 node_modules');
await copyDir(resolvedNodeModulesDir, path.join(runtimeRoot, 'node_modules'));
await rewriteNodeModulesSymlinks(path.join(runtimeRoot, 'node_modules'), resolvedNodeModulesDir);
}
async function writeMetadata() {
const head = await fs.readFile(path.join(root, '.git', 'HEAD'), 'utf8').catch(() => 'unknown');
const runbook = [
'MindSpace service runtime artifact',
'',
'Production runtime target:',
' /Users/john/MindSpace',
'Standalone persisted data root:',
' /Users/john/MindSpace/data/mindspace',
'Standalone users root:',
' /Users/john/MindSpace/users',
'Shared Portal/H5 root:',
' /Users/john/Project/Memind',
'',
'This artifact contains:',
' server.mjs',
' mindspace-rpc-server.mjs',
' mindspace-service-bootstrap.mjs',
' scripts/run-mindspace-prod.sh',
' memind-source/ (controlled source snapshot, no runtime data)',
' node_modules/ (runtime dependencies)',
'',
'This artifact must not contain:',
' MindSpace/ runtime data',
' data/mindspace/ runtime data',
' users/ runtime data',
' .env / .env.local',
'',
'The production service may read shared Portal secrets from /Users/john/Project/Memind/.env,',
'but standalone MindSpace data paths must stay under /Users/john/MindSpace.',
'',
`Git head ref: ${head.trim()}`,
].join('\n');
await fs.writeFile(path.join(runtimeRoot, 'RUNBOOK.txt'), runbook, 'utf8');
}
async function main() {
if (!(await exists(serviceSourceRoot))) {
throw new Error(`缺少 MindSpace service 源目录: ${serviceSourceRoot}`);
}
await copyServiceSource();
await copyMemindSourceSnapshot();
await copyNodeModules();
await writeMetadata();
await fs.chmod(path.join(runtimeRoot, 'scripts', 'run-mindspace-prod.sh'), 0o755);
console.log('');
console.log(`MindSpace service runtime 已生成: ${runtimeRoot}`);
}
main().catch((error) => {
console.error(error instanceof Error ? error.message : error);
process.exit(1);
});