/* RFC-0822: Persist and restore operator-filled .env* files between mission workpiece and cache clone. Provides persistEnvFilesToCacheClone (mission.close) and restoreEnvFilesFromCacheClone (mission.materialize). Do not git-commit .env files — they are untracked artifacts in the cache clone. Do not merge .env with .env.example — .env.example is a committed template, .env is operator-filled. Do not copy .env files on mission.abort — aborted missions discard secrets. Do not encrypt .env files — the cache clone is a local protected store. RFC-0822: initial env-persist module with persistEnvFilesToCacheClone and restoreEnvFilesFromCacheClone. */ import path from "node:path"; import { createDefaultIO, type WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io"; export interface EnvPersistResult { copied: string[]; skipped: string[]; } const EXCLUDE_PATTERNS = [".env.example", /^\.env\..*\.example$/]; function shouldExclude(filename: string): boolean { for (const pattern of EXCLUDE_PATTERNS) { if (typeof pattern === "string") { if (filename === pattern) return true; } else { if (pattern.test(filename)) return true; } } return false; } export async function collectEnvFiles( dir: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { if (!(await io.exists(dir))) return []; const entries = await io.readdir(dir); const envFiles: string[] = []; for (const entry of entries) { if (!entry.isFile) continue; if (!entry.name.startsWith(".env")) continue; if (shouldExclude(entry.name)) continue; envFiles.push(entry.name); } return envFiles; } export async function persistEnvFilesToCacheClone( workpieceDir: string, cacheCloneDir: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { const copied: string[] = []; const skipped: string[] = []; const envFiles = await collectEnvFiles(workpieceDir); for (const filename of envFiles) { const src = path.join(workpieceDir, filename); const dest = path.join(cacheCloneDir, filename); try { await io.copyFile(src, dest); copied.push(filename); } catch { skipped.push(filename); } } return { copied, skipped }; } export async function restoreEnvFilesFromCacheClone( cacheCloneDir: string, workpieceDir: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { const copied: string[] = []; const skipped: string[] = []; const envFiles = await collectEnvFiles(cacheCloneDir); for (const filename of envFiles) { const src = path.join(cacheCloneDir, filename); const dest = path.join(workpieceDir, filename); try { let content = await io.readFile(src); content = content.replace( /^PUBLIC_IMAGE_PROVIDER=.*$/m, "PUBLIC_IMAGE_PROVIDER=build-portable", ); await io.writeFile(dest, content); copied.push(filename); } catch { skipped.push(filename); } } return { copied, skipped }; }