import { existsSync, lstatSync, readFileSync } from "node:fs"; import path from "node:path"; const LEGACY_WINDOWS_HOME_PREFIX_RE = /^([A-Za-z]:[\\/].*?AppData[\\/]Roaming[\\/])(Paperclip CN|Paperclip)([\\/]|$)/i; const DESKTOP_USER_DATA_DIRNAME = "penclip"; function normalizeLegacyDesktopStoragePath(value: string): string { return value.replace( LEGACY_WINDOWS_HOME_PREFIX_RE, (_, prefix: string, _name: string, suffix: string) => `${prefix}${DESKTOP_USER_DATA_DIRNAME}${suffix}`, ); } function parseEnvFile(contents: string): Record { const entries: Record = {}; for (const rawLine of contents.split(/\r?\n/)) { const line = rawLine.trim(); if (!line || line.startsWith("#")) continue; const match = rawLine.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$/); if (!match) continue; const [, key, rawValue] = match; const value = rawValue.trim(); if (!value) { entries[key] = ""; continue; } if (value.startsWith("#")) { entries[key] = ""; continue; } let parsedValue = value; if ( (value.startsWith("\"") && value.endsWith("\"")) || (value.startsWith("'") && value.endsWith("'")) ) { parsedValue = value.slice(1, -1); } else { parsedValue = value.replace(/\s+#.*$/, "").trim(); } if (key === "PAPERCLIP_HOME" || key === "PAPERCLIP_CONTEXT" || key === "PAPERCLIP_WORKTREES_DIR") { parsedValue = normalizeLegacyDesktopStoragePath(parsedValue); } entries[key] = parsedValue; continue; } return entries; } type WorktreeEnvBootstrapResult = | { envPath: null; missingEnv: false } | { envPath: string; missingEnv: true } | { envPath: string; missingEnv: false }; export function isLinkedGitWorktreeCheckout(rootDir: string): boolean { const gitMetadataPath = path.join(rootDir, ".git"); if (!existsSync(gitMetadataPath)) return false; const stat = lstatSync(gitMetadataPath); if (!stat.isFile()) return false; return readFileSync(gitMetadataPath, "utf8").trimStart().startsWith("gitdir:"); } export function resolveWorktreeEnvFilePath(rootDir: string): string { return path.resolve(rootDir, ".paperclip", ".env"); } export function bootstrapDevRunnerWorktreeEnv( rootDir: string, env: NodeJS.ProcessEnv = process.env, ): WorktreeEnvBootstrapResult { if (!isLinkedGitWorktreeCheckout(rootDir)) { return { envPath: null, missingEnv: false, }; } const envPath = resolveWorktreeEnvFilePath(rootDir); if (!existsSync(envPath)) { return { envPath, missingEnv: true, }; } const entries = parseEnvFile(readFileSync(envPath, "utf8")); for (const [key, value] of Object.entries(entries)) { if (typeof env[key] === "string" && env[key]!.trim().length > 0) continue; env[key] = value; } return { envPath, missingEnv: false, }; }