PORT is read in one place, and it has no default

officer-url.mjs is now the only file in the tree that touches process.env.PORT.
Twenty-two others read it and supplied their own default; a value with
twenty-two sources is not configuration, it is twenty-two things to keep in sync,
and they had already drifted three ways.

It throws when PORT is unset rather than guessing. A default only covers the case
where .env was never loaded — which is not a machine anyone wants running,
because POSTGRES_URL is missing in the same breath. What the default bought was a
process that starts, binds somewhere unexpected, and fails later for a reason
that does not name the cause. Same posture as jwt.ts with JWT_SECRET.

It is .mjs, not .ts, and that is the whole reason this could be one file. pm2
launches officer-pty with node (ecosystem.config.cjs) and everything else with
bun; node cannot import TypeScript, so a .ts module would have left the pty
sidecar holding the only surviving copy of the default — precisely the thing
being removed. allowJs is already on, so the TS callers still get types. Verified
both runtimes import it, and that PUBLIC_URL-style overrides still work.

It also exports API_URL and OFFICER_API_URL, because nineteen sidecars were
independently building `ws://127.0.0.1:${PORT}` and two more were building the
http form. Those are one listener described in two protocols — no sidecar binds
anything — so they belong beside the port rather than being rediscovered per
file.

server.tsx now takes PORT as a number, so Number(PORT) at the serve site is gone.

Not typechecked (empty node_modules, frozen installs). Every edited file parses
under `bun build --no-bundle`; node and bun both load the new module; the unset
and non-numeric paths were exercised; the pm2 profile still loads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-12 23:43:39 +00:00
co-authored by Claude Opus 5
parent e72cae4830
commit 3c7f52ab77
23 changed files with 80 additions and 26 deletions
+2 -1
View File
@@ -9,6 +9,7 @@ import {
} from 'officerdb';
import { introduceAgentPanel } from '../agent-handoff/deliver';
import { logger } from './logger';
import { OFFICER_API_URL } from '../../officer-url.mjs';
/**
* The browser's half of the address book: name a panel, look up what a panel is, rename, remove.
@@ -23,7 +24,7 @@ import { logger } from './logger';
/** A name has to survive being typed into a prompt and into a shell, so keep it boring. */
const NAME_RE = /^[a-z0-9][a-z0-9-]{0,30}[a-z0-9]$|^[a-z0-9]$/;
const API_ORIGIN = `http://127.0.0.1:${process.env.PORT ?? '9000'}`;
const API_ORIGIN = OFFICER_API_URL;
export function registerAgentPanelRoutes(router: Hono<any>): void {
// GET /chat/agent-panels?dashboardId=… — the address book for one dashboard.
+3 -2
View File
@@ -1,6 +1,7 @@
import { sign } from '@@/jwt';
import { PORT, OFFICER_API_URL } from '../../officer-url.mjs';
const { PORT = '9000', PUBLIC_URL } = process.env;
const { PUBLIC_URL } = process.env;
const PUBLIC_HOST = (() => {
try {
@@ -24,7 +25,7 @@ type TaskApiUser = { id: number; email: string; username?: string | null };
export async function buildTaskApiEnv(user: TaskApiUser): Promise<Record<string, string>> {
const token = await sign({ id: user.id, email: user.email, username: user.username ?? '' }, '12h');
return {
OFFICER_API_URL: `http://127.0.0.1:${PORT}`,
OFFICER_API_URL,
OFFICER_API_HOST: PUBLIC_HOST ?? `127.0.0.1:${PORT}`,
OFFICER_AUTH_TOKEN: token,
};