@catalyst-cloud/cli 0.14.5 → 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 (63) hide show
  1. package/CHANGELOG.md +9 -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/legacy.js +145 -32
  19. package/dist/oauth.js +125 -33
  20. package/dist/onboard-accounts.js +259 -0
  21. package/dist/onboard-automation-management.js +15 -0
  22. package/dist/onboard-bootstrap.js +162 -0
  23. package/dist/onboard-capabilities.js +15 -7
  24. package/dist/onboard-capacity.js +226 -0
  25. package/dist/onboard-existing.js +106 -32
  26. package/dist/onboard-file-snapshot.js +45 -0
  27. package/dist/onboard-login-candidate.js +261 -0
  28. package/dist/onboard-projects.js +350 -0
  29. package/dist/onboard-repositories.js +147 -37
  30. package/dist/onboard-runtime.js +104 -9
  31. package/dist/onboard-settings-context.js +141 -0
  32. package/dist/onboard-settings-import.js +127 -0
  33. package/dist/onboard-settings.js +234 -0
  34. package/dist/onboard-ui.js +99 -11
  35. package/dist/onboard-workflow.js +316 -0
  36. package/dist/onboard.js +359 -27
  37. package/dist/owned-fetch.js +182 -0
  38. package/dist/query.js +77 -27
  39. package/dist/ready.js +143 -35
  40. package/dist/replica.js +11 -4
  41. package/dist/sdk.js +33 -0
  42. package/dist/watch.js +2 -2
  43. package/package.json +3 -2
  44. package/skills/catalyst-github/SKILL.md +2 -2
  45. package/skills/catalyst-github/references/is-it-mergeable.md +1 -1
  46. package/skills/catalyst-github/references/what-a-pr-accumulates.md +1 -1
  47. package/skills/catalyst-github/scripts/is-it-mergeable.mjs +210 -45
  48. package/skills/catalyst-github/scripts/read-pr.mjs +51 -15
  49. package/skills/catalyst-linear/SKILL.md +3 -3
  50. package/skills/catalyst-linear/references/reading-a-ticket.md +3 -3
  51. package/skills/catalyst-linear/scripts/read-ticket.mjs +37 -12
  52. package/skills/catalyst-onboard/SKILL.md +5 -4
  53. package/skills/catalyst-onboard/references/local-sync.md +3 -1
  54. package/skills/run-this-project/SKILL.md +1 -1
  55. package/skills/unstick/SKILL.md +1 -1
  56. package/skills/what-needs-me/SKILL.md +1 -1
  57. package/skills/what-this-repo-needs/SKILL.md +1 -1
  58. package/skills/whats-happening/SKILL.md +2 -2
  59. package/skills/whats-happening/references/status-reply.md +1 -1
  60. package/skills/whats-happening/scripts/snapshot.mjs +104 -26
  61. package/vendor/settings/index.d.ts +4 -0
  62. package/vendor/settings/index.js +2937 -0
  63. package/vendor/settings/provenance.json +148 -0
