@catalyst-cloud/cli 0.14.6 → 0.14.8

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 (79) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +7 -5
  3. package/dist/args.js +68 -18
  4. package/dist/browser.js +37 -3
  5. package/dist/cli.js +58 -12
  6. package/dist/config.js +17 -5
  7. package/dist/consent-browser.js +58 -0
  8. package/dist/daemon-authority-lifetime.js +171 -0
  9. package/dist/daemon-authority.js +216 -0
  10. package/dist/daemon-cache.js +348 -0
  11. package/dist/daemon-store.js +370 -0
  12. package/dist/daemon-supervisor.js +460 -0
  13. package/dist/env/onboard-draft-store.js +250 -0
  14. package/dist/env/onboard-draft.js +33 -0
  15. package/dist/env/selected-import-writer.js +163 -0
  16. package/dist/env/settings-authority.js +205 -0
  17. package/dist/events-cloud-tail.js +146 -0
  18. package/dist/events-cloud.js +619 -0
  19. package/dist/events.js +186 -31
  20. package/dist/oauth.js +129 -34
  21. package/dist/onboard-accounts.js +259 -0
  22. package/dist/onboard-automation-management.js +15 -0
  23. package/dist/onboard-bootstrap.js +162 -0
  24. package/dist/onboard-capabilities.js +15 -7
  25. package/dist/onboard-capacity.js +226 -0
  26. package/dist/onboard-existing.js +106 -32
  27. package/dist/onboard-file-snapshot.js +45 -0
  28. package/dist/onboard-github.js +14 -3
  29. package/dist/onboard-login-candidate.js +261 -0
  30. package/dist/onboard-next.js +139 -0
  31. package/dist/onboard-personal.js +14 -3
  32. package/dist/onboard-projects.js +350 -0
  33. package/dist/onboard-repositories.js +146 -37
  34. package/dist/onboard-runtime.js +108 -13
  35. package/dist/onboard-settings-context.js +141 -0
  36. package/dist/onboard-settings-import.js +127 -0
  37. package/dist/onboard-settings.js +234 -0
  38. package/dist/onboard-ui.js +85 -59
  39. package/dist/onboard-workflow.js +316 -0
  40. package/dist/onboard-workspace.js +14 -3
  41. package/dist/onboard.js +422 -39
  42. package/dist/owned-fetch.js +182 -0
  43. package/dist/pagination.js +13 -1
  44. package/dist/query.js +104 -31
  45. package/dist/ready.js +143 -35
  46. package/dist/replica.js +11 -4
  47. package/dist/repository-id.js +5 -0
  48. package/dist/sdk.js +33 -0
  49. package/dist/watch.js +2 -2
  50. package/package.json +3 -2
  51. package/skills/catalyst-github/SKILL.md +4 -2
  52. package/skills/catalyst-github/references/is-it-mergeable.md +1 -1
  53. package/skills/catalyst-github/references/local-replica.md +78 -0
  54. package/skills/catalyst-github/references/reading-from-the-cloud.md +24 -0
  55. package/skills/catalyst-github/references/what-a-pr-accumulates.md +1 -1
  56. package/skills/catalyst-github/scripts/is-it-mergeable.mjs +210 -45
  57. package/skills/catalyst-github/scripts/read-pr.mjs +51 -15
  58. package/skills/catalyst-linear/SKILL.md +5 -3
  59. package/skills/catalyst-linear/references/local-replica.md +91 -0
  60. package/skills/catalyst-linear/references/reading-a-ticket.md +3 -3
  61. package/skills/catalyst-linear/references/reading-from-the-cloud.md +38 -0
  62. package/skills/catalyst-linear/scripts/read-ticket.mjs +37 -12
  63. package/skills/catalyst-onboard/SKILL.md +6 -4
  64. package/skills/catalyst-onboard/references/local-replica.md +40 -0
  65. package/skills/catalyst-onboard/references/local-sync.md +8 -4
  66. package/skills/catalyst-onboard/scripts/local-sync.mjs +2 -2
  67. package/skills/catalyst-onboard/scripts/where-am-i.mjs +1 -1
  68. package/skills/run-this-project/SKILL.md +1 -1
  69. package/skills/unstick/SKILL.md +1 -1
  70. package/skills/what-needs-me/SKILL.md +1 -1
  71. package/skills/what-this-repo-needs/SKILL.md +1 -1
  72. package/skills/whats-happening/SKILL.md +4 -2
  73. package/skills/whats-happening/references/local-replica.md +51 -0
  74. package/skills/whats-happening/references/reading-from-the-cloud.md +21 -0
  75. package/skills/whats-happening/references/status-reply.md +1 -1
  76. package/skills/whats-happening/scripts/snapshot.mjs +104 -26
  77. package/vendor/settings/index.d.ts +4 -0
  78. package/vendor/settings/index.js +2937 -0
  79. package/vendor/settings/provenance.json +148 -0
