mirror of
https://github.com/NeuralNomadsAI/CodeNomad.git
synced 2026-08-29 18:22:07 +00:00
Prevent duplicate workspace ownership, bound concurrent creation and cleanup, and preserve canonical Windows and WSL identity while using the shared OpenCode V2 service. Keep session history directory-scoped for global projects, serialize native event projection, restore prompts and pending requests safely, expose interrupted generations, and remove unsupported direct Google credential handling. Make Electron and Tauri server resources reproducible from the root lockfile, retain the pinned beta SDK contract, and update CI and architecture documentation for the shared-service model. Validated with full UI, server, Electron, Rust, packaging, typecheck, Tauri build, and live prompt, permission, Form, compaction, and interruption smoke coverage.
327 lines
11 KiB
JavaScript
327 lines
11 KiB
JavaScript
const fs = require("fs")
|
|
const os = require("os")
|
|
const path = require("path")
|
|
const { spawnSync } = require("child_process")
|
|
|
|
const excludedDistRoots = new Set(["codenomad-server", "opencode-config", "opencode-config-template", "opencode-config.js"])
|
|
const npmTargets = {
|
|
"darwin-x64": { os: "darwin", cpu: "x64" },
|
|
"darwin-arm64": { os: "darwin", cpu: "arm64" },
|
|
"linux-x64": { os: "linux", cpu: "x64" },
|
|
"linux-arm64": { os: "linux", cpu: "arm64" },
|
|
"win32-x64": { os: "win32", cpu: "x64" },
|
|
"win32-arm64": { os: "win32", cpu: "arm64" },
|
|
}
|
|
|
|
function resolveNpmTarget(target = process.env.CODENOMAD_NODE_TARGET || `${process.platform}-${process.arch}`) {
|
|
const npmTarget = npmTargets[target]
|
|
if (!npmTarget) throw new Error(`Unsupported desktop packaging target: ${target}`)
|
|
return { target, ...npmTarget }
|
|
}
|
|
|
|
function resolveLockDependency(packages, ownerPath, dependency) {
|
|
let current = ownerPath
|
|
while (true) {
|
|
const candidate = current ? `${current}/node_modules/${dependency}` : `node_modules/${dependency}`
|
|
if (packages[candidate]) return candidate
|
|
const nested = current.lastIndexOf("/node_modules/")
|
|
if (nested >= 0) current = current.slice(0, nested)
|
|
else if (current.startsWith("node_modules/")) current = ""
|
|
else {
|
|
const parent = current.lastIndexOf("/")
|
|
current = parent >= 0 ? current.slice(0, parent) : ""
|
|
}
|
|
if (!current) {
|
|
const rootCandidate = `node_modules/${dependency}`
|
|
return packages[rootCandidate] ? rootCandidate : null
|
|
}
|
|
}
|
|
}
|
|
|
|
function validateServerProductionLock(lock) {
|
|
const packages = lock && lock.packages
|
|
if (!packages || !packages["packages/server"]) throw new Error("Root package-lock.json does not contain packages/server")
|
|
const pending = ["packages/server"]
|
|
const visited = new Set()
|
|
while (pending.length) {
|
|
const packagePath = pending.pop()
|
|
if (!packagePath || visited.has(packagePath)) continue
|
|
visited.add(packagePath)
|
|
const pkg = packages[packagePath]
|
|
if (!pkg) throw new Error(`Root package-lock.json is missing ${packagePath}`)
|
|
if (packagePath !== "packages/server" && (!pkg.version || !pkg.resolved || !pkg.integrity)) {
|
|
throw new Error(`Root package-lock.json does not integrity-pin ${packagePath}`)
|
|
}
|
|
const dependencies = { ...pkg.dependencies, ...pkg.optionalDependencies }
|
|
for (const dependency of Object.keys(dependencies)) {
|
|
const resolved = resolveLockDependency(packages, packagePath, dependency)
|
|
if (!resolved) throw new Error(`Root package-lock.json cannot resolve ${dependency} from ${packagePath}`)
|
|
pending.push(resolved)
|
|
}
|
|
}
|
|
return visited
|
|
}
|
|
|
|
function stagePackagedServer(options) {
|
|
const { workspaceRoot, serverRoot, log = () => {}, env = process.env } = options
|
|
const npmTarget = resolveNpmTarget(options.target || env.CODENOMAD_NODE_TARGET)
|
|
const lockPath = path.join(workspaceRoot, "package-lock.json")
|
|
validateServerProductionLock(JSON.parse(fs.readFileSync(lockPath, "utf8")))
|
|
|
|
const stagingRoot = fs.mkdtempSync(path.join(os.tmpdir(), "codenomad-server-"))
|
|
const stagedServerRoot = path.join(stagingRoot, "packages", "server")
|
|
try {
|
|
fs.mkdirSync(stagedServerRoot, { recursive: true })
|
|
fs.copyFileSync(path.join(workspaceRoot, "package.json"), path.join(stagingRoot, "package.json"))
|
|
fs.copyFileSync(lockPath, path.join(stagingRoot, "package-lock.json"))
|
|
fs.copyFileSync(path.join(serverRoot, "package.json"), path.join(stagedServerRoot, "package.json"))
|
|
|
|
log(`installing production server dependencies from the workspace lock for ${npmTarget.target}`)
|
|
const npmArgs = [
|
|
"ci",
|
|
"--workspace",
|
|
"@neuralnomads/codenomad",
|
|
"--include-workspace-root=false",
|
|
"--omit=dev",
|
|
"--ignore-scripts",
|
|
`--os=${npmTarget.os}`,
|
|
`--cpu=${npmTarget.cpu}`,
|
|
"--fund=false",
|
|
"--audit=false",
|
|
]
|
|
const npmCli = env.npm_execpath && env.npm_node_execpath
|
|
? [env.npm_node_execpath, [env.npm_execpath, ...npmArgs]]
|
|
: process.platform === "win32"
|
|
? [env.ComSpec || "cmd.exe", ["/d", "/s", "/c", "npm", ...npmArgs]]
|
|
: ["npm", npmArgs]
|
|
const result = spawnSync(npmCli[0], npmCli[1], { cwd: stagingRoot, stdio: "inherit", env })
|
|
if (result.error) throw result.error
|
|
if (result.status !== 0) throw new Error(`npm ci exited with code ${result.status ?? 1}`)
|
|
|
|
const rootModules = path.join(stagingRoot, "node_modules")
|
|
const serverModules = path.join(stagedServerRoot, "node_modules")
|
|
const serverOverrides = path.join(stagingRoot, "server-node-modules")
|
|
if (fs.existsSync(serverModules)) fs.renameSync(serverModules, serverOverrides)
|
|
fs.rmSync(path.join(rootModules, "@neuralnomads", "codenomad"), { recursive: true, force: true })
|
|
fs.cpSync(rootModules, serverModules, { recursive: true, dereference: true })
|
|
if (fs.existsSync(serverOverrides)) fs.cpSync(serverOverrides, serverModules, { recursive: true, dereference: true })
|
|
for (const artifact of ["public", "dist"]) {
|
|
fs.cpSync(path.join(serverRoot, artifact), path.join(stagedServerRoot, artifact), { recursive: true })
|
|
}
|
|
return { stagingRoot, stagedServerRoot, target: npmTarget.target }
|
|
} catch (error) {
|
|
fs.rmSync(stagingRoot, { recursive: true, force: true })
|
|
throw error
|
|
}
|
|
}
|
|
|
|
function copyPackagedServerResources(options) {
|
|
const { serverRoot, serverDest, log = () => {} } = options
|
|
|
|
fs.rmSync(serverDest, { recursive: true, force: true })
|
|
fs.mkdirSync(serverDest, { recursive: true })
|
|
|
|
copyRequiredArtifact(serverRoot, serverDest, "package.json", log)
|
|
copyRequiredArtifact(serverRoot, serverDest, "public", log)
|
|
copyRequiredArtifact(serverRoot, serverDest, "node_modules", log)
|
|
copyServerDist(serverRoot, serverDest, log)
|
|
stripNodeModuleBins(path.join(serverDest, "node_modules"), log)
|
|
pruneKnownServerDependencies(path.join(serverDest, "node_modules"), log)
|
|
}
|
|
|
|
function copyRequiredArtifact(serverRoot, serverDest, name, log) {
|
|
const from = path.join(serverRoot, name)
|
|
const to = path.join(serverDest, name)
|
|
if (!fs.existsSync(from)) {
|
|
throw new Error(`Missing required server artifact: ${from}`)
|
|
}
|
|
fs.cpSync(from, to, { recursive: true, dereference: true })
|
|
log(`copied ${name}`)
|
|
}
|
|
|
|
function copyServerDist(serverRoot, serverDest, log) {
|
|
const from = path.join(serverRoot, "dist")
|
|
const to = path.join(serverDest, "dist")
|
|
|
|
if (!fs.existsSync(from)) {
|
|
throw new Error(`Missing required server artifact: ${from}`)
|
|
}
|
|
|
|
fs.cpSync(from, to, {
|
|
recursive: true,
|
|
dereference: true,
|
|
filter(source) {
|
|
const relative = path.relative(from, source)
|
|
if (!relative) return true
|
|
const [root] = relative.split(path.sep)
|
|
if (excludedDistRoots.has(root)) return false
|
|
return !/\.test\.js$/.test(path.basename(relative))
|
|
},
|
|
})
|
|
log("copied filtered dist")
|
|
}
|
|
|
|
function stripNodeModuleBins(root, log) {
|
|
if (!fs.existsSync(root)) return
|
|
|
|
const stack = [root]
|
|
let removed = 0
|
|
|
|
while (stack.length > 0) {
|
|
const current = stack.pop()
|
|
if (!current) break
|
|
|
|
let entries
|
|
try {
|
|
entries = fs.readdirSync(current, { withFileTypes: true })
|
|
} catch {
|
|
continue
|
|
}
|
|
|
|
for (const entry of entries) {
|
|
const full = path.join(current, entry.name)
|
|
if (entry.name === ".bin") {
|
|
fs.rmSync(full, { recursive: true, force: true })
|
|
removed += 1
|
|
continue
|
|
}
|
|
if (entry.isDirectory()) {
|
|
stack.push(full)
|
|
}
|
|
}
|
|
}
|
|
|
|
if (removed > 0) {
|
|
log(`removed ${removed} node_modules/.bin directories`)
|
|
}
|
|
}
|
|
|
|
function removeIfExists(target) {
|
|
if (!fs.existsSync(target)) return 0
|
|
fs.rmSync(target, { recursive: true, force: true })
|
|
return 1
|
|
}
|
|
|
|
function removeFilesMatching(root, patterns) {
|
|
if (!fs.existsSync(root)) return 0
|
|
|
|
const stack = [root]
|
|
let removed = 0
|
|
|
|
while (stack.length > 0) {
|
|
const current = stack.pop()
|
|
if (!current) break
|
|
|
|
let entries
|
|
try {
|
|
entries = fs.readdirSync(current, { withFileTypes: true })
|
|
} catch {
|
|
continue
|
|
}
|
|
|
|
for (const entry of entries) {
|
|
const full = path.join(current, entry.name)
|
|
if (entry.isDirectory()) {
|
|
stack.push(full)
|
|
continue
|
|
}
|
|
|
|
if (entry.isFile() && patterns.some((pattern) => pattern.test(entry.name))) {
|
|
fs.rmSync(full, { force: true })
|
|
removed += 1
|
|
}
|
|
}
|
|
}
|
|
|
|
return removed
|
|
}
|
|
|
|
function prunePackage(root, options) {
|
|
if (!fs.existsSync(root)) return 0
|
|
|
|
let removed = 0
|
|
for (const relativePath of options.remove ?? []) {
|
|
removed += removeIfExists(path.join(root, relativePath))
|
|
}
|
|
if (options.filePatterns?.length) {
|
|
removed += removeFilesMatching(root, options.filePatterns)
|
|
}
|
|
return removed
|
|
}
|
|
|
|
function pruneKnownServerDependencies(root, log) {
|
|
if (!fs.existsSync(root)) return
|
|
|
|
let removed = 0
|
|
const declarationAndMaps = [/\.d\.[cm]?ts$/, /\.map$/]
|
|
const packageDocs = [/\.md$/i, /\.markdown$/i]
|
|
|
|
removed += prunePackage(path.join(root, "openai"), {
|
|
remove: ["CHANGELOG.md", "README.md", "bin", "src"],
|
|
filePatterns: [...declarationAndMaps],
|
|
})
|
|
removed += prunePackage(path.join(root, "fastify"), {
|
|
remove: ["docs", "examples", "integration", "test", "types", "build", "fastify.d.ts"],
|
|
filePatterns: [...packageDocs],
|
|
})
|
|
removed += prunePackage(path.join(root, "@fastify", "cors"), {
|
|
remove: ["bench.js", "benchmark", "test", "types"],
|
|
filePatterns: [...packageDocs],
|
|
})
|
|
removed += prunePackage(path.join(root, "@fastify", "reply-from"), {
|
|
remove: ["examples", "test", "types"],
|
|
filePatterns: [...packageDocs],
|
|
})
|
|
removed += prunePackage(path.join(root, "@fastify", "static"), {
|
|
remove: ["example", "test", "types", "tsconfig.eslint.json"],
|
|
filePatterns: [...packageDocs],
|
|
})
|
|
removed += prunePackage(path.join(root, "pino"), {
|
|
remove: [
|
|
"benchmarks",
|
|
"browser.js",
|
|
"build",
|
|
"docs",
|
|
"docsify",
|
|
"examples",
|
|
"favicon-16x16.png",
|
|
"favicon-32x32.png",
|
|
"favicon.ico",
|
|
"index.html",
|
|
"pino-banner.png",
|
|
"pino-logo-hire.png",
|
|
"pino-tree.png",
|
|
"pino.d.ts",
|
|
"pretty-demo.png",
|
|
"test",
|
|
"tsconfig.json",
|
|
],
|
|
filePatterns: [...packageDocs],
|
|
})
|
|
removed += prunePackage(path.join(root, "undici"), {
|
|
remove: ["docs", "index.d.ts", "scripts", "types"],
|
|
filePatterns: [...packageDocs],
|
|
})
|
|
removed += prunePackage(path.join(root, "zod"), {
|
|
remove: ["README.md", "src"],
|
|
filePatterns: [...declarationAndMaps],
|
|
})
|
|
removed += prunePackage(path.join(root, "yaml"), {
|
|
remove: ["README.md", "bin.mjs", "browser"],
|
|
filePatterns: [...declarationAndMaps],
|
|
})
|
|
removed += prunePackage(path.join(root, "node-forge"), {
|
|
remove: ["README.md", "flash"],
|
|
})
|
|
|
|
if (removed > 0) {
|
|
log(`removed ${removed} known non-runtime files/directories from server dependencies`)
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
copyPackagedServerResources,
|
|
resolveNpmTarget,
|
|
stagePackagedServer,
|
|
validateServerProductionLock,
|
|
}
|