@@ -0,0 +1,182 @@
1
+ export function ownedFetch(fetchImpl, parent, options = {}) {
2
+ const limit = options.maxBodyBytes;
3
+ if (limit !== undefined && (!Number.isSafeInteger(limit) || limit < 1))
4
+ throw new Error("owned_fetch_body_limit_invalid");
5
+ const owned = new AbortController();
6
+ const requests = new Set();
7
+ const bodies = new Set();
8
+ const cleanupErrors = [];
9
+ let settling = false;
10
+ const stopped = () => owned.abort(parent.reason);
11
+ parent.addEventListener("abort", stopped, { once: true });
12
+ if (parent.aborted)
13
+ stopped();
14
+ const trackedFetch = (input, init) => {
15
+ if (owned.signal.aborted)
16
+ return Promise.reject(owned.signal.reason);
17
+ if (settling)
18
+ return Promise.reject(new Error("owned_fetch_stopped"));
19
+ const requestSignal = input instanceof Request ? input.signal : undefined;
20
+ const signal = AbortSignal.any([
21
+ owned.signal,
22
+ ...(requestSignal ? [requestSignal] : []),
23
+ ...(init?.signal ? [init.signal] : []),
24
+ ]);
25
+ if (signal.aborted)
26
+ return Promise.reject(signal.reason);
27
+ const network = new AbortController();
28
+ const stopHeaders = () => network.abort(signal.reason);
29
+ signal.addEventListener("abort", stopHeaders, { once: true });
30
+ const pending = (async () => {
31
+ let response;
32
+ try {
33
+ response = await fetchImpl(input, {
34
+ ...init,
35
+ signal: network.signal,
36
+ redirect: "error",
37
+ });
38
+ }
39
+ finally {
40
+ signal.removeEventListener("abort", stopHeaders);
41
+ }
42
+ if (!response.body) {
43
+ if (signal.aborted)
44
+ throw signal.reason;
45
+ return response;
46
+ }
47
+ const reader = response.body.getReader();
48
+ let bodyController;
49
+ let terminal = false;
50
+ let consumed = 0;
51
+ let completed = false;
52
+ let enteredRead;
53
+ let firstCancel;
54
+ let complete;
55
+ const joined = new Promise((resolve) => {
56
+ complete = resolve;
57
+ });
58
+ bodies.add(joined);
59
+ const finish = () => {
60
+ if (completed)
61
+ return;
62
+ completed = true;
63
+ signal.removeEventListener("abort", aborted);
64
+ try {
65
+ reader.releaseLock();
66
+ }
67
+ catch (error) {
68
+ cleanupErrors.push(error);
69
+ }
70
+ complete();
71
+ };
72
+ const cancel = (reason) => {
73
+ if (firstCancel)
74
+ return firstCancel;
75
+ if (completed)
76
+ return Promise.resolve();
77
+ terminal = true;
78
+ // Retain this FIRST promise: a second cancel can resolve before underlying IO acknowledges it.
79
+ firstCancel = reader.cancel(reason);
80
+ // Close/error the owned reader FIRST. Aborting native fetch first can instead error its
81
+ // upstream stream and turn an otherwise acknowledged cancellation into a rejection.
82
+ network.abort(reason);
83
+ const read = enteredRead;
84
+ void Promise.allSettled([firstCancel, ...(read ? [read] : [])]).then((results) => {
85
+ const cancellation = results[0];
86
+ if (cancellation?.status === "rejected")
87
+ cleanupErrors.push(cancellation.reason);
88
+ finish();
89
+ });
90
+ return firstCancel;
91
+ };
92
+ const aborted = () => {
93
+ if (completed)
94
+ return;
95
+ terminal = true;
96
+ // An abort is an error, never successful EOF that could publish a truncated page.
97
+ try {
98
+ bodyController?.error(signal.reason);
99
+ }
100
+ catch {
101
+ /* already terminal */
102
+ }
103
+ void cancel(signal.reason).catch(() => { });
104
+ };
105
+ const body = new ReadableStream({
106
+ start(controller) {
107
+ bodyController = controller;
108
+ signal.addEventListener("abort", aborted, { once: true });
109
+ if (signal.aborted)
110
+ aborted();
111
+ },
112
+ async pull(controller) {
113
+ if (terminal)
114
+ return;
115
+ const read = reader.read();
116
+ enteredRead = read;
117
+ try {
118
+ const result = await read;
119
+ if (terminal || signal.aborted)
120
+ return;
121
+ if (result.done) {
122
+ terminal = true;
123
+ controller.close();
124
+ finish();
125
+ }
126
+ else {
127
+ if (limit !== undefined &&
128
+ result.value.byteLength > limit - consumed)
129
+ throw new Error("owned_fetch_body_limit");
130
+ consumed += result.value.byteLength;
131
+ controller.enqueue(result.value);
132
+ }
133
+ }
134
+ catch (error) {
135
+ if (!terminal) {
136
+ terminal = true;
137
+ controller.error(error);
138
+ void cancel(error).catch(() => { });
139
+ }
140
+ }
141
+ finally {
142
+ if (enteredRead === read)
143
+ enteredRead = undefined;
144
+ }
145
+ },
146
+ cancel(reason) {
147
+ return cancel(reason);
148
+ },
149
+ });
150
+ if (signal.aborted) {
151
+ aborted();
152
+ await joined;
153
+ throw signal.reason;
154
+ }
155
+ return new Response(body, {
156
+ status: response.status,
157
+ statusText: response.statusText,
158
+ headers: response.headers,
159
+ });
160
+ })();
161
+ requests.add(pending);
162
+ void pending.then(() => requests.delete(pending), () => requests.delete(pending));
163
+ return pending;
164
+ };
165
+ return {
166
+ fetch: trackedFetch,
167
+ abort(reason) {
168
+ owned.abort(reason);
169
+ },
170
+ async settle() {
171
+ settling = true;
172
+ owned.abort(new Error("owned_fetch_settling"));
173
+ // A fetch that ignores its signal stays owned. Its eventual body is cancelled before return.
174
+ // This join does not pretend a deadline proves that non-cooperative IO has stopped.
175
+ await Promise.allSettled([...requests]);
176
+ await Promise.all([...bodies]);
177
+ parent.removeEventListener("abort", stopped);
178
+ if (cleanupErrors.length > 0)
179
+ throw new AggregateError(cleanupErrors, "owned_fetch_cleanup_failed");
180
+ },
181
+ };
182
+ }
package/dist/query.js CHANGED
@@ -1,12 +1,12 @@
1
1
  // query.ts — `query issues|issue|pulls|pull|projects|cycles|search|changes`: the replica when it is
