botmux 3.18.3 → 3.18.5

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 (101) hide show
  1. package/dist/.runtime-build-id +1 -1
  2. package/dist/adapters/hook-installer.d.ts.map +1 -1
  3. package/dist/adapters/hook-installer.js +75 -2
  4. package/dist/adapters/hook-installer.js.map +1 -1
  5. package/dist/cli/current-actor.d.ts.map +1 -1
  6. package/dist/cli/current-actor.js +2 -1
  7. package/dist/cli/current-actor.js.map +1 -1
  8. package/dist/cli/dashboard-endpoint.d.ts.map +1 -1
  9. package/dist/cli/dashboard-endpoint.js +5 -2
  10. package/dist/cli/dashboard-endpoint.js.map +1 -1
  11. package/dist/cli/exact-chat-grant-client.d.ts.map +1 -1
  12. package/dist/cli/exact-chat-grant-client.js +2 -1
  13. package/dist/cli/exact-chat-grant-client.js.map +1 -1
  14. package/dist/cli/vc-agent.d.ts.map +1 -1
  15. package/dist/cli/vc-agent.js +2 -1
  16. package/dist/cli/vc-agent.js.map +1 -1
  17. package/dist/cli.d.ts.map +1 -1
  18. package/dist/cli.js +105 -11
  19. package/dist/cli.js.map +1 -1
  20. package/dist/core/binary-install-shape.d.ts +77 -0
  21. package/dist/core/binary-install-shape.d.ts.map +1 -0
  22. package/dist/core/binary-install-shape.js +117 -0
  23. package/dist/core/binary-install-shape.js.map +1 -0
  24. package/dist/core/binary-self-update.d.ts +82 -0
  25. package/dist/core/binary-self-update.d.ts.map +1 -0
  26. package/dist/core/binary-self-update.js +239 -0
  27. package/dist/core/binary-self-update.js.map +1 -0
  28. package/dist/core/daemon-ipc-auth.d.ts +9 -1
  29. package/dist/core/daemon-ipc-auth.d.ts.map +1 -1
  30. package/dist/core/daemon-ipc-auth.js +14 -3
  31. package/dist/core/daemon-ipc-auth.js.map +1 -1
  32. package/dist/core/loopback-fetch.d.ts +59 -0
  33. package/dist/core/loopback-fetch.d.ts.map +1 -0
  34. package/dist/core/loopback-fetch.js +347 -0
  35. package/dist/core/loopback-fetch.js.map +1 -0
  36. package/dist/core/maintenance.d.ts +88 -1
  37. package/dist/core/maintenance.d.ts.map +1 -1
  38. package/dist/core/maintenance.js +149 -8
  39. package/dist/core/maintenance.js.map +1 -1
  40. package/dist/core/managed-origin-attestation.d.ts.map +1 -1
  41. package/dist/core/managed-origin-attestation.js +2 -1
  42. package/dist/core/managed-origin-attestation.js.map +1 -1
  43. package/dist/core/release-download.d.ts +13 -0
  44. package/dist/core/release-download.d.ts.map +1 -0
  45. package/dist/core/release-download.js +130 -0
  46. package/dist/core/release-download.js.map +1 -0
  47. package/dist/daemon.d.ts.map +1 -1
  48. package/dist/daemon.js +2 -1
  49. package/dist/daemon.js.map +1 -1
  50. package/dist/dashboard/aggregator.d.ts.map +1 -1
  51. package/dist/dashboard/aggregator.js +2 -1
  52. package/dist/dashboard/aggregator.js.map +1 -1
  53. package/dist/dashboard/daemon-internal-client.d.ts.map +1 -1
  54. package/dist/dashboard/daemon-internal-client.js +6 -1
  55. package/dist/dashboard/daemon-internal-client.js.map +1 -1
  56. package/dist/dashboard/groups-action-helpers.d.ts +0 -10
  57. package/dist/dashboard/groups-action-helpers.d.ts.map +1 -1
  58. package/dist/dashboard/groups-action-helpers.js +3 -2
  59. package/dist/dashboard/groups-action-helpers.js.map +1 -1
  60. package/dist/dashboard/hd2d-assets.d.ts +2 -3
  61. package/dist/dashboard/hd2d-assets.d.ts.map +1 -1
  62. package/dist/dashboard/hd2d-assets.js +7 -109
  63. package/dist/dashboard/hd2d-assets.js.map +1 -1
  64. package/dist/dashboard/web/app.d.ts.map +1 -1
  65. package/dist/dashboard/web/app.js +7 -1
  66. package/dist/dashboard/web/app.js.map +1 -1
  67. package/dist/dashboard/web/settings-page.d.ts.map +1 -1
  68. package/dist/dashboard/web/settings-page.js.map +1 -1
  69. package/dist/dashboard-web/app.js +1 -1
  70. package/dist/dashboard.js +122 -14
  71. package/dist/dashboard.js.map +1 -1
  72. package/dist/services/hook-runner.d.ts.map +1 -1
  73. package/dist/services/hook-runner.js +2 -1
  74. package/dist/services/hook-runner.js.map +1 -1
  75. package/dist/services/voice/openai.d.ts.map +1 -1
  76. package/dist/services/voice/openai.js +10 -1
  77. package/dist/services/voice/openai.js.map +1 -1
  78. package/dist/services/worktree-slug-ai.d.ts.map +1 -1
  79. package/dist/services/worktree-slug-ai.js +8 -1
  80. package/dist/services/worktree-slug-ai.js.map +1 -1
  81. package/dist/utils/file-lock.d.ts +25 -0
  82. package/dist/utils/file-lock.d.ts.map +1 -1
  83. package/dist/utils/file-lock.js +34 -4
  84. package/dist/utils/file-lock.js.map +1 -1
  85. package/dist/utils/global-install.d.ts +19 -0
  86. package/dist/utils/global-install.d.ts.map +1 -1
  87. package/dist/utils/global-install.js +28 -1
  88. package/dist/utils/global-install.js.map +1 -1
  89. package/dist/utils/install-info.d.ts +37 -1
  90. package/dist/utils/install-info.d.ts.map +1 -1
  91. package/dist/utils/install-info.js +45 -3
  92. package/dist/utils/install-info.js.map +1 -1
  93. package/dist/workflows/v3/daemon-ipc-client.d.ts.map +1 -1
  94. package/dist/workflows/v3/daemon-ipc-client.js +2 -1
  95. package/dist/workflows/v3/daemon-ipc-client.js.map +1 -1
  96. package/dist/workflows/v3/session-relay-client.d.ts.map +1 -1
  97. package/dist/workflows/v3/session-relay-client.js +2 -1
  98. package/dist/workflows/v3/session-relay-client.js.map +1 -1
  99. package/package.json +8 -7
  100. package/scripts/install-path-entry.mjs +263 -0
  101. package/scripts/postinstall-bin.mjs +41 -7
