@catalyst-cloud/cli 0.14.6 → 0.14.7

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 (62) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +5 -3
  3. package/dist/args.js +51 -13
  4. package/dist/cli.js +58 -10
  5. package/dist/config.js +16 -3
  6. package/dist/daemon-authority-lifetime.js +171 -0
  7. package/dist/daemon-authority.js +216 -0
  8. package/dist/daemon-cache.js +348 -0
  9. package/dist/daemon-store.js +370 -0
  10. package/dist/daemon-supervisor.js +460 -0
  11. package/dist/env/onboard-draft-store.js +243 -0
  12. package/dist/env/onboard-draft.js +33 -0
  13. package/dist/env/selected-import-writer.js +163 -0
  14. package/dist/env/settings-authority.js +203 -0
  15. package/dist/events-cloud-tail.js +134 -0
  16. package/dist/events-cloud.js +500 -0
  17. package/dist/events.js +13 -4
  18. package/dist/oauth.js +125 -33
  19. package/dist/onboard-accounts.js +259 -0
  20. package/dist/onboard-automation-management.js +15 -0
  21. package/dist/onboard-bootstrap.js +162 -0
  22. package/dist/onboard-capabilities.js +15 -7
  23. package/dist/onboard-capacity.js +226 -0
  24. package/dist/onboard-existing.js +106 -32
  25. package/dist/onboard-file-snapshot.js +45 -0
  26. package/dist/onboard-login-candidate.js +261 -0
  27. package/dist/onboard-projects.js +350 -0
  28. package/dist/onboard-repositories.js +147 -37
  29. package/dist/onboard-runtime.js +104 -9
  30. package/dist/onboard-settings-context.js +141 -0
  31. package/dist/onboard-settings-import.js +127 -0
  32. package/dist/onboard-settings.js +234 -0
  33. package/dist/onboard-ui.js +99 -11
  34. package/dist/onboard-workflow.js +316 -0
  35. package/dist/onboard.js +359 -27
  36. package/dist/owned-fetch.js +182 -0
  37. package/dist/query.js +77 -27
  38. package/dist/ready.js +143 -35
  39. package/dist/replica.js +11 -4
  40. package/dist/sdk.js +33 -0
  41. package/dist/watch.js +2 -2
  42. package/package.json +3 -2
  43. package/skills/catalyst-github/SKILL.md +2 -2
  44. package/skills/catalyst-github/references/is-it-mergeable.md +1 -1
  45. package/skills/catalyst-github/references/what-a-pr-accumulates.md +1 -1
  46. package/skills/catalyst-github/scripts/is-it-mergeable.mjs +210 -45
  47. package/skills/catalyst-github/scripts/read-pr.mjs +51 -15
  48. package/skills/catalyst-linear/SKILL.md +3 -3
  49. package/skills/catalyst-linear/references/reading-a-ticket.md +3 -3
  50. package/skills/catalyst-linear/scripts/read-ticket.mjs +37 -12
  51. package/skills/catalyst-onboard/SKILL.md +5 -4
  52. package/skills/catalyst-onboard/references/local-sync.md +3 -1
  53. package/skills/run-this-project/SKILL.md +1 -1
  54. package/skills/unstick/SKILL.md +1 -1
  55. package/skills/what-needs-me/SKILL.md +1 -1
  56. package/skills/what-this-repo-needs/SKILL.md +1 -1
  57. package/skills/whats-happening/SKILL.md +2 -2
  58. package/skills/whats-happening/references/status-reply.md +1 -1
  59. package/skills/whats-happening/scripts/snapshot.mjs +104 -26
  60. package/vendor/settings/index.d.ts +4 -0
  61. package/vendor/settings/index.js +2937 -0
  62. package/vendor/settings/provenance.json +148 -0
