@theford/runly 0.0.0-stage → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +157 -2
  3. package/dist/cli/client.d.ts +80 -0
  4. package/dist/cli/client.js +294 -0
  5. package/dist/cli/client.js.map +1 -0
  6. package/dist/cli/daemon-unit.d.ts +58 -0
  7. package/dist/cli/daemon-unit.js +289 -0
  8. package/dist/cli/daemon-unit.js.map +1 -0
  9. package/dist/cli/index.d.ts +2 -0
  10. package/dist/cli/index.js +1482 -0
  11. package/dist/cli/index.js.map +1 -0
  12. package/dist/cli/install.d.ts +9 -0
  13. package/dist/cli/install.js +53 -0
  14. package/dist/cli/install.js.map +1 -0
  15. package/dist/cli/logs.d.ts +43 -0
  16. package/dist/cli/logs.js +232 -0
  17. package/dist/cli/logs.js.map +1 -0
  18. package/dist/cli/tether.d.ts +3 -0
  19. package/dist/cli/tether.js +65 -0
  20. package/dist/cli/tether.js.map +1 -0
  21. package/dist/cli/watchdog.d.ts +1 -0
  22. package/dist/cli/watchdog.js +81 -0
  23. package/dist/cli/watchdog.js.map +1 -0
  24. package/dist/core/builds.d.ts +20 -0
  25. package/dist/core/builds.js +93 -0
  26. package/dist/core/builds.js.map +1 -0
  27. package/dist/core/caller-env.d.ts +17 -0
  28. package/dist/core/caller-env.js +77 -0
  29. package/dist/core/caller-env.js.map +1 -0
  30. package/dist/core/diagnostics.d.ts +68 -0
  31. package/dist/core/diagnostics.js +34 -0
  32. package/dist/core/diagnostics.js.map +1 -0
  33. package/dist/core/env-vars.d.ts +35 -0
  34. package/dist/core/env-vars.js +32 -0
  35. package/dist/core/env-vars.js.map +1 -0
  36. package/dist/core/events.d.ts +10 -0
  37. package/dist/core/events.js +42 -0
  38. package/dist/core/events.js.map +1 -0
  39. package/dist/core/exec.d.ts +22 -0
  40. package/dist/core/exec.js +84 -0
  41. package/dist/core/exec.js.map +1 -0
  42. package/dist/core/journal.d.ts +227 -0
  43. package/dist/core/journal.js +395 -0
  44. package/dist/core/journal.js.map +1 -0
  45. package/dist/core/logs.d.ts +56 -0
  46. package/dist/core/logs.js +259 -0
  47. package/dist/core/logs.js.map +1 -0
  48. package/dist/core/manifest.d.ts +292 -0
  49. package/dist/core/manifest.js +179 -0
  50. package/dist/core/manifest.js.map +1 -0
  51. package/dist/core/namespaces.d.ts +7 -0
  52. package/dist/core/namespaces.js +59 -0
  53. package/dist/core/namespaces.js.map +1 -0
  54. package/dist/core/paths.d.ts +31 -0
  55. package/dist/core/paths.js +65 -0
  56. package/dist/core/paths.js.map +1 -0
  57. package/dist/core/policy.d.ts +94 -0
  58. package/dist/core/policy.js +122 -0
  59. package/dist/core/policy.js.map +1 -0
  60. package/dist/core/ports.d.ts +52 -0
  61. package/dist/core/ports.js +94 -0
  62. package/dist/core/ports.js.map +1 -0
  63. package/dist/core/presets.d.ts +25 -0
  64. package/dist/core/presets.js +86 -0
  65. package/dist/core/presets.js.map +1 -0
  66. package/dist/core/procscan.d.ts +111 -0
  67. package/dist/core/procscan.js +342 -0
  68. package/dist/core/procscan.js.map +1 -0
  69. package/dist/core/retention.d.ts +97 -0
  70. package/dist/core/retention.js +322 -0
  71. package/dist/core/retention.js.map +1 -0
  72. package/dist/core/sleep.d.ts +51 -0
  73. package/dist/core/sleep.js +99 -0
  74. package/dist/core/sleep.js.map +1 -0
  75. package/dist/core/tree-ledger.d.ts +39 -0
  76. package/dist/core/tree-ledger.js +92 -0
  77. package/dist/core/tree-ledger.js.map +1 -0
  78. package/dist/core/types.d.ts +21 -0
  79. package/dist/core/types.js +6 -0
  80. package/dist/core/types.js.map +1 -0
  81. package/dist/core/units.d.ts +20 -0
  82. package/dist/core/units.js +69 -0
  83. package/dist/core/units.js.map +1 -0
  84. package/dist/core/upkeep.d.ts +90 -0
  85. package/dist/core/upkeep.js +176 -0
  86. package/dist/core/upkeep.js.map +1 -0
  87. package/dist/core/util.d.ts +46 -0
  88. package/dist/core/util.js +168 -0
  89. package/dist/core/util.js.map +1 -0
  90. package/dist/core/version.d.ts +39 -0
  91. package/dist/core/version.js +144 -0
  92. package/dist/core/version.js.map +1 -0
  93. package/dist/core/worktree.d.ts +51 -0
  94. package/dist/core/worktree.js +309 -0
  95. package/dist/core/worktree.js.map +1 -0
  96. package/dist/daemon/budget.d.ts +155 -0
  97. package/dist/daemon/budget.js +288 -0
  98. package/dist/daemon/budget.js.map +1 -0
  99. package/dist/daemon/election.d.ts +9 -0
  100. package/dist/daemon/election.js +140 -0
  101. package/dist/daemon/election.js.map +1 -0
  102. package/dist/daemon/engine.d.ts +1703 -0
  103. package/dist/daemon/engine.js +5305 -0
  104. package/dist/daemon/engine.js.map +1 -0
  105. package/dist/daemon/index.d.ts +2 -0
  106. package/dist/daemon/index.js +412 -0
  107. package/dist/daemon/index.js.map +1 -0
  108. package/dist/daemon/proxy.d.ts +108 -0
  109. package/dist/daemon/proxy.js +615 -0
  110. package/dist/daemon/proxy.js.map +1 -0
  111. package/dist/daemon/supervisor.d.ts +101 -0
  112. package/dist/daemon/supervisor.js +493 -0
  113. package/dist/daemon/supervisor.js.map +1 -0
  114. package/dist/drivers/appliances.d.ts +11 -0
  115. package/dist/drivers/appliances.js +174 -0
  116. package/dist/drivers/appliances.js.map +1 -0
  117. package/dist/drivers/datastores.d.ts +144 -0
  118. package/dist/drivers/datastores.js +775 -0
  119. package/dist/drivers/datastores.js.map +1 -0
  120. package/dist/drivers/preview.d.ts +69 -0
  121. package/dist/drivers/preview.js +605 -0
  122. package/dist/drivers/preview.js.map +1 -0
  123. package/dist/drivers/types.d.ts +70 -0
  124. package/dist/drivers/types.js +13 -0
  125. package/dist/drivers/types.js.map +1 -0
  126. package/package.json +63 -4
  127. package/schema/backlot.schema.json +549 -0
  128. package/schema/runly.schema.json +556 -0
