/** * `.env` loader for verifier extensions. * * Both `verifiable.ts` (builder) and `verifier.ts` (verifier) call this on * session_start with their `ctx.cwd`. Each agent loads `.env` from its own * working directory — defensive: even if tmux drops env vars between spawn * and child, the verifier independently loads the same `.env` because it * shares cwd (the launcher passes `-c ` to tmux). * * Precedence (matches dotenv conventions): * 1. Existing `process.env` values are preserved (env-vars-already-set wins). * 2. Values in `.env` fill in only the gaps. * * Uses `process.loadEnvFile()` (Node >= 21.7) when available; falls back to a * manual parser for Node 20.6.x to 21.6.x so the declared engine range * (`>=20.6.0`) is actually respected. If `.env` doesn't exist, this is a no-op * (silent). The Pi CLI's own authenticated model configuration is the default * path; `.env` is only for users who want to provide provider API keys explicitly. */ import { promises as fs } from "node:fs"; import * as path from "node:path"; export interface LoadResult { loaded: boolean; path: string; reason?: string; } /** * Load `.env` from `cwd` into `process.env` if the file exists. * * - Non-existent `.env` → returns `{ loaded: false }` silently. Most users rely * on Pi CLI auth / SSO instead; that's fine. * - Malformed `.env` → returns `{ loaded: false, reason }`. Caller decides * whether to surface the warning to the user. * - Successful load → `{ loaded: true, path }`. Caller may choose to notify. * * Existing `process.env` keys are preserved: the manual parser skips keys * already in `process.env`, and `process.loadEnvFile` has the same semantics. */ export async function loadDotEnv(cwd: string): Promise { const envPath = path.join(cwd, ".env"); try { await fs.access(envPath); } catch { return { loaded: false, path: envPath }; } try { if (typeof process.loadEnvFile === "function") { // Node >= 21.7 — fast path, no extra parsing. process.loadEnvFile(envPath); } else { // Node 20.6.x to 21.6.x — manual dotenv parser. // Matches the same syntax process.loadEnvFile supports: // KEY=value // KEY="quoted value" // KEY='quoted value' // export KEY=value // mixed_CASE_Keys=value // Leading `export ` is stripped; surrounding quotes are removed. const raw = await fs.readFile(envPath, "utf8"); for (const line of raw.split("\n")) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#")) continue; const noExport = trimmed.replace(/^export\s+/i, ""); const m = noExport.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/); if (!m) continue; const key = m[1]; if (!key || key in process.env) continue; let value = m[2].trim(); // Strip surrounding single or double quotes if ( (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")) ) { value = value.slice(1, -1); } process.env[key] = value; } } return { loaded: true, path: envPath }; } catch (err) { return { loaded: false, path: envPath, reason: `failed to parse: ${(err as Error).message}`, }; } }