feat(cli): auto-load .env and .env.local from cwd
On every CLI invocation, parse .env (and .env.local if present) from the working directory into process.env before dispatching to command handlers. Existing shell-exported variables always win — the loader never clobbers an explicitly-set value. .env.local overrides .env. Motivation: FUSION_DAEMON_TOKEN (and soon other config knobs) is more ergonomic as a gitignored local file than as a shell export each session. Without this, `fn dashboard` falls back to auto-generating a new token on every restart, which means the banner URL changes every time and stale localStorage tokens silently return 401 on every API call. SSE and WebSocket clients already carry the token via appendTokenQuery (fn_token= query-string fallback, since EventSource and WebSocket constructors cannot set Authorization headers) — verified: every `new EventSource` and `new WebSocket` call site is wrapped. Hand-rolled minimal parser (no new dependency) to keep the bundled single-binary CLI lean. Supports KEY=value, quoted values, comments, blank lines. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -59,6 +59,48 @@ function configurePiPackage(): void {
|
||||
|
||||
configurePiPackage();
|
||||
|
||||
/**
|
||||
* Load `.env` (and `.env.local`) from the current working directory into
|
||||
* process.env so that secrets like FUSION_DAEMON_TOKEN can live in a local
|
||||
* gitignored file instead of being exported manually each session.
|
||||
*
|
||||
* Existing environment variables always win — this loader never clobbers
|
||||
* values the user set explicitly in their shell. `.env.local` overrides
|
||||
* `.env` when both are present.
|
||||
*
|
||||
* We deliberately hand-roll a minimal parser instead of pulling in dotenv:
|
||||
* the CLI ships as a single bundled binary and we want to keep it lean.
|
||||
*/
|
||||
function loadEnvFile(path: string): void {
|
||||
if (!existsSync(path)) return;
|
||||
const contents = readFileSync(path, "utf-8");
|
||||
for (const rawLine of contents.split(/\r?\n/)) {
|
||||
const line = rawLine.trim();
|
||||
if (!line || line.startsWith("#")) continue;
|
||||
const eq = line.indexOf("=");
|
||||
if (eq === -1) continue;
|
||||
const key = line.slice(0, eq).trim();
|
||||
if (!key || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
|
||||
if (process.env[key] !== undefined) continue; // Shell wins.
|
||||
let value = line.slice(eq + 1).trim();
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
function loadLocalEnv(): void {
|
||||
const cwd = process.cwd();
|
||||
loadEnvFile(join(cwd, ".env"));
|
||||
loadEnvFile(join(cwd, ".env.local"));
|
||||
}
|
||||
|
||||
loadLocalEnv();
|
||||
|
||||
// Command handlers are loaded lazily so --help can return immediately
|
||||
// without importing the full command graph.
|
||||
async function loadCommandHandlers() {
|
||||
|
||||
Reference in New Issue
Block a user