prefix the env with OFFICERDEV_, and keep reading the bare names
PORT, PUBLIC_URL and POSTGRES_URL are names half the software in the world uses. Bun auto-loads .env into EVERY process started in the platform directory, so a bare PORT reaches the sidecars, the tools they spawn and anything an agent runs from here — and a bare POSTGRES_URL reaches things with no business holding a database credential. Four parts. officer-setup now writes OFFICERDEV_-prefixed names, with the reasoning in the generated file so whoever opens it later knows why. Every consumer reads `OFFICERDEV_X ?? X` — eleven sites across the databases, the DAV router, officer-url.mjs, os-user-postgres and two scripts. The bare name is the FALLBACK, never the source, which is what makes this safe to ship before every machine has been converted. `bun run fix:env` converts a file in place. Idempotent, preserves comments and `export`, leaves the value untouched including an `=` inside it, and refuses to touch names belonging to other software — prefixing PATH or NODE_ENV does not rename a variable, it deletes it. And it runs on every restart from the server settings screen. There rather than at boot because a restart is the one moment the file can be rewritten and take effect in the same breath: the process about to start is the one that reads it. Non-fatal, since the fallback means the worst case is that nothing changed. gen-index reads either spelling out of the file directly, because it is run by hand as often as by the setup and a bare shell has no .env loaded. Verified by converting this machine's own .env: PORT, PUBLIC_URL and the database all still resolve, gen:index still finds its URL, 1152 pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
|
||||
// Giving every variable in `.env` the `OFFICERDEV_` prefix.
|
||||
//
|
||||
// ── Why prefix at all ──
|
||||
//
|
||||
// `PORT`, `PUBLIC_URL` and `POSTGRES_URL` are names half the software in the world uses. Bun auto-loads
|
||||
// `.env` into EVERY process started in the platform directory, so a bare `PORT` reaches the sidecars,
|
||||
// the tools they spawn and anything an agent runs from here — and a bare `POSTGRES_URL` reaches things
|
||||
// with no business holding a database credential. Prefixed, they cannot be picked up by accident.
|
||||
//
|
||||
// ── Why every consumer still reads both ──
|
||||
//
|
||||
// `process.env.OFFICERDEV_X ?? process.env.X`, everywhere. An install that has not been converted keeps
|
||||
// working, which is what makes this safe to ship before every machine has run it. The unprefixed name is
|
||||
// the fallback, never the source.
|
||||
//
|
||||
// ── Idempotent, because it runs unattended ──
|
||||
//
|
||||
// This runs on every restart from the server settings screen, not only when somebody types
|
||||
// `bun run fix:env`. An already-prefixed line is left exactly as it is, and a file that needs no change
|
||||
// is not written at all — writing an identical file still moves its mtime.
|
||||
|
||||
const PREFIX = 'OFFICERDEV_';
|
||||
|
||||
/**
|
||||
* Names that must NOT be prefixed.
|
||||
*
|
||||
* These are read by other software rather than by Officer: `NODE_ENV` is the runtime's, `PATH` and
|
||||
* `HOME` are the operating system's. Prefixing one does not rename a variable, it deletes it.
|
||||
*/
|
||||
const LEAVE_ALONE = new Set(['NODE_ENV', 'PATH', 'HOME', 'SHELL', 'USER', 'LANG', 'TZ', 'TMPDIR', 'XDG_DATA_HOME']);
|
||||
|
||||
export type FixEnvResult = { changed: string[]; alreadyPrefixed: number; path: string };
|
||||
|
||||
/** Rewrite one env file's text. Pure, so the rules are testable without a filesystem. */
|
||||
export function prefixEnvText(text: string): { text: string; changed: string[]; alreadyPrefixed: number } {
|
||||
const changed: string[] = [];
|
||||
let alreadyPrefixed = 0;
|
||||
|
||||
const out = text.split('\n').map((line) => {
|
||||
// Comments, blanks and anything that is not `KEY=…` pass through untouched. A `.env` is usually
|
||||
// commented, and losing that would be a poor trade for a rename.
|
||||
//
|
||||
// `export` is captured rather than skipped so it can be put back — dropping it would change what the
|
||||
// line does in a shell that sources this file.
|
||||
const match = line.match(/^(\s*)(export\s+)?([A-Za-z_][A-Za-z0-9_]*)(\s*=)(.*)$/);
|
||||
if (!match) return line;
|
||||
|
||||
const [, indent, exported, key, eq, value] = match as unknown as [
|
||||
string,
|
||||
string,
|
||||
string | undefined,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
];
|
||||
|
||||
if (key.startsWith(PREFIX)) {
|
||||
alreadyPrefixed += 1;
|
||||
return line;
|
||||
}
|
||||
if (LEAVE_ALONE.has(key)) return line;
|
||||
|
||||
changed.push(key);
|
||||
return `${indent}${exported ?? ''}${PREFIX}${key}${eq}${value}`;
|
||||
});
|
||||
|
||||
return { text: out.join('\n'), changed, alreadyPrefixed };
|
||||
}
|
||||
|
||||
/** Apply it to a file. Returns what changed; writes nothing when nothing did. */
|
||||
export function fixEnvFile(path: string): FixEnvResult {
|
||||
if (!existsSync(path)) return { changed: [], alreadyPrefixed: 0, path };
|
||||
const before = readFileSync(path, 'utf8');
|
||||
const { text, changed, alreadyPrefixed } = prefixEnvText(before);
|
||||
if (text !== before) writeFileSync(path, text);
|
||||
return { changed, alreadyPrefixed, path };
|
||||
}
|
||||
Reference in New Issue
Block a user