| 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<string, string> { |
| const entries: Record<string, string> = {}; |
|
|
| 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, |
| }; |
| } |
|
|