@@ -0,0 +1,59 @@
1
+ /**
2
+ * `fetch`-shaped client for **loopback-only** HTTP, built on `node:http`.
3
+ *
4
+ * ⚠️ WHY THIS EXISTS — DO NOT "SIMPLIFY" ANY CALLER BACK TO THE GLOBAL `fetch`.
5
+ *
6
+ * Bun's `fetch` automatically routes through `$http_proxy`, and its `no_proxy`
7
+ * handling is **literal-match only**: it does not parse CIDR notation and does
8
+ * not treat `localhost` as covering `127.0.0.1`. Corporate dev machines commonly
9
+ * export exactly that shape (`http_proxy=…` plus `no_proxy=…,127.0.0.0/8,…`), so
10
+ * a request to our OWN loopback port gets handed to the corporate proxy, which
11
+ * refuses to forward an internal address and answers with an nginx HTML
12
+ * `403 Forbidden`. The user sees a failure naming a service that is running
13
+ * perfectly well two ports away.
14
+ *
15
+ * Measured on Bun 1.4.0 (source AND `bun build --compile`), one fresh process per
16
+ * row because Bun snapshots proxy configuration at startup:
17
+ *
18
+ * no_proxy unset / "" → proxied
19
+ * no_proxy=127.0.0.0/8 → proxied ← the common corporate shape
20
+ * no_proxy=localhost → proxied ← does NOT cover 127.0.0.1
21
+ * no_proxy=127.0.0.1 → direct
22
+ * no_proxy=* → direct
23
+ *
24
+ * And the obvious escapes do not work:
25
+ *
26
+ * fetch(…, {proxy: ''|undefined|null}) → still proxied (option exists ≠ works)
27
+ * delete process.env.http_proxy at run → still proxied (startup snapshot)
28
+ * setting process.env.http_proxy later → still direct (same reason)
29
+ *
30
+ * `node:http` ignores proxy environment variables entirely, in both Node and Bun,
31
+ * which is the only reliable way to guarantee a loopback dial stays loopback.
32
+ * Related precedent: `platform/platform-http.ts` (deliberately avoids undici) and
33
+ * `dashboard/hd2d-assets.ts` (the reverse case — it WANTS the proxy).
34
+ */
35
+ /** The subset of RequestInit our loopback callers use. */
36
+ export interface LoopbackFetchInit {
37
+ method?: string;
38
+ headers?: HeadersInit;
39
+ /** Accepts what `fetch` accepts; streams are the one thing we cannot forward. */
40
+ body?: BodyInit | null;
41
+ signal?: AbortSignal | null;
42
+ }
43
+ /** Install (or clear, with `undefined`) the test transport. Returns the previous one. */
44
+ export declare function __setLoopbackTransportForTests(next: ((url: string, init: LoopbackFetchInit) => Promise<Response>) | undefined): ((url: string, init: LoopbackFetchInit) => Promise<Response>) | undefined;
45
+ export declare function loopbackFetch(url: string, init?: LoopbackFetchInit): Promise<Response>;
46
+ /** `typeof fetch`-compatible view, for defaulting an injectable `fetchImpl`. */
47
+ export declare const loopbackFetchImpl: typeof fetch;
48
+ /**
49
+ * Is this URL a literal loopback target, i.e. safe (and necessary) to send through
50
+ * {@link loopbackFetch}? For call sites whose base URL is configurable and may
51
+ * legitimately point at a remote host — those must keep using the global fetch,
52
+ * which handles TLS, redirects and real proxying.
53
+ *
54
+ * Literal only, deliberately: a DNS name that currently resolves to 127.0.0.1
55
+ * is not a loopback guarantee (it can be re-pointed), and `loopbackFetch`'s own
56
+ * guard would reject it anyway.
57
+ */
58
+ export declare function isLoopbackUrl(url: string): boolean;
59
+ //# sourceMappingURL=loopback-fetch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loopback-fetch.d.ts","sourceRoot":"","sources":["../../src/core/loopback-fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAKH,0DAA0D;AAC1D,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,iFAAiF;IACjF,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;IACvB,MAAM,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;CAC7B;AA4FD,yFAAyF;AACzF,wBAAgB,8BAA8B,CAC5C,IAAI,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC,GAAG,SAAS,GAC9E,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC,GAAG,SAAS,CAI3E;AAED,wBAAgB,aAAa,CAC3B,GAAG,EAAE,MAAM,EACX,IAAI,GAAE,iBAAsB,GAC3B,OAAO,CAAC,QAAQ,CAAC,CAyJnB;AAED,gFAAgF;AAChF,eAAO,MAAM,iBAAiB,EAA+B,OAAO,KAAK,CAAC;AAE1E;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAWlD"}
@@ -0,0 +1,347 @@
1
+ /**
2
+ * `fetch`-shaped client for **loopback-only** HTTP, built on `node:http`.
3
+ *
4
+ * ⚠️ WHY THIS EXISTS — DO NOT "SIMPLIFY" ANY CALLER BACK TO THE GLOBAL `fetch`.
5
+ *
6
+ * Bun's `fetch` automatically routes through `$http_proxy`, and its `no_proxy`
7
+ * handling is **literal-match only**: it does not parse CIDR notation and does
8
+ * not treat `localhost` as covering `127.0.0.1`. Corporate dev machines commonly
9
+ * export exactly that shape (`http_proxy=…` plus `no_proxy=…,127.0.0.0/8,…`), so
10
+ * a request to our OWN loopback port gets handed to the corporate proxy, which
11
+ * refuses to forward an internal address and answers with an nginx HTML
12
+ * `403 Forbidden`. The user sees a failure naming a service that is running
13
+ * perfectly well two ports away.
14
+ *
15
+ * Measured on Bun 1.4.0 (source AND `bun build --compile`), one fresh process per
16
+ * row because Bun snapshots proxy configuration at startup:
17
+ *
18
+ * no_proxy unset / "" → proxied
19
+ * no_proxy=127.0.0.0/8 → proxied ← the common corporate shape
20
+ * no_proxy=localhost → proxied ← does NOT cover 127.0.0.1
21
+ * no_proxy=127.0.0.1 → direct
22
+ * no_proxy=* → direct
23
+ *
24
+ * And the obvious escapes do not work:
25
+ *
26
+ * fetch(…, {proxy: ''|undefined|null}) → still proxied (option exists ≠ works)
27
+ * delete process.env.http_proxy at run → still proxied (startup snapshot)
28
+ * setting process.env.http_proxy later → still direct (same reason)
29
+ *
30
+ * `node:http` ignores proxy environment variables entirely, in both Node and Bun,
31
+ * which is the only reliable way to guarantee a loopback dial stays loopback.
32
+ * Related precedent: `platform/platform-http.ts` (deliberately avoids undici) and
33
+ * `dashboard/hd2d-assets.ts` (the reverse case — it WANTS the proxy).
34
+ */
35
+ import { Buffer } from 'node:buffer';
36
+ import { requestLiteralLoopback } from './loopback-target.js';
37
+ /** Normalise a fetch-style body to bytes. Streams are rejected loudly rather
38
+ * than silently sent as "[object ReadableStream]". */
39
+ function bodyToBuffer(body) {
40
+ if (body == null)
41
+ return undefined;
42
+ if (typeof body === 'string')
43
+ return Buffer.from(body);
44
+ if (Buffer.isBuffer(body))
45
+ return body;
46
+ if (body instanceof URLSearchParams)
47
+ return Buffer.from(body.toString());
48
+ if (body instanceof ArrayBuffer)
49
+ return Buffer.from(new Uint8Array(body));
50
+ if (ArrayBuffer.isView(body)) {
51
+ return Buffer.from(body.buffer, body.byteOffset, body.byteLength);
52
+ }
53
+ throw new TypeError('loopbackFetch: unsupported body type (streams/FormData are not forwarded)');
54
+ }
55
+ function headerEntries(headers) {
56
+ if (!headers)
57
+ return {};
58
+ // `Headers` (and anything iterable of pairs) normalises casing for us; a plain
59
+ // object is passed through as written.
60
+ const out = {};
61
+ if (headers instanceof Headers) {
62
+ headers.forEach((value, key) => { out[key] = value; });
63
+ return out;
64
+ }
65
+ if (Array.isArray(headers)) {
66
+ for (const [k, v] of headers)
67
+ out[k] = String(v);
68
+ return out;
69
+ }
70
+ for (const [k, v] of Object.entries(headers))
71
+ out[k] = String(v);
72
+ return out;
73
+ }
74
+ /**
75
+ * The error an aborted request must fail with.
76
+ *
77
+ * ⚠️ Callers branch on `err.name === 'AbortError'` to tell "timed out" from
78
+ * "nothing listening" — e.g. daemon.ts returns 504 `wait_timeout` vs 503
79
+ * `daemon_offline`, and command-handler.ts reports `busy` vs `failed`. A plain
80
+ * `new Error('aborted')` (or node:http's own ECONNRESET) silently sends every
81
+ * timeout down the wrong branch. Measured against the global fetch these replace:
82
+ * it rejects with `name=AbortError`, while the first version of this module gave
83
+ * `name=Error, code=ECONNRESET`.
84
+ *
85
+ * `signal.reason` is what the platform puts there (a DOMException named
86
+ * AbortError, or whatever the caller passed to `abort(reason)`); we only
87
+ * synthesise an equivalent when it is somehow absent.
88
+ */
89
+ function abortError(signal) {
90
+ const reason = signal?.reason;
91
+ if (reason !== undefined && reason !== null)
92
+ return reason;
93
+ // Node and browsers both expose DOMException; fall back to a shaped Error.
94
+ if (typeof DOMException === 'function') {
95
+ return new DOMException('The operation was aborted.', 'AbortError');
96
+ }
97
+ const err = new Error('The operation was aborted.');
98
+ err.name = 'AbortError';
99
+ return err;
100
+ }
101
+ /**
102
+ * Issue one request to a literal loopback host and resolve a real `Response`, so
103
+ * call sites keep using `.ok` / `.status` / `.json()` / `.text()` unchanged.
104
+ *
105
+ * ⚠️ Resolves as soon as the response HEADERS arrive, with the body exposed as a
106
+ * `ReadableStream` — it must NOT buffer to completion first. `dashboard/aggregator.ts`
107
+ * subscribes to a never-ending SSE stream (`/api/events`) and reads frames off
108
+ * `res.body`; a buffer-all implementation never resolves for those callers and the
109
+ * whole session/event aggregation silently stops. Measured: a server that writes
110
+ * one frame and holds the connection open produced no Response at all until this
111
+ * was made streaming. `.text()`/`.json()` still work, because `Response` drains
112
+ * the stream for them.
113
+ *
114
+ * Rejects the way `fetch` does (a transport failure) rather than resolving a
115
+ * synthetic error status, because callers distinguish "no server there" from
116
+ * "server said no".
117
+ */
118
+ /**
119
+ * Test-only transport override.
120
+ *
121
+ * ⚠️ Why this seam exists: several suites drive loopback code paths by replacing
122
+ * `globalThis.fetch`. Moving those call sites onto node:http silently bypassed
123
+ * that stub, so the tests began making REAL connections to ports like 4310/39003
124
+ * — `fetchMock` recorded 0 calls and the assertions failed for a reason unrelated
125
+ * to what they test. Rather than push every caller back onto the global fetch (the
126
+ * whole defect), the transport itself is injectable: production keeps node:http,
127
+ * tests install a mock here.
128
+ *
129
+ * Not for production use — `__setLoopbackTransportForTests(undefined)` restores the default.
130
+ */
131
+ let transportOverride;
132
+ /** Install (or clear, with `undefined`) the test transport. Returns the previous one. */
133
+ export function __setLoopbackTransportForTests(next) {
134
+ const previous = transportOverride;
135
+ transportOverride = next;
136
+ return previous;
137
+ }
138
+ export function loopbackFetch(url, init = {}) {
139
+ if (transportOverride)
140
+ return transportOverride(url, init);
141
+ const target = new URL(url);
142
+ // ⚠️ FAIL CLOSED on the scheme. This transport IS node:http, so an `https:` URL
143
+ // would be dialled as PLAINTEXT — measured: `loopbackFetch('https://127.0.0.1:P')`
144
+ // against a plain HTTP server returned `200 PLAIN`, i.e. a caller that believed it
145
+ // was using TLS would have sent its credentials in the clear. `isLoopbackUrl`
146
+ // already refuses https, but a public helper must not depend on callers routing
147
+ // through a predicate first.
148
+ if (target.protocol !== 'http:') {
149
+ return Promise.reject(new TypeError(`loopbackFetch: only http: is supported (got ${target.protocol})`));
150
+ }
151
+ // `URL.hostname` keeps IPv6 in brackets (`[::1]`), but node:http — and
152
+ // requestLiteralLoopback's allow-list — want the bare address. Passing the
153
+ // bracketed form through was rejected as `remote_host_forbidden`, i.e. a legal
154
+ // IPv6 loopback URL could never be dialled (measured against a `::1` server).
155
+ const host = normaliseLoopbackHost(target.hostname);
156
+ // `URL` normalises away a scheme's DEFAULT port, so both `http://127.0.0.1/x` and
157
+ // an explicit `http://127.0.0.1:80/x` yield `port === ''` — `Number('')` is 0 and
158
+ // was rejected as `invalid_port` (measured against a local server on :80, which the
159
+ // native fetch reaches fine). Fill in the scheme default.
160
+ const port = target.port ? Number(target.port) : 80;
161
+ const method = (init.method ?? 'GET').toUpperCase();
162
+ const headers = headerEntries(init.headers);
163
+ const body = bodyToBuffer(init.body);
164
+ // A body without a length makes some servers wait for more; be explicit.
165
+ if (body && headers['content-length'] === undefined && headers['Content-Length'] === undefined) {
166
+ headers['content-length'] = String(body.byteLength);
167
+ }
168
+ return new Promise((resolve, reject) => {
169
+ let settled = false;
170
+ const finish = (fn) => { if (!settled) {
171
+ settled = true;
172
+ fn();
173
+ } };
174
+ // Set once the signal fires, so the body stream can fail with the SAME
175
+ // AbortError instead of the socket's ECONNRESET.
176
+ let aborted;
177
+ // ⚠️ The abort listener MUST be removed once this request can no longer be
178
+ // aborted. `{ once: true }` only fires-and-forgets on a real abort; a request
179
+ // that simply COMPLETED left its listener attached, and `dashboard/aggregator.ts`
180
+ // reuses one AbortController across an unbounded SSE reconnect loop — measured,
181
+ // 12 completed requests left 12 listeners, each pinning a finished req/res and
182
+ // its closure, and a later abort would destroy every stale request in turn.
183
+ let detachAbort = () => { };
184
+ let req;
185
+ try {
186
+ req = requestLiteralLoopback(
187
+ // Dial the parsed host/port explicitly (and validated as loopback) rather
188
+ // than handing over the URL, so nothing can re-target the request.
189
+ { host, port }, {
190
+ path: `${target.pathname}${target.search}`,
191
+ method,
192
+ headers,
193
+ }, res => {
194
+ const status = res.statusCode ?? 0;
195
+ // Fetch's null-body statuses that can actually ARRIVE HERE are 204/205/304,
196
+ // plus any HEAD response. 101 is deliberately absent: node:http never calls
197
+ // this callback for it (it emits `upgrade` instead — handled below), so a
198
+ // `status === 101` branch here would be dead code that reads as covered. `new Response(stream, {status:205})` THROWS,
199
+ // and it throws inside this async callback — on Node that escaped as an
200
+ // uncaught exception and killed the process, while Bun happens to tolerate
201
+ // 205, which is exactly why it went unnoticed.
202
+ const bodyless = status === 204 || status === 205
203
+ || status === 304 || method === 'HEAD';
204
+ const stream = bodyless ? null : new ReadableStream({
205
+ start(controller) {
206
+ res.on('data', (chunk) => {
207
+ try {
208
+ controller.enqueue(new Uint8Array(Buffer.from(chunk)));
209
+ }
210
+ catch { /* already closed by a cancel() */ }
211
+ });
212
+ res.on('end', () => {
213
+ detachAbort();
214
+ try {
215
+ controller.close();
216
+ }
217
+ catch { /* already closed */ }
218
+ });
219
+ res.on('error', err => {
220
+ detachAbort();
221
+ // An abort must surface as AbortError, not the ECONNRESET that
222
+ // destroying the socket produces.
223
+ try {
224
+ controller.error(aborted ?? err);
225
+ }
226
+ catch { /* already errored */ }
227
+ });
228
+ },
229
+ // A consumer that stops reading (or aborts) must tear the socket down,
230
+ // otherwise a never-ending SSE keeps the connection and the process alive.
231
+ cancel() { detachAbort(); res.destroy(); req.destroy(); },
232
+ });
233
+ if (bodyless)
234
+ detachAbort();
235
+ // Wrapped: any status the Response constructor refuses (an upstream
236
+ // sending something illegal) must reject this promise, never escape as an
237
+ // uncaught exception out of an HTTP event callback.
238
+ let response;
239
+ try {
240
+ response = new Response(stream, {
241
+ status,
242
+ statusText: res.statusMessage ?? '',
243
+ headers: Object.fromEntries(Object.entries(res.headers)
244
+ .filter((entry) => typeof entry[1] === 'string')),
245
+ });
246
+ }
247
+ catch (err) {
248
+ detachAbort();
249
+ res.destroy();
250
+ finish(() => reject(err));
251
+ return;
252
+ }
253
+ finish(() => resolve(response));
254
+ });
255
+ }
256
+ catch (err) {
257
+ // Invalid host/port (the loopback guard) — surface it, do not hang.
258
+ detachAbort();
259
+ finish(() => reject(err));
260
+ return;
261
+ }
262
+ // ⚠️ A 101 arrives as `upgrade`, NOT as a response. Without this listener the
263
+ // promise never settles: measured against a server answering 101, the call only
264
+ // ended when an external AbortSignal fired at 800ms (and `requestDashboardAt`
265
+ // passes no signal at all, so it would hang forever if the recorded port happened
266
+ // to be a WebSocket service). The native fetch rejects such a response in ~1ms;
267
+ // do the same, since a fetch-shaped client cannot express a protocol switch.
268
+ req.on('upgrade', (_res, socket) => {
269
+ socket.destroy();
270
+ req.destroy();
271
+ detachAbort();
272
+ finish(() => reject(new TypeError('loopbackFetch: server switched protocols (101); this client does not support upgrades')));
273
+ });
274
+ // Only a failure BEFORE the response headers can reject the promise; once the
275
+ // Response exists, later socket errors surface through the stream instead.
276
+ req.on('error', err => { detachAbort(); finish(() => reject(aborted ?? err)); });
277
+ if (init.signal) {
278
+ const signal = init.signal;
279
+ const onAbort = () => {
280
+ aborted = abortError(signal);
281
+ req.destroy();
282
+ finish(() => reject(aborted));
283
+ };
284
+ if (signal.aborted) {
285
+ onAbort();
286
+ return;
287
+ }
288
+ signal.addEventListener('abort', onAbort, { once: true });
289
+ detachAbort = () => {
290
+ signal.removeEventListener('abort', onAbort);
291
+ detachAbort = () => { };
292
+ };
293
+ }
294
+ if (body)
295
+ req.end(body);
296
+ else
297
+ req.end();
298
+ });
299
+ }
300
+ /** `typeof fetch`-compatible view, for defaulting an injectable `fetchImpl`. */
301
+ export const loopbackFetchImpl = loopbackFetch;
302
+ /**
303
+ * Is this URL a literal loopback target, i.e. safe (and necessary) to send through
304
+ * {@link loopbackFetch}? For call sites whose base URL is configurable and may
305
+ * legitimately point at a remote host — those must keep using the global fetch,
306
+ * which handles TLS, redirects and real proxying.
307
+ *
308
+ * Literal only, deliberately: a DNS name that currently resolves to 127.0.0.1
309
+ * is not a loopback guarantee (it can be re-pointed), and `loopbackFetch`'s own
310
+ * guard would reject it anyway.
311
+ */
312
+ export function isLoopbackUrl(url) {
313
+ try {
314
+ const parsed = new URL(url);
315
+ // `http:` only — this transport IS node:http, so reporting an `https:` URL as
316
+ // loopback-safe would steer a caller onto a transport that cannot serve it.
317
+ if (parsed.protocol !== 'http:')
318
+ return false;
319
+ const host = normaliseLoopbackHost(parsed.hostname);
320
+ return host === '127.0.0.1' || host === '::1';
321
+ }
322
+ catch {
323
+ return false;
324
+ }
325
+ }
326
+ /**
327
+ * Canonicalise a hostname for the loopback allow-list.
328
+ *
329
+ * · Strips the brackets `URL.hostname` keeps around IPv6 literals.
330
+ * · Maps `localhost` to `127.0.0.1`.
331
+ *
332
+ * ⚠️ `localhost` USED to be rejected here, on the reasoning that "a DNS name is
333
+ * not a loopback guarantee". That was backwards: refusing it did not avoid a
334
+ * lookup, it pushed the request onto the GLOBAL fetch, which then proxied it —
335
+ * measured with a self-hosted TTS endpoint, the `Authorization: Bearer …` header
336
+ * arrived at the stand-in proxy. `localhost` is reserved for loopback by RFC 6761,
337
+ * and mapping it here means we dial the literal address and never resolve DNS at
338
+ * all, which is strictly safer than the alternative. Every OTHER hostname stays
339
+ * rejected — that is where the rebinding concern actually applies.
340
+ */
341
+ function normaliseLoopbackHost(hostname) {
342
+ const bare = hostname.startsWith('[') && hostname.endsWith(']')
343
+ ? hostname.slice(1, -1)
344
+ : hostname;
345
+ return bare === 'localhost' ? '127.0.0.1' : bare;
346
+ }
347
+ //# sourceMappingURL=loopback-fetch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loopback-fetch.js","sourceRoot":"","sources":["../../src/core/loopback-fetch.ts"],"names":[],"mappings":"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"}
@@ -20,6 +20,7 @@ import { spawn } from 'node:child_process';
20
20
  import { type MaintenanceConfig } from '../global-config.js';