@@ -0,0 +1,500 @@
1
+ // The backbone cursor belongs to tenant_events, never the replica's change_log.
2
+ import { normalizeBaseUrl, requireConfig } from "./config.js";
3
+ import { CliError } from "./errors.js";
4
+ import { bearerFor } from "./oauth.js";
5
+ const headHeader = "x-catalyst-event-backbone-head-seq";
6
+ const maxLine = 1_048_576;
7
+ // A filtered page is JSON, sized by the server to about 1 MiB; anything far past that is not ours.
8
+ const maxQueryBody = 16 * maxLine;
9
+ const queryLimit = 200;
10
+ const maxLagPoll = 8_000;
11
+ // CTC-4562 — once the cloud pushes events, the replay is only a safety net for a silent socket.
12
+ const pushedFallback = 600_000;
13
+ const polledFallback = 30_000;
14
+ function sequence(value) {
15
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
16
+ }
17
+ function protocol(message) {
18
+ return new CliError(message, "events-protocol");
19
+ }
20
+ class RetryRequest extends Error {
21
+ delayMs;
22
+ constructor(delayMs) {
23
+ super("cloud event service unavailable");
24
+ this.delayMs = delayMs;
25
+ }
26
+ }
27
+ // Cancel this reader's wait without cancelling a refresh shared with another command.
28
+ function eventBearer(ctx, signal) {
29
+ signal.throwIfAborted();
30
+ return new Promise((resolve, reject) => {
31
+ const abort = () => reject(signal.reason);
32
+ signal.addEventListener("abort", abort, { once: true });
33
+ let authStatus;
34
+ const authCtx = { ...ctx, fetch: async (input, init) => {
35
+ const response = await ctx.fetch(input, init);
36
+ authStatus = response.status;
37
+ return response;
38
+ } };
39
+ bearerFor(authCtx, requireConfig(ctx)).then(resolve, (error) => {
40
+ // The shared helper has no typed retry status. Unknown shared failures may retry;
41
+ // a refresh we own records its actual response, including terminal 4xx refusals.
42
+ const transient = error instanceof CliError && (error.code === "network" ||
43
+ authStatus === 429 || (authStatus !== undefined && authStatus >= 500) ||
44
+ (error.code === "session-refresh-failed" && authStatus === undefined));
45
+ reject(error instanceof CliError && !transient
46
+ ? new CliError("cloud event authentication failed; run catalyst login", "events-auth")
47
+ : new Error("cloud event authentication temporarily unavailable"));
48
+ }).finally(() => signal.removeEventListener("abort", abort));
49
+ });
50
+ }
51
+ function responseHead(response) {
52
+ const raw = response.headers.get(headHeader);
53
+ const head = raw === null || raw.trim() === "" ? NaN : Number(raw);
54
+ if (!sequence(head))
55
+ throw protocol("cloud event head could not be verified");
56
+ return head;
57
+ }
58
+ function eventsUrl(ctx, route, params) {
59
+ const url = new URL(`${normalizeBaseUrl(requireConfig(ctx).baseUrl)}/api/v1/events/${route}`);
60
+ for (const [name, value] of Object.entries(params))
61
+ if (value)
62
+ url.searchParams.set(name, value);
63
+ return url;
64
+ }
65
+ async function request(ctx, since, signal) {
66
+ return send(ctx, eventsUrl(ctx, "backbone", { since: String(since) }), "application/x-ndjson", [409], signal);
67
+ }
68
+ async function send(ctx, url, accept, allowed, signal) {
69
+ const bounded = AbortSignal.any([signal, AbortSignal.timeout(15_000)]);
70
+ const token = await eventBearer(ctx, bounded);
71
+ bounded.throwIfAborted();
72
+ const response = await ctx.fetch(url, {
73
+ headers: { authorization: `Bearer ${token}`, accept },
74
+ signal: bounded,
75
+ redirect: "error",
76
+ });
77
+ if (response.status === 401 || response.status === 403) {
78
+ await response.body?.cancel();
79
+ throw new CliError("cloud event access refused; check catalyst status and sign in again", "events-auth", 2, response.status);
80
+ }
81
+ if (response.status !== 200 && !allowed.includes(response.status)) {
82
+ await response.body?.cancel();
83
+ if (response.status >= 500 || response.status === 429) {
84
+ const raw = response.headers.get("retry-after");
85
+ const seconds = raw === null ? NaN : Number(raw);
86
+ const delay = Number.isFinite(seconds) ? seconds * 1_000 : raw ? Date.parse(raw) - ctx.now().getTime() : 0;
87
+ throw new RetryRequest(Number.isFinite(delay) ? Math.min(300_000, Math.max(0, delay)) : 0);
88
+ }
89
+ throw new CliError(`cloud event request refused (HTTP ${response.status})`, "events-http", 2, response.status);
90
+ }
91
+ return response;
92
+ }
93
+ /** Probe the head without downloading the backlog. An ahead-of-head cursor is deliberate. */
94
+ export async function cloudEventHead(ctx, signal = AbortSignal.timeout(15_000)) {
95
+ const response = await request(ctx, Number.MAX_SAFE_INTEGER, signal);
96
+ try {
97
+ const head = responseHead(response);
98
+ // A successful empty response is possible only at the maximum safe sequence.
99
+ if (response.status !== 409 && head !== Number.MAX_SAFE_INTEGER)
100
+ throw protocol("cloud event head probe did not return an ahead-of-head response");
101
+ return head;
102
+ }
103
+ finally {
104
+ await response.body?.cancel();
105
+ }
106
+ }
107
+ function validEvent(ctx, value, cursor) {
108
+ if (!value || typeof value !== "object")
109
+ return false;
110
+ const event = value;
111
+ return event.tenantId === requireConfig(ctx).account && sequence(event.sequence) && event.sequence > cursor &&
112
+ typeof event.eventId === "string" && typeof event.type === "string" && typeof event.recordedAt === "string" && "payload" in event;
113
+ }
114
+ // A page closed early by its consumer ends with undefined.
115
+ async function* replay(ctx, after, signal) {
116
+ const response = await request(ctx, after, signal);
117
+ let reader;
118
+ try {
119
+ const head = responseHead(response);
120
+ if (response.status === 409)
121
+ throw new CliError(`cloud refused event cursor ${after} (head ${head}); history may be missing or the cursor may be ahead. Inspect events status before choosing a new --after cursor`, "events-gap", 2, 409);
122
+ if (head < after)
123
+ throw protocol("cloud event head moved behind the requested cursor");
124
+ if (!response.body)
125
+ throw protocol("cloud event replay body is absent");
126
+ reader = response.body.getReader();
127
+ const decoder = new TextDecoder("utf-8", { fatal: true });
128
+ let buffer = "";
129
+ let cursor = after;
130
+ const parse = (line) => {
131
+ let value;
132
+ try {
133
+ value = JSON.parse(line);
134
+ }
135
+ catch {
136
+ throw protocol("cloud event replay contains invalid JSON");
137
+ }
138
+ if (!value || typeof value !== "object")
139
+ throw protocol("cloud event replay contains an invalid event");
140
+ if (!validEvent(ctx, value, cursor))
141
+ throw protocol("cloud event replay contains an invalid tenant or event sequence");
142
+ cursor = value.sequence;
143
+ return value;
144
+ };
145
+ while (true) {
146
+ signal.throwIfAborted();
147
+ const { done, value } = await reader.read();
148
+ try {
149
+ buffer += decoder.decode(value, { stream: !done });
150
+ }
151
+ catch {
152
+ throw protocol("cloud event replay contains invalid UTF-8");
153
+ }
154
+ let newline;
155
+ while ((newline = buffer.indexOf("\n")) >= 0) {
156
+ if (newline > maxLine)
157
+ throw protocol("cloud event exceeds the replay line limit");
158
+ const line = buffer.slice(0, newline).trim();
159
+ buffer = buffer.slice(newline + 1);
160
+ if (line) {
161
+ signal.throwIfAborted();
162
+ yield parse(line);
163
+ }
164
+ }
165
+ if (buffer.length > maxLine)
166
+ throw protocol("cloud event exceeds the replay line limit");
167
+ if (done)
168
+ break;
169
+ }
170
+ if (buffer.trim()) {
171
+ signal.throwIfAborted();
172
+ yield parse(buffer.trim());
173
+ }
174
+ if (cursor === after && cursor < head)
175
+ throw protocol("cloud event replay made no progress toward its head");
176
+ return { through: cursor, more: cursor < head, head };
177
+ }
178
+ finally {
179
+ if (reader) {
180
+ await reader.cancel().catch(() => { });
181
+ reader.releaseLock();
182
+ }
183
+ else
184
+ await response.body?.cancel();
185
+ }
186
+ }
187
+ // One page of the indexed, filtered query (CTC-4549). The index fills after the archive write, so
188
+ // `coverage.indexedToSeq` may trail the head; the reader polls until it catches up.
189
+ async function* queryPage(ctx, after, filter, signal) {
190
+ const url = eventsUrl(ctx, "query", { order: "asc", afterSeq: String(after), limit: String(queryLimit), type: filter.type, ticket: filter.ticket });
191
+ const response = await send(ctx, url, "application/json", [404], signal);
192
+ if (response.status === 404) {
193
+ await response.body?.cancel();
194
+ return "absent";
195
+ }
196
+ const text = await response.text();
197
+ if (text.length > maxQueryBody)
198
+ throw protocol("cloud event query response exceeds its size limit");
199
+ let body;
200
+ try {
201
+ body = JSON.parse(text);
202
+ }
203
+ catch {
204
+ throw protocol("cloud event query returned invalid JSON");
205
+ }
206
+ const page = (body && typeof body === "object" ? body : {});
207
+ if (!Array.isArray(page.events) || !("next" in page))
208
+ throw protocol("cloud event query returned an invalid page");
209
+ const from = page.coverage?.indexedFromSeq;
210
+ const to = page.coverage?.indexedToSeq;
211
+ // Nothing indexed yet, or the cursor sits below the index floor: only the backbone has that range.
212
+ if (from === null || to === null || from === undefined || to === undefined)
213
+ return "uncovered";
214
+ if (!sequence(from) || !sequence(to))
215
+ throw protocol("cloud event query coverage is invalid");
216
+ if (after + 1 < from)
217
+ return "uncovered";
218
+ let cursor = after;
219
+ const events = [];
220
+ for (const value of page.events) {
221
+ if (!validEvent(ctx, value, cursor))
222
+ throw protocol("cloud event query contains an invalid tenant or event sequence");
223
+ cursor = value.sequence;
224
+ events.push(value);
225
+ }
226
+ for (const event of events) {
227
+ signal.throwIfAborted();
228
+ yield event;
229
+ }
230
+ if (page.next !== null) {
231
+ if (cursor === after)
232
+ throw protocol("cloud event query made no progress toward its next page");
233
+ return { through: cursor, more: true };
234
+ }
235
+ return { through: Math.max(cursor, to), more: false };
236
+ }
237
+ function pushFrame(ctx, raw) {
238
+ const { after, through } = raw;
239
+ if (!sequence(after) || !sequence(through) || through < after || !Array.isArray(raw.events))
240
+ throw protocol("cloud events frame has invalid bounds");
241
+ let cursor = after;
242
+ let gap = raw.gap === true;
243
+ const events = [];
244
+ for (const value of raw.events) {
245
+ // An event too large to push arrives as a summary; the catch-up read returns it whole.
246
+ if (value && typeof value === "object" && value.payloadOmitted === true) {
247
+ gap = true;
248
+ const seq = value.sequence;
249
+ if (sequence(seq) && seq > cursor)
250
+ cursor = seq;
251
+ continue;
252
+ }
253
+ if (!validEvent(ctx, value, cursor) || value.sequence > through)
254
+ throw protocol("cloud events frame contains an invalid tenant or event sequence");
255
+ cursor = value.sequence;
256
+ events.push(value);
257
+ }
258
+ return { after, through, gap, events };
259
+ }
260
+ function pause(ms, signal, wakes) {
261
+ if (signal.aborted)
262
+ return Promise.resolve();
263
+ return new Promise((resolve) => {
264
+ const done = () => {
265
+ clearTimeout(timer);
266
+ signal.removeEventListener("abort", done);
267
+ wakes?.removeEventListener("wake", done);
268
+ resolve();
269
+ };
270
+ const timer = setTimeout(done, ms);
271
+ signal.addEventListener("abort", done, { once: true });
272
+ wakes?.addEventListener("wake", done, { once: true });
273
+ });
274
+ }
275
+ /** Foreground push subscription with durable replay, reconnecting from the last yielded cursor. */
276
+ export async function* cloudEvents(ctx, options, deps = {}) {
277
+ const { signal } = options;
278
+ const filter = options.filter ?? {};
279
+ let cursor = options.after;
280
+ let backoff = 1_000;
281
+ // A filter reads the server's index; a server without the route gets the backbone for the whole run.
282
+ let indexed = Boolean(filter.type || filter.ticket);
283
+ let lagPoll = 1_000;
284
+ while (!signal.aborted) {
285
+ let socket;
286
+ let heartbeat;
287
+ let openTimer;
288
+ const wakes = new EventTarget();
289
+ let failure;
290
+ let retryDelay = 0;
291
+ let opened = false;
292
+ let notifiedHead = 0;
293
+ // CTC-4562 — the cloud acknowledged the events channel, and the frames it has pushed since.
294
+ let pushing = false;
295
+ const pushed = [];
296
+ let woken = false;
297
+ const wake = () => { woken = true; wakes.dispatchEvent(new Event("wake")); };
298
+ const stop = () => { socket?.close(); wake(); };
299
+ const failed = () => { failure ??= new Error("cloud event connection interrupted"); wake(); };
300
+ const closed = () => {
301
+ // 4401 is routine authorization expiry. The next authenticated probe refreshes it.
302
+ failure ??= new Error("cloud event connection closed");
303
+ wake();
304
+ };
305
+ const message = (event) => {
306
+ if (!("data" in event) || typeof event.data !== "string" || event.data.length > maxLine)
307
+ return;
308
+ let frame;
309
+ try {
310
+ frame = JSON.parse(event.data);
311
+ }
312
+ catch {
313
+ return;
314
+ }
315
+ if (!frame || typeof frame !== "object")
316
+ return;
317
+ const head = frame;
318
+ if (head.type === "channels") {
319
+ pushing = Array.isArray(head.channels) && head.channels.includes("events");
320
+ wake();
321
+ return;
322
+ }
323
+ if (head.type === "events") {
324
+ try {
325
+ pushed.push(pushFrame(ctx, frame));
326
+ }
327
+ catch (error) {
328
+ failure = error instanceof Error ? error : protocol("cloud events frame is invalid");
329
+ }
330
+ wake();
331
+ return;
332
+ }
333
+ if (head.type !== "event-backbone-head")
334
+ return;
335
+ if (head.accountId !== requireConfig(ctx).account || !sequence(head.seq)) {
336
+ failure = protocol("cloud event notification has an invalid tenant or head");
337
+ wake();
338
+ return;
339
+ }
340
+ notifiedHead = Math.max(notifiedHead, head.seq);
341
+ wake();
342
+ };
343
+ const open = () => { opened = true; wake(); };
344
+ try {
345
+ // Capture the future-only start before connecting, then replay to close the handshake race.
346
+ const head = await cloudEventHead(ctx, signal);
347
+ let knownHead = head;
348
+ cursor ??= head;
349
+ if (cursor > head)
350
+ throw new CliError(`cloud refused event cursor ${cursor} (head ${head}); inspect events status before choosing a new --after cursor`, "events-gap", 2, 409);
351
+ if (signal.aborted)
352
+ break;
353
+ const cfg = requireConfig(ctx);
354
+ const url = new URL(`${normalizeBaseUrl(cfg.baseUrl)}/api/v1/connect`);
355
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
356
+ url.searchParams.set("account", cfg.account);
357
+ // CTC-4562 — ask to be pushed the matching events, whole, instead of the change feed. A cloud
358
+ // without the channel ignores these and the reader keeps its nudge-and-replay loop.
359
+ url.searchParams.set("channels", "events");
360
+ if (filter.type)
361
+ url.searchParams.set("type", filter.type);
362
+ if (filter.ticket)
363
+ url.searchParams.set("ticket", filter.ticket);
364
+ url.searchParams.set("fields", "full");
365
+ url.searchParams.set("token", await eventBearer(ctx, AbortSignal.any([signal, AbortSignal.timeout(15_000)])));
366
+ signal.throwIfAborted();
367
+ socket = (deps.socket ?? ((address) => new WebSocket(address)))(url.toString());
368
+ socket.addEventListener("open", open);
369
+ socket.addEventListener("message", message);
370
+ socket.addEventListener("close", closed);
371
+ socket.addEventListener("error", failed);
372
+ signal.addEventListener("abort", stop, { once: true });
373
+ openTimer = setTimeout(failed, 15_000);
374
+ while (!opened && !failure && !signal.aborted)
375
+ await pause(15_000, signal, wakes);
376
+ clearTimeout(openTimer);
377
+ if (failure)
378
+ throw failure;
379
+ if (signal.aborted)
380
+ break;
381
+ heartbeat = setInterval(() => {
382
+ try {
383
+ socket?.send(JSON.stringify({ type: "ping" }));
384
+ }
385
+ catch {
386
+ failed();
387
+ }
388
+ }, 10_000);
389
+ const follow = async function* (page) {
390
+ let result = await page.next();
391
+ try {
392
+ while (!result.done) {
393
+ if (failure)
394
+ throw failure;
395
+ signal.throwIfAborted();
396
+ cursor = result.value.sequence;
397
+ yield result.value;
398
+ if (failure)
399
+ throw failure;
400
+ signal.throwIfAborted();
401
+ result = await page.next();
402
+ }
403
+ }
404
+ finally {
405
+ await page.return(undefined);
406
+ }
407
+ return result.value;
408
+ };
409
+ let catchUp = true;
410
+ while (!signal.aborted) {
411
+ if (failure)
412
+ throw failure;
413
+ if (!catchUp) {
414
+ // Caught up: apply pushed frames in order. One that starts past the cursor, or carries a
415
+ // gap, sends the reader back to the catch-up read; overlap with it is dropped by sequence.
416
+ while (pushed.length > 0 && !catchUp) {
417
+ const next = pushed.shift();
418
+ knownHead = Math.max(knownHead, next.through);
419
+ if (next.through <= cursor)
420
+ continue;
421
+ if (next.gap || next.after > cursor) {
422
+ catchUp = true;
423
+ break;
424
+ }
425
+ for (const event of next.events) {
426
+ if (event.sequence <= cursor)
427
+ continue;
428
+ if (failure)
429
+ throw failure;
430
+ signal.throwIfAborted();
431
+ cursor = event.sequence;
432
+ yield event;
433
+ }
434
+ cursor = Math.max(cursor, next.through);
435
+ }
436
+ if (failure)
437
+ throw failure;
438
+ if (catchUp || cursor < Math.max(knownHead, notifiedHead)) {
439
+ catchUp = true;
440
+ continue;
441
+ }
442
+ woken = false;
443
+ await pause(pushing ? pushedFallback : polledFallback, signal, wakes);
444
+ // A wake with frames is applied without a read; a timeout, or a nudge with no push channel,
445
+ // replays (a bounded replay also catches a lost notification or a silent half-open socket).
446
+ if (!woken || !pushing)
447
+ catchUp = true;
448
+ continue;
449
+ }
450
+ const start = cursor;
451
+ let end = indexed ? yield* follow(queryPage(ctx, start, filter, signal)) : yield* follow(replay(ctx, start, signal));
452
+ const queried = indexed && end !== "absent" && end !== "uncovered";
453
+ if (end === "absent")
454
+ indexed = false;
455
+ if (end === "absent" || end === "uncovered")
456
+ end = yield* follow(replay(ctx, cursor, signal));
457
+ if (!end)
458
+ return;
459
+ backoff = 1_000;
460
+ if (failure)
461
+ throw failure;
462
+ cursor = Math.max(cursor, end.through);
463
+ knownHead = Math.max(knownHead, end.head ?? 0, notifiedHead);
464
+ if (end.more)
465
+ continue;
466
+ if (queried && cursor < knownHead) {
467
+ // The index trails the archive write; poll it instead of spinning or waiting for a push.
468
+ lagPoll = cursor > start ? 1_000 : Math.min(lagPoll * 2, maxLagPoll);
469
+ await pause(lagPoll, signal);
470
+ continue;
471
+ }
472
+ lagPoll = 1_000;
473
+ if (cursor < knownHead)
474
+ continue;
475
+ catchUp = false;
476
+ }
477
+ }
478
+ catch (error) {
479
+ if (signal.aborted)
480
+ return;
481
+ if (error instanceof CliError)
482
+ throw error;
483
+ if (error instanceof RetryRequest)
484
+ retryDelay = error.delayMs;
485
+ // Never propagate socket URLs or transport exceptions: they can contain bearer credentials.
486
+ }
487
+ finally {
488
+ clearInterval(heartbeat);
489
+ clearTimeout(openTimer);
490
+ signal.removeEventListener("abort", stop);
491
+ socket?.removeEventListener("open", open);
492
+ socket?.removeEventListener("message", message);
493
+ socket?.removeEventListener("close", closed);
494
+ socket?.removeEventListener("error", failed);
495
+ socket?.close();
496
+ }
497
+ await pause(Math.max(backoff, retryDelay), signal);
498
+ backoff = Math.min(backoff * 2, 30_000);
499
+ }
500
+ }
package/dist/events.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { flagInt, flagString, positionals } from "./args.js";
2
- import { apiBase, requireConfig } from "./config.js";
2
+ import { normalizeBaseUrl, requireConfig } from "./config.js";
3
3
  import { CliError, UsageError } from "./errors.js";
