@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,350 @@
1
+ import { verifyOnboardRoutes } from "./onboard-capabilities.js";
2
+ import { loadConfig, normalizeBaseUrl } from "./config.js";
3
+ import { readExistingOnboardJson, selectedOnboardTeam, } from "./onboard-existing.js";
4
+ import { existingRepositoryAdapter, } from "./onboard-repositories.js";
5
+ const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
6
+ ? value
7
+ : null;
8
+ const part = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,99}$/;
9
+ const key = (row) => `${row.owner}/${row.name}`.toLowerCase();
10
+ const waiting = (reason) => ({
11
+ state: "waiting",
12
+ reason,
13
+ });
14
+ function repository(value) {
15
+ const row = object(value);
16
+ return row &&
17
+ typeof row.owner === "string" &&
18
+ part.test(row.owner) &&
19
+ typeof row.name === "string" &&
20
+ part.test(row.name)
21
+ ? { owner: row.owner, name: row.name }
22
+ : null;
23
+ }
24
+ function pendingSelection(journal) {
25
+ const step = journal.steps.find((row) => row.id === "github.repos" && row.state === "done");
26
+ const raw = step?.evidence?.repository;
27
+ if (typeof raw !== "string" || raw.length > 64_000)
28
+ return null;
29
+ try {
30
+ const values = JSON.parse(raw);
31
+ if (!Array.isArray(values) ||
32
+ values.length !== 1 ||
33
+ object(values[0])?.teamId !== selectedOnboardTeam(journal))
34
+ return null;
35
+ return repository(values[0]);
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ async function options(ctx, journal, signal) {
42
+ const cfg = loadConfig(ctx.home);
43
+ if (!cfg?.user ||
44
+ !["owner", "admin"].includes(cfg.user.role) ||
45
+ cfg.account !== journal.account ||
46
+ cfg.user.id !== journal.membershipId ||
47
+ !journal.baseUrl ||
48
+ normalizeBaseUrl(cfg.baseUrl) !== normalizeBaseUrl(journal.baseUrl))
49
+ return { reason: "project_identity_unverified" };
50
+ const personId = cfg.user.id;
51
+ const teamId = selectedOnboardTeam(journal);
52
+ if (!teamId)
53
+ return { reason: "team_selection_unverified" };
54
+ const support = await verifyOnboardRoutes(ctx, journal, [{ method: "GET", path: "/api/v1/me/repositories/options" }], signal);
55
+ if ("reason" in support)
56
+ return support;
57
+ const sameIdentity = () => {
58
+ const current = loadConfig(ctx.home);
59
+ return (current?.user &&
60
+ ["owner", "admin"].includes(current.user.role) &&
61
+ current.account === cfg.account &&
62
+ current.user.id === personId &&
63
+ normalizeBaseUrl(current.baseUrl) === normalizeBaseUrl(cfg.baseUrl));
64
+ };
65
+ // The read helper loads its own credential. Reject a switch before that read and again
66
+ // after its body settles, before any provider names become receipt evidence.
67
+ if (!sameIdentity())
68
+ return { reason: "project_identity_unverified" };
69
+ const read = await readExistingOnboardJson(ctx, "/api/v1/me/repositories/options", signal);
70
+ if (!sameIdentity())
71
+ return { reason: "project_identity_unverified" };
72
+ if ("reason" in read)
73
+ return { reason: "project_options_unavailable" };
74
+ const body = object(read.body), linear = object(body?.linear), github = object(body?.github);
75
+ if (!linear ||
76
+ !github ||
77
+ linear.connected !== true ||
78
+ github.connected !== true ||
79
+ linear.error !== null ||
80
+ github.error !== null)
81
+ return { reason: "project_provider_inventory_unavailable" };
82
+ if (!Array.isArray(linear.teams) ||
83
+ linear.teams.length > 1_000 ||
84
+ !Array.isArray(github.repositories) ||
85
+ github.repositories.length > 10_000 ||
86
+ typeof github.truncated !== "boolean" ||
87
+ !Array.isArray(body?.taken) ||
88
+ body.taken.length > 10_000)
89
+ return { reason: "project_options_unverified" };
90
+ const teams = linear.teams.filter((value) => object(value)?.id === teamId);
91
+ const team = object(teams[0]);
92
+ if (teams.length !== 1 ||
93
+ typeof team?.key !== "string" ||
94
+ !/^[A-Za-z0-9_-]{1,64}$/.test(team.key))
95
+ return { reason: "team_selection_unverified" };
96
+ const rows = [], seen = new Set();
97
+ for (const value of github.repositories) {
98
+ const row = repository(value);
99
+ if (!row || seen.has(key(row)))
100
+ return { reason: "project_options_unverified" };
101
+ seen.add(key(row));
102
+ rows.push(row);
103
+ }
104
+ const taken = [];
105
+ for (const value of body.taken) {
106
+ const row = object(value);
107
+ const repo = repository({
108
+ owner: row?.githubRepoOwner,
109
+ name: row?.githubRepoName,
110
+ });
111
+ if (!row ||
112
+ !repo ||
113
+ typeof row.linearTeamId !== "string" ||
114
+ typeof row.status !== "string" ||
115
+ !["active", "archived"].includes(row.status))
116
+ return { reason: "project_options_unverified" };
117
+ taken.push({
118
+ teamId: row.linearTeamId,
119
+ repo,
120
+ active: row.status === "active",
121
+ });
122
+ }
123
+ return { rows, taken, teamKey: team.key, truncated: github.truncated };
124
+ }
125
+ /** Q3 records a choice. It does not report an unregistered repository as a cloud binding. */
126
+ export function firstProjectAdapters(args, input = {}) {
127
+ const existing = existingRepositoryAdapter(args, input.chooseExisting);
128
+ let chosen = null;
129
+ const selected = (journal) => {
130
+ const explicit = args.flags.repo;
131
+ if (explicit !== undefined) {
132
+ const values = Array.isArray(explicit) ? explicit : [explicit];
133
+ if (values.length !== 1 || typeof values[0] !== "string")
134
+ return null;
135
+ const parts = values[0].split("/");
136
+ return parts.length === 2
137
+ ? repository({ owner: parts[0], name: parts[1] })
138
+ : null;
139
+ }
140
+ return chosen ?? pendingSelection(journal);
141
+ };
142
+ const selectionResult = (row, journal) => ({
143
+ state: "done",
144
+ evidence: {
145
+ repository: JSON.stringify([
146
+ { ...row, teamId: selectedOnboardTeam(journal) },
147
+ ]),
148
+ count: 1,
149
+ },
150
+ });
151
+ const binding = async (ctx, journal, signal) => {
152
+ const row = selected(journal);
153
+ if (!row)
154
+ return waiting("repository_selection_unverified");
155
+ return existingRepositoryAdapter({
156
+ ...args,
157
+ flags: { ...args.flags, repo: `${row.owner}/${row.name}` },
158
+ }).check(ctx, journal, signal);
159
+ };
160
+ const projectCheck = async (ctx, journal, signal) => {
161
+ const known = await existing.check(ctx, journal, signal);
162
+ if (known.state === "done")
163
+ return known;
164
+ const live = await binding(ctx, journal, signal);
165
+ if (live.state === "done")
166
+ return live;
167
+ if (live.reason !== "repository_inventory_empty")
168
+ return live;
169
+ const row = selected(journal);
170
+ if (!row)
171
+ return waiting("repository_selection_unverified");
172
+ const list = await options(ctx, journal, signal);
173
+ if ("reason" in list)
174
+ return waiting(list.reason);
175
+ const held = list.taken.filter((value) => value.teamId === selectedOnboardTeam(journal) ||
176
+ key(value.repo) === key(row));
177
+ if (held.length)
178
+ return waiting(held.length === 1 &&
179
+ held[0].active &&
180
+ held[0].teamId === selectedOnboardTeam(journal) &&
181
+ key(held[0].repo) === key(row)
182
+ ? "project_registration_visibility_pending"
183
+ : "project_binding_conflict");
184
+ return list.rows.some((value) => key(value) === key(row))
185
+ ? { state: "pending" }
186
+ : waiting("repository_selection_unverified");
187
+ };
188
+ return {
189
+ "github.repos": {
190
+ check: async (ctx, journal, signal) => {
191
+ const live = await existing.check(ctx, journal, signal);
192
+ if (live.state === "done")
193
+ return live;
194
+ // A names-only Q3 receipt remains the selected choice when registration later becomes
195
+ // visible. Resolve its actual ID without asking the registered-repository picker again.
196
+ if (selected(journal)) {
197
+ const registered = await binding(ctx, journal, signal);
198
+ if (registered.state === "done" ||
199
+ registered.reason !== "repository_inventory_empty")
200
+ return registered;
201
+ }
202
+ if (live.reason !== "repository_inventory_empty")
203
+ return live;
204
+ const list = await options(ctx, journal, signal);
205
+ if ("reason" in list)
206
+ return waiting(list.reason);
207
+ const row = selected(journal);
208
+ if (row)
209
+ return list.rows.some((value) => key(value) === key(row))
210
+ ? selectionResult(row, journal)
211
+ : waiting("repository_selection_unverified");
212
+ return input.chooseFirst && list.rows.length
213
+ ? { state: "pending" }
214
+ : waiting("repository_choice_required");
215
+ },
216
+ act: async (ctx, journal, signal) => {
217
+ const live = await existing.check(ctx, journal, signal);
218
+ if (live.reason !== "repository_inventory_empty")
219
+ return existing.act ? existing.act(ctx, journal, signal) : live;
220
+ const list = await options(ctx, journal, signal);
221
+ if ("reason" in list)
222
+ return waiting(list.reason);
223
+ if (!input.chooseFirst || !list.rows.length)
224
+ return waiting("repository_choice_required");
225
+ if (list.truncated)
226
+ input.message?.("GitHub returned part of its repository list. Choose a displayed repository, or stop and adjust the App access before resuming.");
227
+ let remove = () => { };
228
+ const answer = await new Promise((resolve, reject) => {
229
+ const stopped = () => resolve(null);
230
+ if (signal?.aborted) {
231
+ stopped();
232
+ return;
233
+ }
234
+ signal?.addEventListener("abort", stopped, { once: true });
235
+ remove = () => signal?.removeEventListener("abort", stopped);
236
+ Promise.resolve()
237
+ .then(() => signal?.aborted
238
+ ? null
239
+ : input.chooseFirst(list.rows.map((row) => ({ ...row }))))
240
+ .then(resolve, reject);
241
+ }).finally(() => remove());
242
+ if (signal?.aborted || answer === null)
243
+ return waiting("interrupted");
244
+ chosen =
245
+ list.rows.find((row) => key(row) === answer.toLowerCase()) ?? null;
246
+ if (!chosen)
247
+ return waiting("repository_selection_unverified");
248
+ const fresh = await options(ctx, journal, signal);
249
+ return "reason" in fresh
250
+ ? waiting(fresh.reason)
251
+ : fresh.rows.some((row) => key(row) === key(chosen))
252
+ ? selectionResult(chosen, journal)
253
+ : waiting("repository_selection_unverified");
254
+ },
255
+ },
256
+ projects: {
257
+ check: projectCheck,
258
+ act: async (ctx, journal, signal) => {
259
+ const before = await projectCheck(ctx, journal, signal);
260
+ if (before.state !== "pending")
261
+ return before;
262
+ const row = selected(journal);
263
+ if (!row)
264
+ return waiting("repository_selection_unverified");
265
+ const list = await options(ctx, journal, signal);
266
+ if ("reason" in list)
267
+ return waiting(list.reason);
268
+ // Recheck after the second provider read before any mutation; an archived holder is a conflict.
269
+ if (!list.rows.some((value) => key(value) === key(row)) ||
270
+ list.taken.some((value) => value.teamId === selectedOnboardTeam(journal) ||
271
+ key(value.repo) === key(row)))
272
+ return projectCheck(ctx, journal, signal);
273
+ const result = await createFirstProject(ctx, journal, row, list.teamKey, signal);
274
+ if (result)
275
+ return waiting(result);
276
+ return projectCheck(ctx, journal, signal);
277
+ },
278
+ },
279
+ };
280
+ }
281
+ async function createFirstProject(ctx, journal, row, teamKey, external) {
282
+ const support = await verifyOnboardRoutes(ctx, journal, [{ method: "POST", path: "/api/v1/me/repositories" }], external);
283
+ if ("reason" in support)
284
+ return support.reason;
285
+ const cfg = loadConfig(ctx.home);
286
+ if (!cfg?.user ||
287
+ !["admin", "owner"].includes(cfg.user.role) ||
288
+ cfg.account !== journal.account ||
289
+ cfg.user.id !== journal.membershipId ||
290
+ !journal.baseUrl ||
291
+ normalizeBaseUrl(cfg.baseUrl) !== normalizeBaseUrl(journal.baseUrl))
292
+ return "project_identity_unverified";
293
+ const expiry = cfg.auth
294
+ ? Date.parse(cfg.auth.expiresAt) - ctx.now().getTime()
295
+ : 0;
296
+ const bearer = cfg.key ||
297
+ (cfg.auth && Number.isFinite(expiry) && expiry > 30_000
298
+ ? cfg.auth.accessToken
299
+ : undefined);
300
+ if (!bearer)
301
+ return "project_login_refresh_required";
302
+ const owned = new AbortController(), signal = external
303
+ ? AbortSignal.any([external, owned.signal])
304
+ : owned.signal;
305
+ const timer = setTimeout(() => owned.abort(), 30_000);
306
+ let remove = () => { };
307
+ try {
308
+ return await new Promise((resolve) => {
309
+ const stopped = () => resolve(external?.aborted ? "interrupted" : "project_create_unverified");
310
+ if (signal.aborted) {
311
+ stopped();
312
+ return;
313
+ }
314
+ signal.addEventListener("abort", stopped, { once: true });
315
+ remove = () => signal.removeEventListener("abort", stopped);
316
+ Promise.resolve()
317
+ .then(async () => {
318
+ if (signal.aborted)
319
+ return "interrupted";
320
+ const response = await ctx.fetch(`${normalizeBaseUrl(cfg.baseUrl)}/api/v1/me/repositories`, {
321
+ method: "POST",
322
+ redirect: "error",
323
+ signal,
324
+ headers: {
325
+ authorization: `Bearer ${bearer}`,
326
+ "content-type": "application/json",
327
+ accept: "application/json",
328
+ },
329
+ body: JSON.stringify({
330
+ name: `${row.owner}/${row.name}`.slice(0, 80),
331
+ linearTeamId: selectedOnboardTeam(journal),
332
+ linearTeamKey: teamKey,
333
+ githubRepoOwner: row.owner,
334
+ githubRepoName: row.name,
335
+ }),
336
+ });
337
+ // A response is not readiness evidence. The next check reads the binding and contract again.
338
+ return response.status === 201 || response.status === 409
339
+ ? null
340
+ : "project_create_unverified";
341
+ })
342
+ .then((result) => (signal.aborted ? stopped() : resolve(result)), () => signal.aborted ? stopped() : resolve("project_create_unverified"));
343
+ });
344
+ }
345
+ finally {
346
+ clearTimeout(timer);
347
+ remove();
348
+ owned.abort();
349
+ }
350
+ }
@@ -1,44 +1,105 @@
1
- import { loadConfig, normalizeBaseUrl, readManifest } from "./config.js";
1
+ import { isRepositoryId } from "./repository-id.js";
2
+ import { loadConfig, normalizeBaseUrl, readManifest, } from "./config.js";
2
3
  import { contractVersionInRange } from "./contract.js";
3
- import { readExistingOnboardJson, selectedOnboardTeam } from "./onboard-existing.js";
4
+ import { readExistingOnboardJson, selectedOnboardTeam, } from "./onboard-existing.js";
4
5
  const part = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,99}$/;
5
6
  const id = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
6
- const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
7
+ const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
8
+ ? value
9
+ : null;
7
10
  const key = (row) => `${row.owner}/${row.name}`.toLowerCase();
8
- const waiting = (reason) => ({ state: "waiting", reason });
11
+ const waiting = (reason) => ({
12
+ state: "waiting",
13
+ reason,
14
+ });
9
15
  const valid = (value) => {
10
16
  const row = object(value);
11
- return !!row && typeof row.teamId === "string" && id.test(row.teamId) && typeof row.repoId === "string" && id.test(row.repoId) &&
12
- typeof row.owner === "string" && part.test(row.owner) && typeof row.name === "string" && part.test(row.name);
17
+ return (!!row &&
18
+ typeof row.teamId === "string" &&
19
+ id.test(row.teamId) &&
20
+ isRepositoryId(row.repoId) &&
21
+ typeof row.owner === "string" &&
22
+ part.test(row.owner) &&
23
+ typeof row.name === "string" &&
24
+ part.test(row.name));
13
25
  };
14
26
  /** Names/IDs only. A saved selection never proves current access or repository readiness. */
15
27
  export function selectedOnboardRepositories(journal) {
16
- const step = journal?.steps.find(row => row.id === "github.repos" && row.state === "done");
17
- const receipt = step?.evidence?.repository;
28
+ const selection = journal?.steps.find((row) => row.id === "github.repos" && row.state === "done");
29
+ const registered = journal?.steps.find((row) => row.id === "projects" && row.state === "done");
30
+ const receipt = selection?.evidence?.repository;
18
31
  if (typeof receipt !== "string" || receipt.length > 64_000)
19
32
  return [];
20
33
  try {
21
34
  const rows = JSON.parse(receipt);
22
- if (!Array.isArray(rows) || !rows.length || rows.length > 100 || !rows.every(valid))
35
+ if (!Array.isArray(rows) || !rows.length || rows.length > 100)
23
36
  return [];
24
- if (new Set(rows.map(key)).size !== rows.length)
37
+ if (rows.every(valid)) {
38
+ if (new Set(rows.map(key)).size !== rows.length)
39
+ return [];
40
+ return rows.map((row) => ({
41
+ teamId: row.teamId,
42
+ owner: row.owner,
43
+ name: row.name,
44
+ repoId: row.repoId,
45
+ }));
46
+ }
47
+ // A fresh Q3 choice has names only. It may use later verified registration evidence only
48
+ // when the exact selected team/repository matches, never an earlier project's selection.
49
+ if (rows.length !== 1)
50
+ return [];
51
+ const selected = object(rows[0]);
52
+ if (!selected ||
53
+ selected.repoId !== undefined ||
54
+ typeof selected.teamId !== "string" ||
55
+ !id.test(selected.teamId) ||
56
+ typeof selected.owner !== "string" ||
57
+ !part.test(selected.owner) ||
58
+ typeof selected.name !== "string" ||
59
+ !part.test(selected.name))
60
+ return [];
61
+ const evidence = registered?.evidence?.repository;
62
+ if (typeof evidence !== "string" || evidence.length > 64_000)
25
63
  return [];
26
- return rows.map(row => ({ teamId: row.teamId, owner: row.owner, name: row.name, repoId: row.repoId }));
64
+ const registeredRows = JSON.parse(evidence);
65
+ if (!Array.isArray(registeredRows) ||
66
+ registeredRows.length !== 1 ||
67
+ !registeredRows.every(valid))
68
+ return [];
69
+ const live = registeredRows[0];
70
+ return live.teamId === selected.teamId &&
71
+ key(live) === key({ owner: selected.owner, name: selected.name })
72
+ ? [{ ...live }]
73
+ : [];
27
74
  }
28
75
  catch {
29
76
  return [];
30
77
  }
31
78
  }
32
79
  /** The personal ACL inventory chooses bindings; a fresh contract supplies IDs only. No cache read. */
33
- async function inventory(ctx, journal, signal) {
80
+ export async function readOnboardRepositoryInventory(ctx, journal, signal) {
34
81
  const cfg = loadConfig(ctx.home);
35
- if (!cfg?.user || !journal.account || cfg.account !== journal.account || journal.membershipId !== cfg.user.id ||
36
- !journal.baseUrl || normalizeBaseUrl(journal.baseUrl) !== normalizeBaseUrl(cfg.baseUrl))
82
+ if (!cfg?.user ||
83
+ !journal.account ||
84
+ cfg.account !== journal.account ||
85
+ journal.membershipId !== cfg.user.id ||
86
+ !journal.baseUrl ||
87
+ normalizeBaseUrl(journal.baseUrl) !== normalizeBaseUrl(cfg.baseUrl))
37
88
  return { reason: "repository_identity_unverified" };
89
+ const personId = cfg.user.id;
90
+ const sameIdentity = () => {
91
+ const current = loadConfig(ctx.home);
92
+ return (current?.user &&
93
+ current.account === cfg.account &&
94
+ current.user.id === personId &&
95
+ normalizeBaseUrl(current.baseUrl) === normalizeBaseUrl(cfg.baseUrl));
96
+ };
38
97
  const teamId = selectedOnboardTeam(journal);
39
98
  if (!teamId)
40
99
  return { reason: "team_selection_unverified" };
41
100
  const read = await readExistingOnboardJson(ctx, "/api/v1/repos", signal);
101
+ if (!sameIdentity())
102
+ return { reason: "repository_identity_unverified" };
42
103
  if ("reason" in read)
43
104
  return { reason: "repository_read_unavailable" };
44
105
  const body = object(read.body);
@@ -48,8 +109,13 @@ async function inventory(ctx, journal, signal) {
48
109
  const seen = new Set();
49
110
  for (const value of body.repos) {
50
111
  const row = object(value);
51
- if (!row || typeof row.teamId !== "string" || !id.test(row.teamId) || typeof row.owner !== "string" || !part.test(row.owner) ||
52
- typeof row.name !== "string" || !part.test(row.name))
112
+ if (!row ||
113
+ typeof row.teamId !== "string" ||
114
+ !id.test(row.teamId) ||
115
+ typeof row.owner !== "string" ||
116
+ !part.test(row.owner) ||
117
+ typeof row.name !== "string" ||
118
+ !part.test(row.name))
53
119
  return { reason: "repository_inventory_shape" };
54
120
  if (row.teamId !== teamId)
55
121
  continue;
@@ -59,24 +125,36 @@ async function inventory(ctx, journal, signal) {
59
125
  seen.add(name);
60
126
  bindings.push({ owner: row.owner, name: row.name });
61
127
  }
128
+ if (!sameIdentity())
129
+ return { reason: "repository_identity_unverified" };
62
130
  if (!bindings.length)
63
131
  return { repositories: [] };
64
132
  const contractRead = await readExistingOnboardJson(ctx, "/api/v1/agent/contract", signal);
133
+ if (!sameIdentity())
134
+ return { reason: "repository_identity_unverified" };
65
135
  if ("reason" in contractRead)
66
136
  return { reason: "repository_contract_unavailable" };
67
137
  const doc = object(contractRead.body);
68
138
  const account = object(doc?.account);
69
139
  const merge = object(doc?.merge);
70
- if (!doc || account?.id !== journal.account || typeof doc.contractVersion !== "string" ||
140
+ if (!doc ||
141
+ account?.id !== journal.account ||
142
+ typeof doc.contractVersion !== "string" ||
71
143
  contractVersionInRange(doc.contractVersion, readManifest().tenantContractRange) !== true ||
72
- !merge || !Array.isArray(merge.repositories) || merge.repositories.length > 10_000)
144
+ !merge ||
145
+ !Array.isArray(merge.repositories) ||
146
+ merge.repositories.length > 10_000)
73
147
  return { reason: "repository_contract_unverified" };
74
148
  const lookup = new Map();
75
149
  const idNames = new Map();
76
150
  for (const value of merge.repositories) {
77
151
  const row = object(value);
78
- if (!row || typeof row.owner !== "string" || !part.test(row.owner) || typeof row.name !== "string" || !part.test(row.name) ||
79
- typeof row.repoId !== "string" || !id.test(row.repoId))
152
+ if (!row ||
153
+ typeof row.owner !== "string" ||
154
+ !part.test(row.owner) ||
155
+ typeof row.name !== "string" ||
156
+ !part.test(row.name) ||
157
+ !isRepositoryId(row.repoId))
80
158
  return { reason: "repository_contract_unverified" };
81
159
  const name = key({ owner: row.owner, name: row.name });
82
160
  if (idNames.has(row.repoId) && idNames.get(row.repoId) !== name)
@@ -99,34 +177,57 @@ export function existingRepositoryAdapter(args, choose) {
99
177
  const explicit = args.flags.repo;
100
178
  if (explicit !== undefined) {
101
179
  const names = Array.isArray(explicit) ? explicit : [explicit];
102
- if (!names.length || names.length > 100 || !names.every(name => typeof name === "string" && /^[A-Za-z0-9][A-Za-z0-9_.-]{0,99}\/[A-Za-z0-9][A-Za-z0-9_.-]{0,99}$/.test(name)))
180
+ if (!names.length ||
181
+ names.length > 100 ||
182
+ !names.every((name) => typeof name === "string" &&
183
+ /^[A-Za-z0-9][A-Za-z0-9_.-]{0,99}\/[A-Za-z0-9][A-Za-z0-9_.-]{0,99}$/.test(name)))
103
184
  return null;
104
- const wanted = names.map(name => String(name).toLowerCase());
185
+ const wanted = names.map((name) => String(name).toLowerCase());
105
186
  if (new Set(wanted).size !== wanted.length)
106
187
  return null;
107
- const selected = wanted.map(name => repositories.find(row => key(row) === name));
108
- return selected.every((row) => !!row) ? selected : null;
188
+ const selected = wanted.map((name) => repositories.find((row) => key(row) === name));
189
+ return selected.every((row) => !!row)
190
+ ? selected
191
+ : null;
109
192
  }
110
193
  const saved = chosen ?? selectedOnboardRepositories(journal);
111
194
  if (!saved.length)
112
195
  return null;
113
- const selected = saved.map(row => repositories.find(live => key(live) === key(row) && live.teamId === row.teamId && live.repoId === row.repoId));
114
- return selected.every((row) => !!row) ? selected : null;
196
+ const selected = saved.map((row) => repositories.find((live) => key(live) === key(row) &&
197
+ live.teamId === row.teamId &&
198
+ live.repoId === row.repoId));
199
+ return selected.every((row) => !!row)
200
+ ? selected
201
+ : null;
115
202
  };
116
203
  const check = async (ctx, journal, signal) => {
117
- const list = await inventory(ctx, journal, signal);
204
+ const list = await readOnboardRepositoryInventory(ctx, journal, signal);
118
205
  if ("reason" in list)
119
206
  return waiting(list.reason);
120
207
  if (!list.repositories.length)
121
208
  return waiting("repository_inventory_empty");
122
209
  const rows = requested(list.repositories, journal);
123
210
  if (!rows)
124
- return args.flags.repo !== undefined || chosen || selectedOnboardRepositories(journal).length
125
- ? waiting("repository_selection_unverified") : choose ? { state: "pending" } : waiting("repository_choice_required");
126
- return { state: "done", evidence: { repository: JSON.stringify(rows), count: rows.length, checkedAt: ctx.now().getTime() } };
211
+ return args.flags.repo !== undefined ||
212
+ chosen ||
213
+ selectedOnboardRepositories(journal).length
214
+ ? waiting("repository_selection_unverified")
215
+ : choose
216
+ ? { state: "pending" }
217
+ : waiting("repository_choice_required");
218
+ return {
219
+ state: "done",
220
+ evidence: {
221
+ repository: JSON.stringify(rows),
222
+ count: rows.length,
223
+ checkedAt: ctx.now().getTime(),
224
+ },
225
+ };
127
226
  };
128
- return { check, act: async (ctx, journal, signal) => {
129
- const list = await inventory(ctx, journal, signal);
227
+ return {
228
+ check,
229
+ act: async (ctx, journal, signal) => {
230
+ const list = await readOnboardRepositoryInventory(ctx, journal, signal);
130
231
  if ("reason" in list)
131
232
  return waiting(list.reason);
132
233
  if (!list.repositories.length)
@@ -142,19 +243,27 @@ export function existingRepositoryAdapter(args, choose) {
142
243
  }
143
244
  signal?.addEventListener("abort", stopped, { once: true });
144
245
  remove = () => signal?.removeEventListener("abort", stopped);
145
- Promise.resolve().then(() => signal?.aborted ? null : choose(list.repositories.map(row => ({ ...row })))).then(resolve, reject);
246
+ Promise.resolve()
247
+ .then(() => signal?.aborted
248
+ ? null
249
+ : choose(list.repositories.map((row) => ({ ...row }))))
250
+ .then(resolve, reject);
146
251
  }).finally(() => remove());
147
252
  if (!answer || signal?.aborted)
148
253
  return waiting("interrupted");
149
- if (!Array.isArray(answer) || !answer.length || answer.length > 100 || !answer.every(name => typeof name === "string"))
254
+ if (!Array.isArray(answer) ||
255
+ !answer.length ||
256
+ answer.length > 100 ||
257
+ !answer.every((name) => typeof name === "string"))
150
258
  return waiting("repository_selection_unverified");
151
- const names = answer.map(name => name.toLowerCase());
259
+ const names = answer.map((name) => name.toLowerCase());
152
260
  if (new Set(names).size !== names.length)
153
261
  return waiting("repository_selection_unverified");
154
- const rows = names.map(name => list.repositories.find(row => key(row) === name));
262
+ const rows = names.map((name) => list.repositories.find((row) => key(row) === name));
155
263
  if (!rows.every((row) => !!row))
156
264
  return waiting("repository_selection_unverified");
157
265
  chosen = rows;
158
266
  return check(ctx, journal, signal);
159
- } };
267
+ },
268
+ };
160
269
  }