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>
76 lines
3.9 KiB
JavaScript
76 lines
3.9 KiB
JavaScript
// Where the app is, and the only place that knows.
|
|
//
|
|
// ── Why this file is .mjs ──
|
|
//
|
|
// Every process is bun except `officer-pty`, which pm2 launches with node (see
|
|
// ecosystem.config.cjs). Node cannot import TypeScript, so a .ts module here would have left the pty
|
|
// sidecar with its own copy — which is exactly the thing this file exists to end. Plain JS is
|
|
// importable by both, and `allowJs` in tsconfig.json means the TS callers still get types.
|
|
//
|
|
// ── Why there is no default ──
|
|
//
|
|
// There were twenty-two, and they disagreed: 5000 in the app and nineteen sidecars, 9010 in
|
|
// user-instance.ts, 9000 in .env.example. Each was defensible where it was written and none was
|
|
// visible from the others.
|
|
//
|
|
// A default is a guess at a value that .env always supplies. The one case it covers — PORT genuinely
|
|
// unset — is not a machine anyone wants running: it means .env was not loaded, so POSTGRES_URL is
|
|
// missing too and nothing works anyway. What a default buys there is a process that starts, binds
|
|
// somewhere unexpected and fails later for a reason that does not name the cause.
|
|
//
|
|
// So: no default anywhere, and this throws. Same posture as jwt.ts with JWT_SECRET.
|
|
|
|
const raw = process.env.OFFICERDEV_PORT ?? process.env.PORT;
|
|
|
|
if (!raw) {
|
|
throw new Error(
|
|
[
|
|
'PORT is not set.',
|
|
'',
|
|
'Officer reads it from .env, which bun auto-loads from the working directory. If this is a pm2',
|
|
'process, the `cwd` pin in ecosystem.profile.cjs is what puts it there — a PORT this empty',
|
|
'usually means .env was never loaded at all, and POSTGRES_URL is missing too.',
|
|
'',
|
|
'Set PORT in .env (officer-setup writes 9000), or start from the repo root.',
|
|
].join('\n'),
|
|
);
|
|
}
|
|
|
|
const parsed = Number(raw);
|
|
|
|
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
|
|
throw new Error(`PORT must be an integer between 1 and 65535, but it is ${JSON.stringify(raw)}`);
|
|
}
|
|
|
|
/** The port the app binds. Everything else addresses it; nothing else binds. */
|
|
export const PORT = parsed;
|
|
|
|
// The app serves HTTP and WebSocket on ONE listener, so these are the same port in two protocols.
|
|
// Sidecars append their own path — `/api/sidecar/register` for the registration socket.
|
|
//
|
|
// The env overrides are kept because they are explicit escape hatches rather than defaults: nothing
|
|
// sets either today, and a value that is present is a deliberate act rather than a guess.
|
|
export const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${PORT}`;
|
|
export const OFFICER_API_URL = process.env.OFFICER_API_URL ?? `http://127.0.0.1:${PORT}`;
|
|
|
|
// ── The Anthropic proxy: PORT + 1, derived, not configured ──
|
|
//
|
|
// This is the ONE sidecar that binds a fixed port. Every other one binds `port: 0`, lets the kernel
|
|
// choose, and reports what it got back over the registration socket — which works because their
|
|
// consumer is the platform.
|
|
//
|
|
// The proxy cannot do that. Its consumer is `claude`, spawned by a DIFFERENT pm2 process
|
|
// (officer-agent), which needs ANTHROPIC_BASE_URL at spawn time and has no channel to ask the proxy
|
|
// what port it landed on. Two independent processes with nothing between them have to agree in
|
|
// advance, so the number has to be predictable rather than discovered.
|
|
//
|
|
// It was ANTHROPIC_PROXY_PORT, defaulting to 5051 in four separate files: the one that binds it and
|
|
// three that guessed the same constant to find it. 5051 was chosen against nothing and could collide
|
|
// with anything the owner installs later, with the symptom being chat failing while the rest of the
|
|
// platform looked healthy.
|
|
//
|
|
// PORT + 1 keeps the predictability and removes both problems. There is no variable to set, no second
|
|
// number to keep in agreement with the first, and the pair moves together when the install moves.
|
|
export const ANTHROPIC_PROXY_PORT = PORT + 1;
|
|
export const ANTHROPIC_PROXY_URL = `http://127.0.0.1:${ANTHROPIC_PROXY_PORT}`;
|