4
4
  import { authStrategyFor } from "./oauth.js";
5
5
  import { eventCacheStatus } from "./event-status.js";
@@ -12,7 +12,8 @@ export async function createEventSync(ctx, deps = {}) {
12
12
  if (auth.kind === "cookie")
13
13
  throw new CliError("event sync requires a token or OAuth session", "events-auth");
14
14
  return new sdk.CatalystEventSync({
15
- baseUrl: apiBase(cfg),
15
+ // Events SDK adds /api/v1/events/backbone itself; unlike the replica it takes an origin.
16
+ baseUrl: normalizeBaseUrl(cfg.baseUrl),
16
17
  auth,
17
18
  tenantId: cfg.account,
18
19
  fetch: ctx.fetch,
@@ -30,8 +31,16 @@ export async function cmdEvents(args, ctx, deps = {}) {
30
31
  sdk.defaultEventCacheDirectory(cfg.account);
31
32
  if (sub === "status") {
32
33
  const status = await eventCacheStatus(ctx, directory, args.flags.probe === true);
33
- ctx.stdout(args.json ? JSON.stringify(status) : `events: ${status.verdict} at ${directory}${status.reasons.length ? ` (${status.reasons.join("; ")})` : ` (cursor ${status.cursor}, cloud head ${status.head})`}`);
34
- return status.verdict === "current" ? 0 : status.verdict === "stale" ? 1 : status.verdict === "absent" ? 3 : 2;
34
+ ctx.stdout(args.json
35
+ ? JSON.stringify(status)
36
+ : `events: ${status.verdict} at ${directory}${status.reasons.length ? ` (${status.reasons.join("; ")})` : ` (cursor ${status.cursor}, cloud head ${status.head})`}`);
37
+ return status.verdict === "current"
38
+ ? 0
39
+ : status.verdict === "stale"
40
+ ? 1
41
+ : status.verdict === "absent"
42
+ ? 3
43
+ : 2;
35
44
  }
36
45
  const after = startingCursor(args, sub === "query");
37
46
  const matches = matcher(args);