// child-env — resolve the environment a supervised app is spawned with.
//
// Extracted from supervisor.js so it is TESTABLE. supervisor.js is a script: importing it starts a
// respawn loop, so nothing inside it could be asserted on. This is load-bearing enough that
// "it looked right" is not an acceptable standard — a defect here mis-configures every app on the
// fleet at once, silently, because a wrong env usually still boots.
//
// THE BUG THIS EXISTS FOR. The supervisor parsed PGH_SUP_CHILD_ENV once at start and reused it for
// every respawn. An OTA respawn (exit 75) therefore re-exec'd the NEW code with the OLD environment:
// `pgh app update` installs a tree whose manifest declares new `defaults.env`, the app restarts, and
// none of it applies. `effectiveEnv` was always correct; it simply never ran again.
//
// Measured on tabertron 2026-08-05: favela-web auto-updated to 0.3.33 and came up without
// FAVELA_AP_BRIDGE (added in that release) AND without ATPROTO_OVERLAY_ROOT — in the manifest since
// 0.2.25. The latter points favela-web at hpp's SHARED record store; absent, it fell back to its own
// empty overlay and co-hosted DIDs were rebuilt from firehose backfill instead of the authoritative
// baseline. Everything still rendered, so nothing surfaced it.
// The supervisor's own plumbing. Present in the SUPERVISOR's process.env but never in the child's,
// because the parent computed the child env in a process that had none of it. effectiveEnv() starts
// from process.env, so re-resolving inside the supervisor leaks all of this into the app — including
// the entire PGH_SUP_CHILD_ENV JSON blob as a single string variable.
export const SUP_ONLY = Object.freeze([
'PGH_SUP_NAME',
'PGH_SUP_ENTRYPOINT',
'PGH_SUP_RUNTIME',
'PGH_SUP_RUNTIME_PATH',
'PGH_SUP_ARGS_JSON',
'PGH_SUP_CHILD_ENV'
])
// Resolve the env for the next spawn.
//
// deps are injected (rather than imported) so a test can drive this without a real install tree:
// readInstallManifest(name) -> manifest | null
// effectiveEnv(name, manifest) -> object (merges process.env + defaults.env + user overrides)
// fallback -> the env the PARENT computed at `pgh app start`
// onWarn(msg) -> optional logger
//
// ⚠ NEVER THROWS. A supervisor that cannot spawn because a manifest read failed leaves the app DOWN,
// which is strictly worse than running one release behind on env. Any failure returns `fallback`.
export function resolveChildEnv ({ name, readInstallManifest, effectiveEnv, fallback = {}, onWarn = null } = {}) {
try {
if (!name) return fallback
const manifest = readInstallManifest(name)
// No manifest is NOT an error — an adopted or hand-placed app may have none. Keep what works.
if (!manifest) return fallback
const env = effectiveEnv(name, manifest)
// An empty result is as wrong as a non-object: effectiveEnv() always merges process.env, so a
// zero-key answer means something upstream failed. Spawning with no environment would strip PATH
// and HOME from the app — it would fail in a way that looks nothing like a config problem.
if (!env || typeof env !== 'object' || Object.keys(env).length === 0) return fallback
for (const k of SUP_ONLY) delete env[k]
// The parent sets this explicitly after effectiveEnv(): an install name can differ from the
// package name (adopted dev installs), and the app uses it to register with pgh-sidecar.
env.PGH_APP_NAME = name
return env
} catch (err) {
if (onWarn) { try { onWarn('env re-resolve failed, reusing the env from start: ' + (err && err.message)) } catch {} }
return fallback
}
}
// Which keys changed between two resolved envs. Used to say so ONCE in the log — the failure above
// was invisible from every surface, and an env that silently differs from the manifest must not be
// invisible again. Values are compared, not just key presence, so a CHANGED value is reported too.
export function envDelta (before, after) {
if (!before) return []
const out = []
for (const k of new Set([...Object.keys(before), ...Object.keys(after)])) {
if (before[k] !== after[k]) out.push(k)
}
return out.sort()
}