2
- // fresh, else the API, with the source named on stderr every time. Output shape is the read-model
2
+ // explicitly selected, otherwise the API, with the source named on stderr every time. Output shape is the read-model
3
3
  // view either way. `pull`, `cycles`, `search` and `changes` are API-only (the SDK wraps no view for
4
4
  // them and the read-model package is not a declared dependency of this bundle).
5
- import { flagBool, flagInt, flagString, positionals } from "./args.js";
6
- import { requireConfig, replicaDbPath } from "./config.js";
5
+ import { flagBool, flagInt, flagString, positionals, } from "./args.js";
6
+ import { requireConfig, replicaDbPath, } from "./config.js";
7
7
  import { CliError, UsageError } from "./errors.js";
8
8
  import { apiClient } from "./transport.js";
9
- import { fetchAllPages, fetchPage, rowsOf, truncationNotice } from "./pagination.js";
9
+ import { fetchAllPages, fetchPage, rowsOf, truncationNotice, } from "./pagination.js";
10
10
  import { engineFor, replicaStatus } from "./replica.js";
11
11
  import { loadSdk } from "./sdk.js";
12
12
  export { rowsOf };
@@ -38,7 +38,9 @@ export async function cmdQuery(args, ctx, deps = {}) {
38
38
  if (filters.all && flagString(args, "limit") !== undefined) {
39
39
  ctx.stderr("--all reads the whole scope, so --limit is ignored");
40
40
  }
41
- const status = replicaStatus(ctx, cfg);
41
+ const status = forced === "replica" && REPLICA_CAPABLE.has(sub)
42
+ ? replicaStatus(ctx, cfg)
43
+ : null;
42
44
  let source;
43
45
  let why;
44
46
  if (!REPLICA_CAPABLE.has(sub)) {
@@ -53,19 +55,17 @@ export async function cmdQuery(args, ctx, deps = {}) {
53
55
  source = "api";
54
56
  why = "--all follows the cloud's pages";
55
57
  }
56
- else if (status.verdict === "fresh") {
57
- source = "replica";
58
- why = `cursor ${status.cursor}`;
59
- }
60
58
  else {
61
59
  source = "api";
62
- why = `replica ${status.verdict}`;
60
+ why = "cloud reads by default";
63
61
  }
64
62
  ctx.stderr(source === "replica" ? `source: replica (${why})` : `source: api (${why})`);
65
63
  let result;
66
64
  if (source === "replica") {
67
- if (status.verdict === "absent" || status.verdict === "not-configured") {
68
- throw new UsageError(`--source replica but the replica is ${status.verdict}`);
65
+ if (!status ||
66
+ status.verdict === "absent" ||
67
+ status.verdict === "not-configured") {
68
+ throw new UsageError(`--source replica but the replica is ${status?.verdict ?? "unavailable"}`);
69
69
  }
70
70
  result = await fromReplica(ctx, cfg, sub, rest, filters, deps);
71
71
  }
@@ -82,8 +82,15 @@ export async function cmdQuery(args, ctx, deps = {}) {
82
82
  async function fromReplica(ctx, cfg, sub, rest, f, deps) {
83
83
  const sdk = await loadSdk();
84
84
  const dbPath = replicaDbPath(cfg, ctx.home, ctx.env);
85
- const engine = await engineFor(sdk, dbPath, ctx, { ...deps.engineDeps, readonly: true });
86
- const replica = await sdk.CatalystReplica.openReadOnly({ dbPath, engine, log: () => { } });
85
+ const engine = await engineFor(sdk, dbPath, ctx, {
86
+ ...deps.engineDeps,
87
+ readonly: true,
88
+ });
89
+ const replica = await sdk.CatalystReplica.openReadOnly({
90
+ dbPath,
91
+ engine,
92
+ log: () => { },
93
+ });
87
94
  try {
88
95
  switch (sub) {
89
96
  case "issues": {
@@ -95,7 +102,9 @@ async function fromReplica(ctx, cfg, sub, rest, f, deps) {
95
102
  case "pulls":
96
103
  return replica.pulls({ limit: f.limit });
97
104
  case "projects":
98
- return applyFilters(replica.projects({ limit: Math.max(f.limit, 200) }), { limit: f.limit, state: f.state }).slice(0, f.limit);
105
+ return applyFilters(replica.projects({
106
+ limit: Math.max(f.limit, 200),
107
+ }), { limit: f.limit, state: f.state }).slice(0, f.limit);
99
108
  default:
100
109
  throw new UsageError(`${sub} is not readable from the replica`);
101
110
  }
@@ -106,11 +115,20 @@ async function fromReplica(ctx, cfg, sub, rest, f, deps) {
106
115
  }
107
116
  async function fromApi(ctx, cfg, sub, rest, f, args) {
108
117
  const api = apiClient(cfg, ctx);
109
- const common = { team: f.team, project: f.project, state: f.state, limit: f.limit };
118
+ const common = {
119
+ team: f.team,
120
+ project: f.project,
121
+ state: f.state,
122
+ limit: f.limit,
123
+ };
110
124
  switch (sub) {
111
125
  case "issues": {
112
126
  if (f.all) {
113
- const { rows } = await fetchAllPages(api, "/api/v1/issues", { team: f.team, project: f.project, state: f.state });
127
+ const { rows } = await fetchAllPages(api, "/api/v1/issues", {
128
+ team: f.team,
129
+ project: f.project,
130
+ state: f.state,
131
+ });
114
132
  return applyFilters(rows, f); // no .slice — --all means the whole scope
115
133
  }
116
134
  const page = await fetchPage(api, "/api/v1/issues", common);
@@ -131,7 +149,10 @@ async function fromApi(ctx, cfg, sub, rest, f, args) {
131
149
  const { rows } = await fetchAllPages(api, "/api/v1/pulls", { ticket });
132
150
  return rows;
133
151
  }
134
- const page = await fetchPage(api, "/api/v1/pulls", { limit: f.limit, ticket });
152
+ const page = await fetchPage(api, "/api/v1/pulls", {
153
+ limit: f.limit,
154
+ ticket,
155
+ });
135
156
  const rows = page.rows.slice(0, f.limit);
136
157
  const notice = truncationNotice(page);
137
158
  if (notice)
@@ -144,18 +165,24 @@ async function fromApi(ctx, cfg, sub, rest, f, args) {
144
165
  return res.status === 404 ? null : res.body;
145
166
  }
146
167
  case "projects": {
147
- const res = await api.getJson("/api/v1/projects", { query: { limit: f.limit } });
168
+ const res = await api.getJson("/api/v1/projects", {
169
+ query: { limit: f.limit },
170
+ });
148
171
  return rowsOf(res.body).slice(0, f.limit);
149
172
  }
150
173
  case "cycles": {
151
- const res = await api.getJson("/api/v1/cycles", { query: { team: f.team, limit: f.limit } });
174
+ const res = await api.getJson("/api/v1/cycles", {
175
+ query: { team: f.team, limit: f.limit },
176
+ });
152
177
  return rowsOf(res.body);
153
178
  }
154
179
  case "search": {
155
180
  const q = rest.join(" ").trim();
156
181
  if (!q)
157
182
  throw new UsageError("search needs terms: query search <terms>");
158
- const res = await api.getJson("/api/v1/search", { query: { q, limit: f.limit } });
183
+ const res = await api.getJson("/api/v1/search", {
184
+ query: { q, limit: f.limit },
185
+ });
159
186
  return searchRows(res.body);
160
187
  }
161
188
  case "changes": {
@@ -191,7 +218,11 @@ async function readChanges(api, since, limit) {
191
218
  });
192
219
  const head = res.headers.get(HEAD_SEQ_HEADER);
193
220
  if (res.status !== 409) {
194
- return { since: Number(resolved), head: head === null ? null : Number(head), changes: res.body };
221
+ return {
222
+ since: Number(resolved),
223
+ head: head === null ? null : Number(head),
224
+ changes: res.body,
225
+ };
195
226
  }
196
227
  const where = head === null ? "" : ` The feed's live cursor is ${head}.`;
197
228
  throw new CliError(`cursor ${resolved} is not usable: it is either past the feed's head or no longer in the change log, which keeps only recent changes.${where} Re-run with \`--since head\` to start from now, or with a cursor the feed still holds.`, "changefeed-resync", 1, 409);
@@ -199,7 +230,10 @@ async function readChanges(api, since, limit) {
199
230
  /** The live head, read off any `/changes` response's own header — a 409 carries it too, which is
200
231
  * what makes this work on a tenant whose log has rotated past every cursor the caller could guess. */
201
232
  async function headCursor(api) {
202
- const probe = await api.getNdjson("/api/v1/changes", { query: { since: "0", limit: 1 }, accept: [409] });
233
+ const probe = await api.getNdjson("/api/v1/changes", {
234
+ query: { since: "0", limit: 1 },
235
+ accept: [409],
236
+ });
203
237
  const raw = probe.headers.get(HEAD_SEQ_HEADER);
204
238
  const head = raw === null ? Number.NaN : Number(raw);
205
239
  if (!Number.isFinite(head)) {
@@ -215,11 +249,17 @@ function needArg(rest, usage) {
215
249
  }
216
250
  export function applyFilters(rows, f) {
217
251
  return rows.filter((r) => {
218
- if (f.team && !(String(r.identifier ?? "").toUpperCase().startsWith(`${f.team.toUpperCase()}-`) || r.team_id === f.team || r.team_key === f.team))
252
+ if (f.team &&
253
+ !(String(r.identifier ?? "")
254
+ .toUpperCase()
255
+ .startsWith(`${f.team.toUpperCase()}-`) ||
256
+ r.team_id === f.team ||
257
+ r.team_key === f.team))
219
258
  return false;
220
259
  if (f.project && r.project_id !== f.project)
221
260
  return false;
222
- if (f.state && String(r.state ?? "").toLowerCase() !== f.state.toLowerCase())
261
+ if (f.state &&
262
+ String(r.state ?? "").toLowerCase() !== f.state.toLowerCase())
223
263
  return false;
224
264
  return true;
225
265
  });
@@ -227,7 +267,12 @@ export function applyFilters(rows, f) {
227
267
  /** The hub answers search in four groups (`issues`, `pulls`, `projects`, `initiatives`, the read
228
268
  * model's SearchView). `rowsOf` returns only the first array it finds, so it dropped every match that
229
269
  * was not a ticket. Every group comes back here, flattened, each row tagged with its `kind`. */
230
- const SEARCH_GROUPS = [["issues", "issue"], ["pulls", "pull"], ["projects", "project"], ["initiatives", "initiative"]];
270
+ const SEARCH_GROUPS = [
271
+ ["issues", "issue"],
272
+ ["pulls", "pull"],
273
+ ["projects", "project"],
274
+ ["initiatives", "initiative"],
275
+ ];
231
276
  export function searchRows(body) {
232
277
  if (Array.isArray(body))
233
278
  return body;
@@ -235,7 +280,12 @@ export function searchRows(body) {
235
280
  return [];
236
281
  const b = body;
237
282
  if (SEARCH_GROUPS.some(([group]) => Array.isArray(b[group]))) {
238
- return SEARCH_GROUPS.flatMap(([group, kind]) => Array.isArray(b[group]) ? b[group].map((row) => ({ kind, ...row })) : []);
283
+ return SEARCH_GROUPS.flatMap(([group, kind]) => Array.isArray(b[group])
284
+ ? b[group].map((row) => ({
285
+ kind,
286
+ ...row,
287
+ }))
288
+ : []);
239
289
  }
240
290
  return rowsOf(body);
241
291
  }