21
21
  import { type RestartIntent } from '../services/restart-intent-store.js';
22
22
  import { type GlobalInstallPlan } from '../utils/global-install.js';
23
+ import { type BinaryInstallShape, type UpdateStrategy } from './binary-self-update.js';
23
24
  export interface MaintenanceState {
24
25
  /** Local date the auto-update run was last handled (fired or skipped). */
25
26
  autoUpdate?: {
@@ -39,6 +40,30 @@ export interface MaintenanceDeps {
39
40
  currentVersion: () => string;
40
41
  /** Updates the owning npm/pnpm/Bun global install (download/install only). */
41
42
  runUpdate: () => void;
43
+ /**
44
+ * What `runUpdate` actually installed, or '' when it could not be determined.
45
+ *
46
+ * ⚠️ WHY THIS EXISTS — THE BAKED VERSION SHADOWS A COMPLETED UPDATE, ON BOTH
47
+ * STRATEGIES. A compiled binary's version is baked in at compile time, and
48
+ * `botmuxVersionAt` returns that value for ANY directory you ask about. So
49
+ * `currentVersion()` after an update still reports this process's OLD version:
50
+ *
51
+ * · self-replace — the file on disk was swapped; nothing on disk is read.
52
+ * · package-manager — npm rewrote the install's package.json, but the baked
53
+ * value takes priority over reading it (measured:
54
+ * package.json says 3.19.0, the call returns 3.18.4).
55
+ *
56
+ * Either way `after === before`, so the tick logs "already on the latest
57
+ * version" and NEVER restarts onto what it just installed. Both branches
58
+ * therefore report the installed version explicitly — the self-replace path from
59
+ * what it downloaded, the package-manager path via `diskVersionAt` (which
60
+ * deliberately ignores the baked value).
61
+ *
62
+ * Returns '' when undeterminable, and the tick falls back to the re-read
63
+ * comparison. Under Node the baked value is absent, so all of this is a no-op
64
+ * and behaviour is byte-identical to before.
65
+ */
66
+ installedVersion?: () => string;
42
67
  writeIntent: (intent: RestartIntent) => void;
43
68
  /** Spawn a detached `botmux restart` (this process is then killed by pm2). */
44
69
  triggerRestart: () => void;
@@ -73,6 +98,23 @@ export declare function maintenanceRestartLogPath(): string;
73
98
  export declare function globalInstallUpdateCwd(): string;
74
99
  /** Run the ownership-aware update synchronously. */
75
100
  export declare function installLatestBotmuxSync(plan?: GlobalInstallPlan): void;
101
+ /**
102
+ * Apply an update for whatever install shape this process is, resolving the
103
+ * strategy first so a compiled binary is handled instead of being rejected as an
104
+ * "unsupported install".
105
+ *
106
+ * Shared by the CLI (`botmux update`), the dashboard's `/api/update/run` and the
107
+ * scheduled auto-update so the three cannot drift — they previously all funnelled
108
+ * into `resolveGlobalInstallPlan`, which is exactly the call that fails in
109
+ * compiled mode.
110
+ *
111
+ * @param spec the version spec for the package-manager path, and the version to
112
+ * download for the self-replace path ('latest' resolves upstream).
113
+ */
114
+ export declare function applyBotmuxUpdate(installRoot: string, version: string): Promise<{
115
+ strategy: UpdateStrategy['kind'];
116
+ detail: string;
117
+ }>;
76
118
  /**
77
119
  * Cross-process lock target that serializes global botmux updates
78
120
  * between the scheduled auto-update (this daemon process) and a
@@ -92,12 +134,43 @@ export declare function globalInstallUpdateLockTarget(): string;
92
134
  * restarts the rest (the 2026-06-11 incident). `setsid` starts it in a brand
93
135
  * new session, reparented to init, immune to botmux-0's teardown. Without
94
136
  * setsid we fall back to a plain spawn (still detached by the caller).
137
+ *
138
+ * ⚠️ COMPILED BINARY: there is no `cli.js` to pass. Under Node the shape is
139
+ * `node <dist/cli.js> restart`, where argv[2] is `restart`. A single-file
140
+ * executable IS the CLI, so the same shape becomes `<binary> <cli.js path>
141
+ * restart` — and argv[2] is then the PATH, not `restart`. MEASURED on the real
142
+ * v3.18.4 binary: it printed the help banner and **exited 0**, i.e. a restart
143
+ * that silently never happened while reporting success. So in standalone mode the
144
+ * entry argument is dropped entirely and the binary is invoked as
145
+ * `<binary> restart`.
95
146
  */
96
- export declare function buildRestartLauncher(node: string, cliEntry: string, hasSetsid: boolean): {
147
+ export declare function buildRestartLauncher(node: string, cliEntry: string, hasSetsid: boolean, standalone?: boolean): {
97
148
  cmd: string;
98
149
  args: string[];
99
150
  };
100
151
  export declare function detachedRestartEnv(inheritedEnv?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
152
+ /**
153
+ * Resolve the executable a detached restart should launch.
154
+ *
155
+ * ⚠️ WHY `process.execPath` IS NOT ALWAYS RIGHT FOR A COMPILED BINARY. For the
156
+ * self-replacing (install.sh) shape it is exactly right: the path is stable and we
157
+ * just swapped the bytes underneath it. But for a PACKAGE-MANAGER-owned binary the
158
+ * running path can be VERSIONED — pnpm's virtual store yields
159
+ * `…/.pnpm/botmux-linux-x64@<old version>/node_modules/botmux-linux-x64/botmux`.
160
+ * After the update, npm/pnpm's postinstall has re-pointed the stable launcher at
161
+ * the NEW subpackage, while this process's `execPath` still names the OLD one. So
162
+ * restarting from `execPath` either fails with ENOENT (the old store entry was
163
+ * pruned) or silently starts the OLD binary — an update that reports success and
164
+ * does not take effect.
165
+ *
166
+ * The stable launcher (`~/.botmux/bin/botmux`) is what both installers maintain and
167
+ * re-point, so it is the correct target after a package-manager update. Fall back
168
+ * to `execPath` when it is missing (nothing else to go on) — that is the
169
+ * pre-existing behaviour.
170
+ *
171
+ * Pure over its inputs so every branch is testable without a compiled binary.
172
+ */
173
+ export declare function resolveStandaloneRestartExecutable(standalone: boolean, execPath: string, shape: BinaryInstallShape, launcherPath: string, launcherExists: boolean): string;
101
174
  /**
102
175
  * Spawn a detached `botmux restart`, immune to this process's own teardown
103
176
  * (setsid → a new session reparented to init, so PM2 killing the current
@@ -108,6 +181,20 @@ export declare function detachedRestartEnv(inheritedEnv?: NodeJS.ProcessEnv): No
108
181
  * @param reason short tag written to the log (e.g. 'auto-update', 'dashboard').
109
182
  */
110
183
  export declare function spawnDetachedRestart(reason: string, activePackageRoot?: string, restartLeaseId?: string): ReturnType<typeof spawn>;
184
+ /**
185
+ * Perform a binary self-replace in a CHILD process and block until it finishes.
186
+ *
187
+ * WHY A CHILD AND NOT AN `await` HERE: `runMaintenanceTick` is synchronous and
188
+ * runs inside `withFileLockSync`, so it cannot await the download. Re-execing this
189
+ * same binary with a hidden `__self-update` subcommand keeps the lock semantics
190
+ * byte-identical to the package-manager branch (spawn one child, wait, throw on a
191
+ * non-zero exit) without turning the whole tick async — which would change the
192
+ * locking shape for the npm path too, the one that is currently working.
193
+ *
194
+ * Returns the version that was installed (printed by the child on its last line),
195
+ * or '' when the child reported success without a parseable version.
196
+ */
197
+ export declare function runSelfReplaceBlocking(version?: string): string;
111
198
  /** Start the maintenance loop. Call only on the primary daemon (bot-0). */
112
199
  export declare function startMaintenance(): void;
113
200
  export declare function stopMaintenance(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"maintenance.d.ts","sourceRoot":"","sources":["../../src/core/maintenance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAY,KAAK,EAAa,MAAM,oBAAoB,CAAC;AAMhE,OAAO,EAAoB,KAAK,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAG/E,OAAO,EAKL,KAAK,aAAa,EACnB,MAAM,qCAAqC,CAAC;AAQ7C,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,4BAA4B,CAAC;AAIpC,MAAM,WAAW,gBAAgB;IAC/B,0EAA0E;IAC1E,UAAU,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;CACnC;AAED,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,iBAAiB,GAAG,SAAS,CAAC;IAChD,SAAS,EAAE,MAAM,gBAAgB,CAAC;IAClC,UAAU,EAAE,CAAC,CAAC,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAC1C,OAAO,EAAE,MAAM,OAAO,CAAC;IACvB,UAAU,EAAE,MAAM,OAAO,CAAC;IAC1B,iEAAiE;IACjE,cAAc,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;IACzC,6EAA6E;IAC7E,cAAc,EAAE,MAAM,MAAM,CAAC;IAC7B,8EAA8E;IAC9E,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,WAAW,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;IAC7C,8EAA8E;IAC9E,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,eAAe,GAAG,IAAI,CA8D9D;AAMD,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB,CASpE;AAED,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,EAAE,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAM9E;AAID;2CAC2C;AAC3C,eAAO,MAAM,mBAAmB,QAAS,CAAC;AAE1C;uEACuE;AACvE,wBAAgB,yBAAyB,IAAI,MAAM,CAElD;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CAE/C;AAED,oDAAoD;AACpD,wBAAgB,uBAAuB,CAAC,IAAI,GAAE,iBAA8C,GAAG,IAAI,CAWlG;AAED;;;;;;GAMG;AACH,wBAAgB,+BAA+B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAIvE;AAED,wBAAgB,6BAA6B,IAAI,MAAM,CAEtD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,OAAO,GACjB;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAGjC;AAED,wBAAgB,kBAAkB,CAAC,YAAY,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAAC,UAAU,CAuCnG;AAWD;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,iBAAiB,CAAC,EAAE,MAAM,EAC1B,cAAc,CAAC,EAAE,MAAM,GACtB,UAAU,CAAC,OAAO,KAAK,CAAC,CAmC1B;AA2CD,2EAA2E;AAC3E,wBAAgB,gBAAgB,IAAI,IAAI,CAavC;AAED,wBAAgB,eAAe,IAAI,IAAI,CAEtC"}
1
+ {"version":3,"file":"maintenance.d.ts","sourceRoot":"","sources":["../../src/core/maintenance.ts"],"names":[],"mappings":"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"}