@@ -0,0 +1,412 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The runly daemon: HTTP over a unix socket, auto-spawned by the CLI
4
+ * (decision 0009). One RPC endpoint; the CLI is a thin client. Serializes
5
+ * requests through a simple queue — policy code stays race-free.
6
+ */
7
+ import { createServer, request } from 'node:http';
8
+ import { existsSync, rmSync, writeFileSync, chmodSync } from 'node:fs';
9
+ import { socketPath, pidPath, stateRoot } from '../core/paths.js';
10
+ import { electSelf, releaseSelf } from './election.js';
11
+ import { BrokerError } from '../core/util.js';
12
+ import { BUILD, VERSION } from '../core/version.js';
13
+ import { JOURNAL_SCHEMA_VERSION } from '../core/journal.js';
14
+ import { Engine } from './engine.js';
15
+ import { logEvent } from '../core/events.js';
16
+ // Singleton guard: recover() and listen happen only AFTER we win the socket,
17
+ // so a losing daemon never reaps the winner's children or mutates the journal.
18
+ const engine = new Engine();
19
+ // Concurrency lives in the engine: a short pool lock for claim bookkeeping and
20
+ // one lock per environment. Requests for different environments overlap; two
21
+ // operations on one environment never do.
22
+ async function dispatch(verb, args, emit, signal) {
23
+ const cwd = String(args.cwd ?? process.cwd());
24
+ const holder = args.holder ? String(args.holder) : undefined;
25
+ const holderPid = args.holderPid !== undefined ? Number(args.holderPid) : undefined;
26
+ switch (verb) {
27
+ case 'ping':
28
+ // The version rides on ping because the CLI already pings on EVERY
29
+ // invocation (ensureDaemon), so skew detection costs no extra round trip.
30
+ return { pid: process.pid, version: VERSION, build: BUILD, journalSchema: JOURNAL_SCHEMA_VERSION };
31
+ case 'up':
32
+ return engine.up({
33
+ cwd, holder, holderPid, callerEnv: args.callerEnv, presets: args.presets,
34
+ hygiene: args.hygiene ?? undefined,
35
+ ttlMs: args.ttlMs ? Number(args.ttlMs) : undefined,
36
+ // [] (not undefined) so a bare `up` adds the default set — every
37
+ // service; undefined is reserved for the internal reset-data rebind,
38
+ // which adds nothing and never comes through this RPC.
39
+ services: Array.isArray(args.services) ? args.services.map(String) : [],
40
+ // Removed by decision 0034; refused by the engine if a client sends it.
41
+ dataOnly: Boolean(args.dataOnly),
42
+ rebuild: Boolean(args.rebuild),
43
+ onProgress: emit,
44
+ signal,
45
+ });
46
+ case 'down':
47
+ return engine.down({
48
+ cwd, holder,
49
+ services: Array.isArray(args.services) ? args.services.map(String) : [],
50
+ onProgress: emit,
51
+ });
52
+ case 'db-new':
53
+ return engine.dbNew({
54
+ cwd, holder, holderPid,
55
+ datastore: String(args.datastore ?? ''),
56
+ preset: args.preset === undefined ? undefined : String(args.preset),
57
+ onProgress: emit,
58
+ });
59
+ case 'db-drop':
60
+ return engine.dbDrop(String(args.name ?? ''));
61
+ case 'db-attach':
62
+ return engine.dbAttach(String(args.name ?? ''), {
63
+ pid: Number(args.pid),
64
+ start: args.start === undefined || args.start === null ? undefined : Number(args.start),
65
+ pgid: args.pgid === undefined || args.pgid === null ? undefined : Number(args.pgid),
66
+ });
67
+ case 'db-ls':
68
+ return engine.dbLs(cwd, Boolean(args.all));
69
+ case 'ps':
70
+ return engine.ps(cwd, Boolean(args.all));
71
+ case 'ctx':
72
+ return engine.ctx(cwd, holder);
73
+ case 'reset-data':
74
+ return engine.resetData(cwd, holder, emit, args.presets);
75
+ case 'exec-env':
76
+ return engine.execEnv(cwd, holder);
77
+ case 'exec-touch':
78
+ return engine.execTouch(String(args.envId ?? ''));
79
+ case 'logs':
80
+ return engine.logs(cwd, String(args.service), Number(args.lines ?? 40), holder);
81
+ case 'logs-spec':
82
+ return engine.logsSpec(cwd, Array.isArray(args.services) ? args.services.map(String) : [], Boolean(args.build), holder);
83
+ case 'plan':
84
+ return engine.plan(cwd, Array.isArray(args.services) ? args.services.map(String) : [], Boolean(args.rebuild), holder);
85
+ case 'destroy':
86
+ return engine.destroy(cwd);
87
+ case 'pool-doctor':
88
+ return engine.poolDoctor(cwd, Boolean(args.fix));
89
+ case 'token':
90
+ return engine.token(cwd, String(args.role ?? 'admin'), holder);
91
+ case 'warm':
92
+ return engine.warm(cwd, emit);
93
+ case 'release':
94
+ return engine.release(cwd, holder);
95
+ case 'preview':
96
+ return engine.previewStart(cwd, String(args.service), holder, args.ttlMs ? Number(args.ttlMs) : undefined, args.httpsPort !== undefined ? Number(args.httpsPort) : undefined);
97
+ case 'preview-stop':
98
+ return engine.previewStop(cwd, holder);
99
+ case 'appliance-ls':
100
+ return engine.applianceLs(cwd);
101
+ case 'appliance-start':
102
+ return engine.applianceStart(cwd, args.name ? String(args.name) : undefined);
103
+ case 'appliance-stop':
104
+ return engine.applianceStop(cwd, String(args.name));
105
+ case 'status':
106
+ return engine.status();
107
+ case 'doctor':
108
+ // The CLI's own version is passed in because only the CALLER knows it,
109
+ // and skew belongs in the diagnosis every RPC client reads — not just in
110
+ // the CLI that happens to have the comparison in front of it.
111
+ return engine.doctor(args.cliVersion === undefined ? undefined : String(args.cliVersion));
112
+ case 'update-plan':
113
+ return engine.updatePlan(args.cliVersion === undefined ? undefined : String(args.cliVersion));
114
+ case 'daemon-restart':
115
+ // The refusal lives in the ENGINE, not the CLI, so any
116
+ // other RPC caller cannot restart past an in-flight operation just by
117
+ // not implementing the check (the same reason the old --data-only
118
+ // contradictions were moved out of the CLI in 0.8.0).
119
+ engine.assertRestartable({
120
+ force: Boolean(args.force),
121
+ cliVersion: args.cliVersion === undefined ? undefined : String(args.cliVersion),
122
+ });
123
+ stopDaemon();
124
+ return { stopping: true, was: VERSION, holders: engine.leaseHolders() };
125
+ case 'pool-recycle':
126
+ return engine.poolRecycle({
127
+ envId: args.envId ? String(args.envId) : undefined,
128
+ force: Boolean(args.force ?? args.all),
129
+ });
130
+ case 'pool-reconcile':
131
+ // Local substrate: reconcile = doctor + reap anything degraded/stuck.
132
+ return engine.poolReconcile();
133
+ case 'pool-gc':
134
+ return engine.poolGc();
135
+ case 'shutdown':
136
+ stopDaemon();
137
+ return { stopping: true };
138
+ default:
139
+ throw new BrokerError('env-error', `daemon does not know verb '${verb}'`, 'rpc');
140
+ }
141
+ }
142
+ const sock = socketPath();
143
+ let ownsSocket = false;
144
+ let ownsLock = false;
145
+ /**
146
+ * Stop this daemon after answering the caller.
147
+ *
148
+ * The 50ms delay is what lets the result frame reach the client before the
149
+ * process goes: exiting inside the handler leaves the CLI reading a socket that
150
+ * closed without a result. `runly update` and `daemon stop` share this path
151
+ * on purpose — a restart is a stop plus the next verb's autospawn (decision
152
+ * 0009), so there is no second teardown implementation to keep in step.
153
+ */
154
+ function stopDaemon() {
155
+ stopSweeper();
156
+ setTimeout(async () => {
157
+ await engine.shutdown();
158
+ // Clean up what we own, exactly as the signal path does — a stale lock
159
+ // is recoverable (the next daemon sees a dead holder) but leaving one
160
+ // behind makes every restart pay a lock-break round.
161
+ if (ownsSocket)
162
+ rmSync(sock, { force: true });
163
+ if (ownsLock)
164
+ releaseSelf();
165
+ process.exit(0);
166
+ }, 50);
167
+ }
168
+ /** Is a LIVE daemon already answering on the socket? (vs. a stale socket file) */
169
+ function pingExisting() {
170
+ return new Promise((resolve) => {
171
+ if (!existsSync(sock))
172
+ return resolve(false);
173
+ const req = request({ socketPath: sock, path: '/', method: 'POST', timeout: 1500 }, (res) => {
174
+ res.resume();
175
+ resolve(res.statusCode === 200);
176
+ });
177
+ req.on('error', () => resolve(false)); // ECONNREFUSED = stale socket
178
+ req.on('timeout', () => {
179
+ req.destroy();
180
+ resolve(false);
181
+ });
182
+ req.end(JSON.stringify({ verb: 'ping', args: {} }));
183
+ });
184
+ }
185
+ /**
186
+ * Recovery reconciles prior daemon state, and now genuinely waits on kills
187
+ * (verified reaps, orphan sweep) instead of firing signals blind. Requests are
188
+ * therefore held until it completes: serving `status` mid-reconcile would show
189
+ * a caller envs that are about to change state, which is the old synchronous
190
+ * contract silently broken.
191
+ *
192
+ * `ping` is exempt — the singleton election must be answerable immediately, or
193
+ * a second daemon would conclude the socket is stale and take over.
194
+ */
195
+ let recovered = Promise.resolve();
196
+ /** The sweeper's timer; cleared the moment a shutdown begins. */
197
+ let sweepTimer;
198
+ function stopSweeper() {
199
+ if (sweepTimer)
200
+ clearInterval(sweepTimer);
201
+ sweepTimer = undefined;
202
+ }
203
+ const server = createServer((req, res) => {
204
+ let body = '';
205
+ req.on('data', (d) => (body += d));
206
+ // A client that hangs up (Ctrl-C, a killed agent) aborts what it asked for
207
+ // where that is still possible: a queued `up` leaves the queue instead of
208
+ // binding later for nobody.
209
+ const aborter = new AbortController();
210
+ res.on('close', () => {
211
+ if (!res.writableFinished)
212
+ aborter.abort();
213
+ });
214
+ req.on('end', () => {
215
+ void (async () => {
216
+ // Response is newline-delimited JSON: zero or more {type:'progress'}
217
+ // frames, then exactly one {type:'result'} frame. The CLI renders
218
+ // progress to stderr (TTY only) and the result to stdout.
219
+ res.writeHead(200, { 'content-type': 'application/x-ndjson' });
220
+ const emit = (phase) => {
221
+ try {
222
+ res.write(JSON.stringify({ type: 'progress', phase }) + '\n');
223
+ }
224
+ catch {
225
+ /* client hung up mid-stream */
226
+ }
227
+ };
228
+ let verbForLog = '?';
229
+ try {
230
+ const { verb, args } = JSON.parse(body || '{}');
231
+ verbForLog = String(verb ?? '?');
232
+ if (verb !== 'ping')
233
+ await recovered;
234
+ const data = await dispatch(verb, args ?? {}, emit, aborter.signal);
235
+ res.end(JSON.stringify({ type: 'result', ok: true, data }) + '\n');
236
+ }
237
+ catch (err) {
238
+ // An unclassified throw is a DAEMON bug, not a bad environment.
239
+ // Labelling it env-error told the agent to recycle an environment,
240
+ // which cannot fix a TypeError in the broker — and buried the defect.
241
+ const e = err instanceof BrokerError
242
+ ? err.toJSON()
243
+ : { class: 'infra-error', message: `internal daemon error: ${String(err.message ?? err)}` };
244
+ if (!(err instanceof BrokerError)) {
245
+ logEvent({ level: 'error', kind: 'internal', detail: `${verbForLog}: ${String(err.stack ?? err)}`.slice(0, 1000) });
246
+ }
247
+ res.end(JSON.stringify({ type: 'result', ok: false, error: e }) + '\n');
248
+ }
249
+ })();
250
+ });
251
+ });
252
+ // EADDRINUSE means another daemon won the listen race between our ping and our
253
+ // bind — concede cleanly rather than clobber it.
254
+ server.on('error', (err) => {
255
+ if (err.code === 'EADDRINUSE') {
256
+ if (process.send)
257
+ process.send('ready'); // a live daemon exists; the client can proceed
258
+ process.exit(0);
259
+ }
260
+ throw err;
261
+ });
262
+ async function start() {
263
+ if (await pingExisting()) {
264
+ // A healthy daemon already owns the socket — do NOT recover() or listen.
265
+ if (process.send)
266
+ process.send('ready');
267
+ process.exit(0);
268
+ }
269
+ // Win the lock BEFORE touching the socket. Without this, two daemons that
270
+ // both saw no answer to their ping would each unlink the socket — the second
271
+ // deleting the first's LIVE one — and both would then bind successfully,
272
+ // sweeping the same journal and reaping each other's processes.
273
+ if (!electSelf()) {
274
+ if (process.send)
275
+ process.send('ready'); // someone else owns it; the client can proceed
276
+ process.exit(0);
277
+ }
278
+ ownsLock = true;
279
+ // The daemon inherited its cwd from whichever CLI spawned it — usually a
280
+ // WORKTREE, which the consumer may delete while the daemon lives on. A
281
+ // deleted cwd makes anything that reads it (worker_threads spawn calls
282
+ // uv_cwd) fail with ENOENT. Pin cwd to the state root, which the daemon
283
+ // owns for exactly its own lifetime; nothing daemon-side resolves against
284
+ // cwd (every RPC carries the client's cwd explicitly).
285
+ process.chdir(stateRoot());
286
+ // Only the election winner may remove a socket, so this can no longer be a
287
+ // live one: any daemon that could have been listening lost the lock.
288
+ if (existsSync(sock))
289
+ rmSync(sock, { force: true }); // stale socket from a dead daemon
290
+ // Create the socket ALREADY private. The chmod below runs inside the listen
291
+ // callback, by which point the socket exists and can accept connections, so
292
+ // a permissive umask left a window where it was world-reachable. The socket
293
+ // has no RPC auth and exposes arbitrary-shell verbs, so the window matters.
294
+ const callerUmask = process.umask(0o077);
295
+ server.listen(sock, () => {
296
+ // …and only the socket. Everything the daemon spawns — services, builds,
297
+ // upkeep, exec — inherited 077 and wrote owner-only files into the
298
+ // caller's worktree (a build output nobody else could read). The state
299
+ // root is 0700 itself, so restoring the caller's umask keeps it private.
300
+ process.umask(callerUmask);
301
+ ownsSocket = true;
302
+ // The socket has no RPC auth and exposes arbitrary-shell verbs (exec/token),
303
+ // so it must be owner-only. On macOS the socket-file mode is what actually
304
+ // gates connect() (the dir traversal isn't enough), so this chmod is the
305
+ // real boundary, not just belt-and-suspenders.
306
+ try {
307
+ chmodSync(sock, 0o600);
308
+ }
309
+ catch {
310
+ /* best-effort; the 0700 state dir is the backstop */
311
+ }
312
+ writeFileSync(pidPath(), String(process.pid));
313
+ // Only NOW, as the sole owner, reconcile prior daemon state. Recovery
314
+ // verifies each reap and sweeps for orphans, so it awaits real work; every
315
+ // non-ping request queues behind it.
316
+ recovered = engine.recover().catch((err) => {
317
+ // A failed reconcile must not wedge the daemon forever — surface it and
318
+ // let requests through rather than hanging every client.
319
+ logEvent({ level: 'error', kind: 'recover', detail: `recovery failed: ${String(err.message ?? err)}` });
320
+ });
321
+ const sweepMs = Number(process.env.BACKLOT_SWEEP_MS ?? 15_000);
322
+ // The sweeper must not run DURING recovery: it would act on rows recovery
323
+ // is still reconciling, and a stale-snapshot save could resurrect an env
324
+ // recovery had just deleted. Each tick also swallows its own rejection —
325
+ // a fire-and-forget async call whose promise rejects is an unhandled
326
+ // rejection, which takes the daemon down on any transient journal or FS
327
+ // write failure.
328
+ void recovered.then(() => {
329
+ if (engine.stopping)
330
+ return;
331
+ sweepTimer = setInterval(() => {
332
+ void engine.sweep().catch((err) => {
333
+ logEvent({ level: 'error', kind: 'sweep', detail: `sweep failed: ${String(err.message ?? err)}` });
334
+ });
335
+ }, sweepMs);
336
+ sweepTimer.unref();
337
+ }).catch((err) => logEvent({ level: 'error', kind: 'recover', detail: `sweeper never armed: ${String(err.message ?? err)}` }));
338
+ // Detach from the spawning CLI's lifetime.
339
+ if (process.send)
340
+ process.send('ready');
341
+ });
342
+ }
343
+ void start().catch((err) => {
344
+ // Nothing has been established yet, so there is no graceful path — but dying
345
+ // silently left the client waiting on a daemon that would never answer.
346
+ console.error(`runly daemon failed to start: ${String(err.stack ?? err)}`);
347
+ process.exit(1);
348
+ });
349
+ /**
350
+ * Runtime-level guards.
351
+ *
352
+ * Node's default for an unhandled rejection or an uncaught exception is to kill
353
+ * the process — which for this daemon means every environment loses its
354
+ * supervisor over one missed `.catch`. Four confirmed defects in the 2026-07-18
355
+ * review were exactly that shape (a socket 'error', an fs.watch 'error', a
356
+ * spawn 'error' with no 'exit', a fire-and-forget sweep).
357
+ *
358
+ * The two are handled ASYMMETRICALLY on purpose:
359
+ *
360
+ * - An unhandled REJECTION is almost always a missing `.catch` on an otherwise
361
+ * contained operation. Logging and staying alive is right: the daemon keeps
362
+ * supervising, and the event log names the gap.
363
+ * - An uncaught EXCEPTION leaves the process in an undefined state, and this
364
+ * daemon owns a journal that is the machine's source of truth. Continuing
365
+ * risks corrupting it. Exiting is safe BECAUSE of the crash-recovery contract
366
+ * (decision 0009): services survive, and the next daemon reconciles them. So
367
+ * log loudly, then die deliberately rather than limp.
368
+ */
369
+ process.on('unhandledRejection', (reason) => {
370
+ logEvent({
371
+ level: 'error',
372
+ kind: 'internal',
373
+ detail: `unhandled rejection (daemon continuing): ${String(reason?.stack ?? reason)}`.slice(0, 2000),
374
+ });
375
+ });
376
+ process.on('uncaughtException', (err) => {
377
+ logEvent({
378
+ level: 'error',
379
+ kind: 'internal',
380
+ detail: `uncaught exception — exiting so a clean daemon can reconcile: ${String(err?.stack ?? err)}`.slice(0, 2000),
381
+ });
382
+ try {
383
+ if (ownsSocket)
384
+ rmSync(sock, { force: true });
385
+ if (ownsLock)
386
+ releaseSelf();
387
+ }
388
+ catch {
389
+ /* best-effort */
390
+ }
391
+ process.exit(1);
392
+ });
393
+ for (const sig of ['SIGINT', 'SIGTERM']) {
394
+ process.on(sig, () => {
395
+ // A daemon that LOST the election must exit without touching shared state:
396
+ // engine.shutdown() stops services and rewrites env rows, which would let a
397
+ // conceding process tear down the winner's environments.
398
+ if (!ownsLock && !ownsSocket)
399
+ process.exit(0);
400
+ stopSweeper();
401
+ void engine.shutdown().then(() => {
402
+ // Only remove the socket if WE own it — a losing/duplicate daemon must
403
+ // never delete the healthy daemon's socket.
404
+ if (ownsSocket)
405
+ rmSync(sock, { force: true });
406
+ if (ownsLock)
407
+ releaseSelf();
408
+ process.exit(0);
409
+ });
410
+ });
411
+ }
412
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/daemon/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,108 @@
1
+ export type TargetState = 'down' | 'starting' | 'up';
2
+ export interface ProxyStats {
3
+ /** The public port (what ctx reports). */
4
+ port: number;
5
+ /** The service currently behind it, when known. */
6
+ service?: string;
7
+ state: TargetState;
8
+ /** Where the service listens right now (diagnostics only — never hand it out). */
9
+ internalPort?: number;
10
+ /** Client→server bytes since the daemon started proxying this port. */
11
+ clientBytes: number;
12
+ /** Epoch ms of the last client→server byte; null when none arrived yet. */
13
+ lastActivityAt: number | null;
14
+ /** Connections currently piped. */
15
+ open: number;
16
+ /** Connections currently held while the service starts. */
17
+ held: number;
18
+ /** Connections accepted in total. */
19
+ accepted: number;
20
+ }
21
+ /**
22
+ * Called when a connection arrives for a target that is DOWN. Returning true
23
+ * means "a start has been requested" — the connection is held (bounded by
24
+ * BACKLOT_PROXY_HOLD_MS) until the engine calls `up` (forwarded) or `down`
25
+ * (closed). Returning false closes it at once, which is what a client saw
26
+ * before the proxy existed: nothing listening.
27
+ */
28
+ export type WakeHook = (envId: string, key: string) => boolean;
29
+ /** How long a connection may wait for its service to become ready. */
30
+ export declare const HOLD_MS: () => number;
31
+ /** What every connection carries for a replay right now (tests, `status`). */
32
+ export declare const replayCarriedBytes: () => number;
33
+ /**
34
+ * May bytes a process already received be sent to the next one? Only when the
35
+ * connection's first bytes are an HTTP request with a method that is safe to
36
+ * repeat: the dead process may have acted on what it read (charged a card,
37
+ * inserted a row) before it died, and a replay would act a second time. Any
38
+ * other request — POST, PUT, PATCH, DELETE, a database or TLS wire protocol,
39
+ * a first chunk too short to tell — is not replayed once delivered.
40
+ */
41
+ export declare function replayable(chunks: readonly Buffer[]): boolean;
42
+ export declare class PortInUse extends Error {
43
+ readonly port: number;
44
+ readonly host: string;
45
+ constructor(port: number, host: string);
46
+ }
47
+ export declare class ProxyHub {
48
+ private targets;
49
+ /**
50
+ * Internal ports handed to a start that is not `up` yet, by env/key. A port
51
+ * is probed free when it is allocated, and the service binds it seconds
52
+ * later; without this, two starts in that window were given the same port
53
+ * and one environment's public URL served the other's process.
54
+ */
55
+ private reservedInternal;
56
+ private wakeHook?;
57
+ private activityHook?;
58
+ private id;
59
+ /** The engine installs this (decision 0035): start a stopped service on its first connection. */
60
+ setWakeHook(hook: WakeHook | undefined): void;
61
+ /** Optional observer of client bytes (idle bookkeeping lives with the caller). */
62
+ setActivityHook(hook: ((envId: string, key: string) => void) | undefined): void;
63
+ wake(envId: string, key: string): boolean;
64
+ noteActivity(envId: string, key: string): void;
65
+ /** Where the service behind env/key listens, also while it restarts (decision 0035). */
66
+ internalPortOf(envId: string, key: string): number | undefined;
67
+ /** Start a target's activity clock from the journal, so a daemon restart keeps it (decision 0035). */
68
+ seedActivity(envId: string, key: string, at: number): void;
69
+ /** The last client byte per port key of an environment (null = none yet). */
70
+ activity(envId: string): Record<string, number | null>;
71
+ /** Every env id with a listener (doctor's stale-listener check, decision 0037). */
72
+ envIds(): Set<string>;
73
+ /** Is the public listener for this env/key currently held, on this port? */
74
+ holds(envId: string, key: string, port: number): boolean;
75
+ /** Every public port this daemon is listening on. */
76
+ heldPorts(): Set<number>;
77
+ /** Every internal port currently assigned to a target (up or not) or reserved for a start, so none is handed out twice. */
78
+ assignedInternalPorts(): Set<number>;
79
+ /** Hold `port` for the start of env/key until it is `up` (then the target owns it) or goes down. */
80
+ reserveInternal(envId: string, key: string, port: number): void;
81
+ private release;
82
+ /**
83
+ * Listen on `port` for env/key (idempotent). Throws PortInUse when another
84
+ * process holds it; any previous listener for this env/key on a different
85
+ * port is closed first, and its counters carried over.
86
+ */
87
+ listen(envId: string, key: string, port: number): Promise<void>;
88
+ /** New connections are held until `up` (or dropped by `down`). */
89
+ starting(envId: string, key: string, service?: string): void;
90
+ /** The service behind env/key is ready on `internalPort`; held connections go through. */
91
+ up(envId: string, key: string, internalPort: number, service?: string): void;
92
+ /** Nothing is (or will soon be) behind env/key: held connections are closed. */
93
+ down(envId: string, key: string): void;
94
+ /** A service was stopped: its targets go down unless a restart already marked them starting. */
95
+ serviceStopped(envId: string, service: string): void;
96
+ /** Every `starting` target of an env that never reached `up` goes down (a failed bind). */
97
+ settle(envId: string): void;
98
+ state(envId: string, key: string): TargetState | undefined;
99
+ /** Close one env/key listener (a port key the manifest dropped). */
100
+ closeKey(envId: string, key: string): void;
101
+ /** The port keys this daemon holds a listener for, for one environment. */
102
+ keysOf(envId: string): string[];
103
+ /** Release every listener of an environment (teardown). */
104
+ closeEnv(envId: string): void;
105
+ closeAll(): void;
106
+ /** Stats by port key for one environment. */
107
+ stats(envId: string): Record<string, ProxyStats>;
108
+ }