@@ -0,0 +1,316 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import { configPathFor, loadConfig, normalizeBaseUrl } from "./config.js";
4
+ import { verifyOnboardRoutes } from "./onboard-capabilities.js";
5
+ import { selectedOnboardTeam } from "./onboard-existing.js";
6
+ import { ownedFetch } from "./owned-fetch.js";
7
+ import { loadHttpSdk } from "./sdk.js";
8
+ import { fetchMe } from "./transport.js";
9
+ const path = "/api/v1/agent/team-workflow";
10
+ const slots = [
11
+ "dispatch",
12
+ "intake",
13
+ "research",
14
+ "plan",
15
+ "implement",
16
+ "remediate",
17
+ "verify",
18
+ "review",
19
+ "pr",
20
+ "done",
21
+ "canceled",
22
+ ];
23
+ const requiredTypes = {
24
+ dispatch: ["unstarted", "backlog"],
25
+ intake: ["unstarted", "backlog"],
26
+ pr: ["started"],
27
+ done: ["completed"],
28
+ canceled: ["canceled"],
29
+ };
30
+ const waiting = (reason) => ({
31
+ state: "waiting",
32
+ reason,
33
+ });
34
+ const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
35
+ ? value
36
+ : null;
37
+ const id = (value) => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(value);
38
+ const integer = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
39
+ /** Existing mappings can satisfy this step by observation. This never means a preview was applied. */
40
+ export function observeOnboardWorkflow(value, team, now) {
41
+ const row = object(value), config = object(row?.config), readiness = object(row?.readiness);
42
+ if (!row ||
43
+ !config ||
44
+ !readiness ||
45
+ config.teamId !== team ||
46
+ readiness.teamId !== team ||
47
+ typeof config.mode !== "string" ||
48
+ !["mapped-existing", "adopted-recommended", "mixed"].includes(config.mode) ||
49
+ (config.gitAutomation !== "off" && config.gitAutomation !== "managed") ||
50
+ !integer(config.workflowRev) ||
51
+ !integer(readiness.workflowRev) ||
52
+ typeof readiness.status !== "string" ||
53
+ !["ready", "degraded", "blocked"].includes(readiness.status) ||
54
+ !integer(readiness.checkedAt) ||
55
+ readiness.checkedAt > now ||
56
+ now - readiness.checkedAt >= 300_000 ||
57
+ (row.stageSource !== "linear" && row.stageSource !== "mirror") ||
58
+ typeof row.mappingHash !== "string" ||
59
+ !/^[a-f0-9]{64}$/.test(row.mappingHash) ||
60
+ !Array.isArray(row.rows) ||
61
+ row.rows.length > slots.length ||
62
+ !Array.isArray(row.stages) ||
63
+ row.stages.length > 1_000 ||
64
+ !Array.isArray(readiness.checks) ||
65
+ readiness.checks.length > 100)
66
+ return null;
67
+ const stages = new Map();
68
+ for (const candidate of row.stages) {
69
+ const stage = object(candidate);
70
+ if (!stage ||
71
+ !id(stage.id) ||
72
+ stages.has(stage.id) ||
73
+ typeof stage.type !== "string" ||
74
+ ![
75
+ "triage",
76
+ "backlog",
77
+ "unstarted",
78
+ "started",
79
+ "completed",
80
+ "canceled",
81
+ ].includes(stage.type))
82
+ return null;
83
+ stages.set(stage.id, stage.type);
84
+ }
85
+ const mapping = new Map();
86
+ for (const candidate of row.rows) {
87
+ const item = object(candidate);
88
+ if (!item ||
89
+ typeof item.slot !== "string" ||
90
+ !slots.includes(item.slot) ||
91
+ mapping.has(item.slot) ||
92
+ (item.linearStateId !== null && !id(item.linearStateId)) ||
93
+ (item.source !== undefined &&
94
+ item.source !== "created" &&
95
+ item.source !== "matched" &&
96
+ item.source !== "chosen") ||
97
+ (item.linearStateId !== null &&
98
+ (item.stateStillExists !== true || !stages.has(item.linearStateId))))
99
+ return null;
100
+ mapping.set(item.slot, item.linearStateId);
101
+ }
102
+ for (const [slot, types] of Object.entries(requiredTypes)) {
103
+ const target = mapping.get(slot);
104
+ if (!target ||
105
+ !types.includes(stages.get(target) ?? ""))
106
+ return null;
107
+ }
108
+ const checks = new Map();
109
+ for (const candidate of readiness.checks) {
110
+ const check = object(candidate);
111
+ if (!check ||
112
+ typeof check.id !== "string" ||
113
+ checks.has(check.id) ||
114
+ (check.state !== "pass" &&
115
+ check.state !== "fail" &&
116
+ check.state !== "unknown"))
117
+ return null;
118
+ checks.set(check.id, check.state);
119
+ }
120
+ // Adoption does not establish coding accounts, automation rule safety or first-ticket readiness.
121
+ if ([
122
+ "team_visible",
123
+ "mapped_states_exist",
124
+ "mapping_total",
125
+ "types_compatible",
126
+ "labels_present",
127
+ ].some((check) => checks.get(check) !== "pass"))
128
+ return null;
129
+ return {
130
+ mappingHash: row.mappingHash,
131
+ mappingRevision: config.workflowRev,
132
+ readinessRevision: readiness.workflowRev,
133
+ checkedAt: readiness.checkedAt,
134
+ mappedSlots: [...mapping.values()].filter((value) => value !== null).length,
135
+ };
136
+ }
137
+ /** HTTP only, without rotating credentials or calling adopt/apply. Every entered fetch/body joins. */
138
+ export function onboardWorkflowVerificationAdapter(input = {}) {
139
+ return {
140
+ check: async (ctx, journal, external) => {
141
+ let cfg;
142
+ try {
143
+ cfg = loadConfig(ctx.home);
144
+ }
145
+ catch {
146
+ return waiting("workflow_identity_unverified");
147
+ }
148
+ const team = selectedOnboardTeam(journal);
149
+ if (!team ||
150
+ !cfg?.user ||
151
+ (cfg.user.role !== "owner" && cfg.user.role !== "admin") ||
152
+ cfg.account !== (journal.account ?? journal.tenant) ||
153
+ cfg.user.id !== journal.membershipId ||
154
+ !journal.baseUrl ||
155
+ normalizeBaseUrl(cfg.baseUrl) !== normalizeBaseUrl(journal.baseUrl))
156
+ return waiting("workflow_identity_unverified");
157
+ const origin = normalizeBaseUrl(cfg.baseUrl), person = cfg.user.id, role = cfg.user.role;
158
+ let url;
159
+ try {
160
+ url = new URL(origin);
161
+ }
162
+ catch {
163
+ return waiting("workflow_identity_unverified");
164
+ }
165
+ if (url.protocol !== "https:" ||
166
+ url.origin !== origin ||
167
+ url.username ||
168
+ url.password)
169
+ return waiting("workflow_identity_unverified");
170
+ const credential = cfg.key ||
171
+ (cfg.auth &&
172
+ Date.parse(cfg.auth.expiresAt) - ctx.now().getTime() > 30_000
173
+ ? cfg.auth.accessToken
174
+ : undefined);
175
+ if (!credential)
176
+ return waiting("workflow_login_refresh_required");
177
+ const configWitness = () => createHash("sha256")
178
+ .update(readFileSync(configPathFor(ctx.home)))
179
+ .digest("hex");
180
+ let before;
181
+ try {
182
+ before = configWitness();
183
+ }
184
+ catch {
185
+ return waiting("workflow_identity_unverified");
186
+ }
187
+ const current = () => {
188
+ try {
189
+ const now = loadConfig(ctx.home);
190
+ return (now?.user?.id === person &&
191
+ now.user.role === role &&
192
+ now.account === cfg.account &&
193
+ normalizeBaseUrl(now.baseUrl) === origin &&
194
+ configWitness() === before &&
195
+ selectedOnboardTeam(journal) === team &&
196
+ (!!cfg.key ||
197
+ (!!cfg.auth &&
198
+ Date.parse(cfg.auth.expiresAt) > ctx.now().getTime())));
199
+ }
200
+ catch {
201
+ return false;
202
+ }
203
+ };
204
+ const deadline = new AbortController();
205
+ const timer = setTimeout(() => deadline.abort(), 30_000);
206
+ const signal = external
207
+ ? AbortSignal.any([external, deadline.signal])
208
+ : deadline.signal;
209
+ const owned = ownedFetch(ctx.fetch, signal, { maxBodyBytes: 524_288 });
210
+ const read = (request, init) => {
211
+ if (!current() || signal.aborted)
212
+ return Promise.reject(new Error("workflow_read_stopped"));
213
+ const requested = new URL(request instanceof Request ? request.url : String(request));
214
+ const method = init?.method ?? (request instanceof Request ? request.method : "GET");
215
+ if (method !== "GET" ||
216
+ requested.origin !== origin ||
217
+ requested.username ||
218
+ requested.password ||
219
+ requested.hash ||
220
+ !["/api/v1/me", "/api/v1/agent/contract", path].includes(requested.pathname) ||
221
+ (requested.pathname === path
222
+ ? requested.search !== `?team=${encodeURIComponent(team)}`
223
+ : requested.search !== ""))
224
+ return Promise.reject(new Error("workflow_read_scope"));
225
+ return owned.fetch(request, { ...init, signal, redirect: "error" });
226
+ };
227
+ const live = async () => {
228
+ const me = await fetchMe(origin, credential, read);
229
+ return (current() &&
230
+ !signal.aborted &&
231
+ me.account === cfg.account &&
232
+ me.user?.id === person &&
233
+ me.user.role === role);
234
+ };
235
+ const result = await (async () => {
236
+ try {
237
+ if (!(await live()))
238
+ return waiting("workflow_identity_unverified");
239
+ const support = await verifyOnboardRoutes({ ...ctx, fetch: read }, journal, [{ method: "GET", path }], signal);
240
+ if ("reason" in support)
241
+ return waiting(support.reason);
242
+ if (!current() || signal.aborted)
243
+ return waiting("workflow_identity_unverified");
244
+ const sdk = await loadHttpSdk();
245
+ if (!current() || signal.aborted)
246
+ return waiting("workflow_identity_unverified");
247
+ const client = sdk.createTenantClient({
248
+ baseUrl: origin,
249
+ key: credential,
250
+ fetch: read,
251
+ timeoutMs: 30_000,
252
+ now: () => ctx.now().getTime(),
253
+ });
254
+ const first = await client.teamWorkflow.get(team);
255
+ const observed = first.outcome === "ok"
256
+ ? observeOnboardWorkflow(first, team, ctx.now().getTime())
257
+ : null;
258
+ if (!observed || !current() || signal.aborted) {
259
+ input.message?.("Workflow adoption is waiting for a verified mapping or full-plan server support. Setup made no workflow change.");
260
+ return waiting("workflow_mapping_unverified");
261
+ }
262
+ if (!(await live()))
263
+ return waiting("workflow_identity_unverified");
264
+ const last = await client.teamWorkflow.get(team);
265
+ const verified = last.outcome === "ok"
266
+ ? observeOnboardWorkflow(last, team, ctx.now().getTime())
267
+ : null;
268
+ if (!verified ||
269
+ verified.mappingHash !== observed.mappingHash ||
270
+ verified.mappingRevision !== observed.mappingRevision ||
271
+ verified.readinessRevision !== observed.readinessRevision ||
272
+ !current() ||
273
+ signal.aborted)
274
+ return waiting("workflow_mapping_changed");
275
+ if (!(await live()))
276
+ return waiting("workflow_identity_unverified");
277
+ if (!current() || signal.aborted)
278
+ return waiting("workflow_identity_unverified");
279
+ return {
280
+ state: "done",
281
+ evidence: {
282
+ team,
283
+ revision: verified.readinessRevision,
284
+ mappingRevision: verified.mappingRevision,
285
+ checkedAt: verified.checkedAt,
286
+ mappingHash: verified.mappingHash,
287
+ count: verified.mappedSlots,
288
+ },
289
+ };
290
+ }
291
+ catch {
292
+ return waiting(external?.aborted ? "interrupted" : "workflow_unavailable");
293
+ }
294
+ finally {
295
+ try {
296
+ owned.abort(new Error("workflow_read_complete"));
297
+ await owned.settle();
298
+ }
299
+ finally {
300
+ clearTimeout(timer);
301
+ }
302
+ }
303
+ })();
304
+ if (!current())
305
+ return waiting("workflow_identity_unverified");
306
+ if (signal.aborted)
307
+ return waiting(external?.aborted ? "interrupted" : "workflow_unavailable");
308
+ if (result.state === "done") {
309
+ input.message?.("The selected team's existing workflow mapping and labels are verified. Setup made no workflow change.");
310
+ if (!current() || signal.aborted)
311
+ return waiting("workflow_identity_unverified");
312
+ }
313
+ return result;
314
+ },
315
+ };
316
+ }
@@ -1,3 +1,4 @@
1
+ import { finishOnTheWeb } from "./consent-browser.js";
1
2
  import { loadConfig, normalizeBaseUrl } from "./config.js";
