import { existsSync, statSync } from "node:fs"; import { delimiter, dirname, join, resolve } from "node:path"; export const NEXTCLAW_COMMAND_SURFACE_BIN_ENV = "NEXTCLAW_COMMAND_SURFACE_BIN"; const DEVELOPMENT_CONDITION_PATTERN = /(^|\s)--conditions(?:=|\s+)development(?=\s|$)/g; const COMMON_POSIX_BIN_DIRS = [ "/opt/homebrew/bin", "/usr/local/bin", "/opt/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin", ]; function splitPathEntries(rawPath: string): string[] { return rawPath .split(delimiter) .map((entry) => entry.trim()) .filter(Boolean); } function copyStringEnv(env: NodeJS.ProcessEnv): Record { return Object.fromEntries( Object.entries(env).filter(([, value]) => typeof value === "string"), ) as Record; } export type RuntimeChildEnvOptions = { execPath?: string; inheritBaseEnv?: boolean; }; function collectNodeModulesBinDirs(cwd: string): string[] { const entries: string[] = []; let current = resolve(cwd); while (current.length > 0) { const candidate = join(current, "node_modules", ".bin"); if (existsSync(candidate)) { entries.push(candidate); } const parent = resolve(current, ".."); if (parent === current) { break; } current = parent; } return entries; } function collectExternalCommandPathAdditions(cwd: string, env: NodeJS.ProcessEnv): string[] { const commandSurfaceBin = env[NEXTCLAW_COMMAND_SURFACE_BIN_ENV]?.trim() ?? ""; const additions = [ commandSurfaceBin, dirname(process.execPath), process.argv[1] ? dirname(process.argv[1]) : "", ...collectNodeModulesBinDirs(cwd) ]; if (process.platform !== "win32") { additions.push(...COMMON_POSIX_BIN_DIRS); } return additions.filter((entry) => { if (!entry.trim() || !existsSync(entry)) { return false; } try { return statSync(entry).isDirectory(); } catch { return false; } }); } function resolvePathKey(env: NodeJS.ProcessEnv): "PATH" | "Path" | "path" { if (typeof env.PATH === "string") { return "PATH"; } if (typeof env.Path === "string") { return "Path"; } if (typeof env.path === "string") { return "path"; } return "PATH"; } function buildExternalCommandPathValue(env: NodeJS.ProcessEnv, cwd: string): string | undefined { const pathKey = resolvePathKey(env); const existingEntries = splitPathEntries(env[pathKey] ?? ""); const additions = collectExternalCommandPathAdditions(cwd, env); const mergedEntries = Array.from(new Set([...additions, ...existingEntries])); return mergedEntries.length > 0 ? mergedEntries.join(delimiter) : undefined; } export function sanitizeNodeOptionsForExternalCommand(nodeOptions?: string): string | undefined { if (typeof nodeOptions !== "string") { return undefined; } const sanitized = nodeOptions .replace(DEVELOPMENT_CONDITION_PATTERN, " ") .replace(/\s+/g, " ") .trim(); return sanitized || undefined; } export function createRuntimeChildEnv( baseEnv: NodeJS.ProcessEnv = process.env, extraEnv: NodeJS.ProcessEnv = {}, options: RuntimeChildEnvOptions = {}, ): Record { const env = options.inheritBaseEnv ? { ...copyStringEnv(baseEnv), ...copyStringEnv(extraEnv) } : copyStringEnv(extraEnv); const pathKey = resolvePathKey({ ...baseEnv, ...extraEnv }); env[pathKey] = Array.from(new Set([ ...splitPathEntries(baseEnv[pathKey] ?? ""), ...splitPathEntries(extraEnv[pathKey] ?? ""), dirname(options.execPath ?? process.execPath), ])).join(delimiter); const sanitizedNodeOptions = sanitizeNodeOptionsForExternalCommand(env.NODE_OPTIONS); if (sanitizedNodeOptions) { env.NODE_OPTIONS = sanitizedNodeOptions; } else { delete env.NODE_OPTIONS; } return env; } export function createExternalCommandEnv( baseEnv: NodeJS.ProcessEnv = process.env, extraEnv: NodeJS.ProcessEnv = {}, options: { cwd?: string; } = {}, ): NodeJS.ProcessEnv { const env = { ...baseEnv, ...extraEnv }; const sanitizedNodeOptions = sanitizeNodeOptionsForExternalCommand(env.NODE_OPTIONS); if (sanitizedNodeOptions) { env.NODE_OPTIONS = sanitizedNodeOptions; } else { delete env.NODE_OPTIONS; } const pathKey = resolvePathKey(env); const nextPath = buildExternalCommandPathValue(env, options.cwd ?? process.cwd()); if (nextPath) { env[pathKey] = nextPath; } else { delete env[pathKey]; } return env; }