#!/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); });