2
3
  import { pollConsent } from "./onboard-consent.js";
3
4
  const STATUS = "/api/v1/me/connections/linear/workspace";
@@ -252,11 +253,15 @@ export function linearWorkspaceAdapter(options) {
252
253
  return { state: "refused", reason: "workspace_admin_required" };
253
254
  ctx.stderr("Approve the organization's Linear connection in your browser. Your personal approval follows.");
254
255
  // The signed URL is transient credential material: only the browser receives it.
256
+ let browserUnavailable = false;
255
257
  try {
256
- options.openBrowser(url);
258
+ await options.openBrowser(url, signal);
257
259
  }
258
260
  catch {
259
- return waiting("workspace_browser_unavailable");
261
+ if (signal?.aborted)
262
+ return waiting("interrupted");
263
+ browserUnavailable = true;
264
+ ctx.stderr(finishOnTheWeb(current.baseUrl, "connections", "connect Linear"));
260
265
  }
261
266
  let latest = waiting("workspace_status_unavailable");
262
267
  const run = () => pollConsent({
@@ -271,6 +276,8 @@ export function linearWorkspaceAdapter(options) {
271
276
  outcome: "waiting",
272
277
  reason: "cloud_capability_unavailable",
273
278
  };
279
+ if (latest.reason === "workspace_login_refresh_required")
280
+ return { outcome: "waiting", reason: latest.reason };
274
281
  if (latest.state === "done")
275
282
  return { outcome: "connected" };
276
283
  if (latest.state === "refused" || latest.state === "failed")
@@ -283,7 +290,11 @@ export function linearWorkspaceAdapter(options) {
283
290
  const result = options.wait
284
291
  ? await options.wait("Waiting for workspace approval", run)
285
292
  : await run();
286
- return result.state === "done" ? latest : result;
293
+ if (result.state === "done")
294
+ return latest;
295
+ return browserUnavailable && result.reason === "consent_timeout"
296
+ ? waiting("workspace_browser_unavailable")
297
+ : result;
287
298
  },
288
299
  };
289
300
  }