@topogram/cli 0.3.62 → 0.3.64

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 (121) hide show
  1. package/package.json +1 -1
  2. package/src/adoption/plan.d.ts +6 -0
  3. package/src/adoption/reporting.d.ts +10 -0
  4. package/src/adoption/review-groups.d.ts +6 -0
  5. package/src/agent-brief.d.ts +3 -0
  6. package/src/agent-brief.js +495 -0
  7. package/src/agent-ops/query-builders.d.ts +26 -0
  8. package/src/archive/archive.d.ts +2 -0
  9. package/src/archive/compact.d.ts +1 -0
  10. package/src/archive/unarchive.d.ts +1 -0
  11. package/src/catalog.d.ts +10 -0
  12. package/src/catalog.js +62 -66
  13. package/src/cli/catalog-alias.d.ts +1 -0
  14. package/src/cli/command-parser.js +38 -0
  15. package/src/cli/command-parsers/core.js +102 -0
  16. package/src/cli/command-parsers/generator.js +39 -0
  17. package/src/cli/command-parsers/import.js +44 -0
  18. package/src/cli/command-parsers/legacy-workflow.js +21 -0
  19. package/src/cli/command-parsers/project.js +47 -0
  20. package/src/cli/command-parsers/sdlc.js +47 -0
  21. package/src/cli/command-parsers/shared.js +51 -0
  22. package/src/cli/command-parsers/template.js +48 -0
  23. package/src/cli/commands/agent.js +47 -0
  24. package/src/cli/commands/catalog.js +617 -0
  25. package/src/cli/commands/check.js +268 -0
  26. package/src/cli/commands/doctor.js +268 -0
  27. package/src/cli/commands/emit.js +149 -0
  28. package/src/cli/commands/generate.js +96 -0
  29. package/src/cli/commands/generator-policy.js +785 -0
  30. package/src/cli/commands/generator.js +443 -0
  31. package/src/cli/commands/import-runner.js +157 -0
  32. package/src/cli/commands/import.js +1734 -0
  33. package/src/cli/commands/inspect.js +55 -0
  34. package/src/cli/commands/new.js +94 -0
  35. package/src/cli/commands/package.js +815 -0
  36. package/src/cli/commands/query.js +1302 -0
  37. package/src/cli/commands/release-rollout.js +257 -0
  38. package/src/cli/commands/release-shared.js +528 -0
  39. package/src/cli/commands/release-status.js +429 -0
  40. package/src/cli/commands/release.js +107 -0
  41. package/src/cli/commands/sdlc.js +168 -0
  42. package/src/cli/commands/setup.js +76 -0
  43. package/src/cli/commands/source.js +291 -0
  44. package/src/cli/commands/template-runner.js +198 -0
  45. package/src/cli/commands/template.js +2145 -0
  46. package/src/cli/commands/trust.js +219 -0
  47. package/src/cli/commands/version.js +40 -0
  48. package/src/cli/commands/widget.js +168 -0
  49. package/src/cli/commands/workflow.js +63 -0
  50. package/src/cli/dispatcher.js +392 -0
  51. package/src/cli/help-dispatch.js +188 -0
  52. package/src/cli/help.js +296 -0
  53. package/src/cli/migration-guidance.js +59 -0
  54. package/src/cli/options.js +96 -0
  55. package/src/cli/output-safety.js +107 -0
  56. package/src/cli/path-normalization.js +29 -0
  57. package/src/cli.js +47 -11711
  58. package/src/example-implementation.d.ts +2 -0
  59. package/src/format.d.ts +1 -0
  60. package/src/generator/check.d.ts +1 -0
  61. package/src/generator/context/bundle.d.ts +1 -0
  62. package/src/generator/context/shared.d.ts +2 -0
  63. package/src/generator/native/parity-bundle.js +2 -1
  64. package/src/generator/surfaces/web/html-escape.js +22 -0
  65. package/src/generator/surfaces/web/react.js +10 -8
  66. package/src/generator/surfaces/web/sveltekit.js +7 -5
  67. package/src/generator/surfaces/web/vanilla.js +8 -4
  68. package/src/generator.d.ts +2 -0
  69. package/src/github-client.js +520 -0
  70. package/src/import/core/shared.js +20 -62
  71. package/src/import/extractors/api/flutter-dio.js +4 -8
  72. package/src/import/extractors/api/react-native-repository.js +4 -8
  73. package/src/import/index.d.ts +4 -0
  74. package/src/import/provenance.d.ts +4 -0
  75. package/src/new-project.js +100 -11
  76. package/src/npm-safety.js +79 -0
  77. package/src/parser.d.ts +1 -0
  78. package/src/path-helpers.d.ts +1 -0
  79. package/src/path-helpers.js +20 -0
  80. package/src/project-config.js +1 -0
  81. package/src/reconcile/docs.d.ts +8 -0
  82. package/src/reconcile/journeys.d.ts +1 -0
  83. package/src/resolver.d.ts +1 -0
  84. package/src/runtime-support.js +29 -0
  85. package/src/sdlc/adopt.d.ts +1 -0
  86. package/src/sdlc/check.d.ts +1 -0
  87. package/src/sdlc/explain.d.ts +1 -0
  88. package/src/sdlc/release.d.ts +1 -0
  89. package/src/sdlc/scaffold.d.ts +1 -0
  90. package/src/sdlc/transition.d.ts +1 -0
  91. package/src/text-helpers.d.ts +6 -0
  92. package/src/text-helpers.js +245 -0
  93. package/src/topogram-config.js +306 -0
  94. package/src/validator.d.ts +2 -0
  95. package/src/workflows/adoption/index.js +26 -0
  96. package/src/workflows/docs-generate.js +262 -0
  97. package/src/workflows/docs-scan.js +703 -0
  98. package/src/workflows/docs.js +15 -0
  99. package/src/workflows/import-app/api.js +799 -0
  100. package/src/workflows/import-app/db.js +538 -0
  101. package/src/workflows/import-app/index.js +30 -0
  102. package/src/workflows/import-app/shared.js +218 -0
  103. package/src/workflows/import-app/ui.js +443 -0
  104. package/src/workflows/import-app/workflow.js +159 -0
  105. package/src/workflows/reconcile/adoption-plan.js +742 -0
  106. package/src/workflows/reconcile/auth.js +692 -0
  107. package/src/workflows/reconcile/bundle-core.js +600 -0
  108. package/src/workflows/reconcile/bundle-shared.js +75 -0
  109. package/src/workflows/reconcile/candidate-model.js +477 -0
  110. package/src/workflows/reconcile/canonical-surface.js +264 -0
  111. package/src/workflows/reconcile/gap-report.js +333 -0
  112. package/src/workflows/reconcile/ids.js +6 -0
  113. package/src/workflows/reconcile/impacts.js +625 -0
  114. package/src/workflows/reconcile/index.js +7 -0
  115. package/src/workflows/reconcile/renderers.js +461 -0
  116. package/src/workflows/reconcile/summary.js +90 -0
  117. package/src/workflows/reconcile/workflow.js +309 -0
  118. package/src/workflows/shared.js +189 -0
  119. package/src/workflows/types.d.ts +93 -0
  120. package/src/workflows.d.ts +1 -0
  121. package/src/workflows.js +10 -7652
@@ -0,0 +1,528 @@
1
+ // @ts-check
2
+
3
+ import childProcess from "node:child_process";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+
7
+ import {
8
+ latestWorkflowRun,
9
+ workflowRunJobs
10
+ } from "../../github-client.js";
11
+ import {
12
+ githubRepoSlug,
13
+ releaseConsumerRepos,
14
+ releaseConsumerWorkflowJobs,
15
+ releaseConsumerWorkflowName
16
+ } from "../../topogram-config.js";
17
+
18
+ const REPO_ROOT = decodeURIComponent(new URL("../../../../", import.meta.url).pathname);
19
+
20
+ /**
21
+ * @typedef {Record<string, any>} AnyRecord
22
+ */
23
+
24
+ /**
25
+ * @param {unknown} error
26
+ * @returns {string}
27
+ */
28
+ export function messageFromError(error) {
29
+ return error instanceof Error ? error.message : String(error);
30
+ }
31
+
32
+ /**
33
+ * @param {string} version
34
+ * @param {string} cwd
35
+ * @returns {{ tag: string, local: boolean|null, remote: boolean|null, diagnostics: Array<AnyRecord> }}
36
+ */
37
+ export function inspectReleaseGitTag(version, cwd) {
38
+ const tag = `topogram-v${version}`;
39
+ const diagnostics = [];
40
+ let local = null;
41
+ let remote = null;
42
+ const localResult = childProcess.spawnSync("git", ["tag", "--list", tag], {
43
+ cwd,
44
+ encoding: "utf8",
45
+ env: { ...process.env, PATH: process.env.PATH || "" }
46
+ });
47
+ if (localResult.status === 0) {
48
+ local = String(localResult.stdout || "").trim() === tag;
49
+ } else {
50
+ diagnostics.push({
51
+ code: "release_local_tag_unavailable",
52
+ severity: "warning",
53
+ message: `Could not inspect local git tag ${tag}.`,
54
+ path: cwd,
55
+ suggestedFix: "Run from a git checkout with git available."
56
+ });
57
+ }
58
+ const remoteResult = childProcess.spawnSync("git", ["ls-remote", "--exit-code", "--tags", "origin", `refs/tags/${tag}`], {
59
+ cwd,
60
+ encoding: "utf8",
61
+ env: { ...process.env, PATH: process.env.PATH || "" }
62
+ });
63
+ if (remoteResult.status === 0) {
64
+ remote = true;
65
+ } else if (remoteResult.status === 2) {
66
+ remote = false;
67
+ } else {
68
+ diagnostics.push({
69
+ code: "release_remote_tag_unavailable",
70
+ severity: "warning",
71
+ message: `Could not inspect remote git tag ${tag}.`,
72
+ path: cwd,
73
+ suggestedFix: "Check git remote access, then rerun `topogram release status`."
74
+ });
75
+ }
76
+ return { tag, local, remote, diagnostics };
77
+ }
78
+
79
+ /**
80
+ * @param {string} name
81
+ * @returns {string|null}
82
+ */
83
+ export function expectedConsumerWorkflowName(name) {
84
+ return releaseConsumerWorkflowName(name);
85
+ }
86
+
87
+ /**
88
+ * @param {string} name
89
+ * @returns {string[]}
90
+ */
91
+ function expectedConsumerWorkflowJobs(name) {
92
+ return releaseConsumerWorkflowJobs(name);
93
+ }
94
+
95
+ /**
96
+ * @param {{ name: string }|string} consumer
97
+ * @returns {string}
98
+ */
99
+ function consumerGithubRepoSlug(consumer) {
100
+ const name = typeof consumer === "string" ? consumer : consumer.name;
101
+ return githubRepoSlug(name);
102
+ }
103
+
104
+ /**
105
+ * @param {string[]} args
106
+ * @param {string} cwd
107
+ * @returns {ReturnType<typeof childProcess.spawnSync>}
108
+ */
109
+ export function runGit(args, cwd) {
110
+ return childProcess.spawnSync("git", args, {
111
+ cwd,
112
+ encoding: "utf8",
113
+ env: { ...process.env, PATH: process.env.PATH || "" }
114
+ });
115
+ }
116
+
117
+ /**
118
+ * @param {string} cwd
119
+ * @returns {{ ok: boolean, dirty: boolean|null, error: string|null }}
120
+ */
121
+ export function inspectGitWorktreeClean(cwd) {
122
+ const result = runGit(["status", "--porcelain"], cwd);
123
+ if (result.status !== 0) {
124
+ return {
125
+ ok: false,
126
+ dirty: null,
127
+ error: `Could not inspect git status: ${commandOutput(result) || "unknown error"}`
128
+ };
129
+ }
130
+ const dirty = String(result.stdout || "").trim().length > 0;
131
+ return {
132
+ ok: !dirty,
133
+ dirty,
134
+ error: dirty ? "Consumer repo has uncommitted changes." : null
135
+ };
136
+ }
137
+
138
+ /**
139
+ * @param {string} cwd
140
+ * @returns {{ ok: boolean, changed: boolean, result: ReturnType<typeof childProcess.spawnSync> }}
141
+ */
142
+ export function hasStagedGitChanges(cwd) {
143
+ const result = runGit(["diff", "--cached", "--quiet"], cwd);
144
+ return {
145
+ ok: result.status === 0 || result.status === 1,
146
+ changed: result.status === 1,
147
+ result
148
+ };
149
+ }
150
+
151
+ /**
152
+ * @param {string} cwd
153
+ * @returns {string|null}
154
+ */
155
+ export function currentGitHead(cwd) {
156
+ const result = runGit(["rev-parse", "HEAD"], cwd);
157
+ return result.status === 0 ? String(result.stdout || "").trim() || null : null;
158
+ }
159
+
160
+ /**
161
+ * @param {{ code: string, severity: "error"|"warning", message: string, path: string|null, suggestedFix: string, result: ReturnType<typeof childProcess.spawnSync> }} input
162
+ * @returns {{ code: string, severity: "error"|"warning", message: string, path: string|null, suggestedFix: string }}
163
+ */
164
+ export function commandDiagnostic(input) {
165
+ const output = commandOutput(input.result);
166
+ return {
167
+ code: input.code,
168
+ severity: input.severity,
169
+ message: output ? `${input.message}\n${output}` : input.message,
170
+ path: input.path,
171
+ suggestedFix: input.suggestedFix
172
+ };
173
+ }
174
+
175
+ /**
176
+ * @param {ReturnType<typeof childProcess.spawnSync>} result
177
+ * @returns {string}
178
+ */
179
+ function commandOutput(result) {
180
+ return [result.error?.message, result.stderr, result.stdout].filter(Boolean).join("\n").trim();
181
+ }
182
+
183
+ /**
184
+ * @param {number} ms
185
+ * @returns {void}
186
+ */
187
+ function sleepSync(ms) {
188
+ if (ms <= 0) {
189
+ return;
190
+ }
191
+ const buffer = new SharedArrayBuffer(4);
192
+ const view = new Int32Array(buffer);
193
+ Atomics.wait(view, 0, 0, ms);
194
+ }
195
+
196
+ /**
197
+ * @param {string} name
198
+ * @param {number} fallback
199
+ * @returns {number}
200
+ */
201
+ function positiveIntegerEnv(name, fallback) {
202
+ const value = Number.parseInt(process.env[name] || "", 10);
203
+ return Number.isFinite(value) && value > 0 ? value : fallback;
204
+ }
205
+
206
+ /**
207
+ * @param {{ name: string, root?: string|null, workflow?: string|null }} consumer
208
+ * @param {{ timeoutMs?: number, intervalMs?: number }} [options]
209
+ * @returns {ReturnType<typeof inspectConsumerCi>}
210
+ */
211
+ export function waitForConsumerCi(consumer, options = {}) {
212
+ const timeoutMs = options.timeoutMs || positiveIntegerEnv("TOPOGRAM_RELEASE_WATCH_TIMEOUT_MS", 20 * 60 * 1000);
213
+ const intervalMs = options.intervalMs || positiveIntegerEnv("TOPOGRAM_RELEASE_WATCH_INTERVAL_MS", 5000);
214
+ const startedAt = Date.now();
215
+ let latest = inspectConsumerCi(consumer, { strict: false });
216
+ while (true) {
217
+ const currentRun = latest.run &&
218
+ latest.headSha &&
219
+ latest.run?.headSha &&
220
+ latest.run.headSha === latest.headSha;
221
+ if (currentRun && latest.run?.status === "completed") {
222
+ return inspectConsumerCi(consumer, { strict: true });
223
+ }
224
+ if (Date.now() - startedAt >= timeoutMs) {
225
+ const strictLatest = inspectConsumerCi(consumer, { strict: true });
226
+ strictLatest.diagnostics.push({
227
+ code: "release_consumer_ci_watch_timeout",
228
+ severity: "error",
229
+ message: `${consumer.name} verification workflow did not complete on the current commit before the watch timeout.`,
230
+ path: strictLatest.run?.url || consumerGithubRepoSlug(consumer),
231
+ suggestedFix: "Open the consumer workflow, fix failures if needed, then rerun release status."
232
+ });
233
+ strictLatest.ok = false;
234
+ return strictLatest;
235
+ }
236
+ sleepSync(intervalMs);
237
+ latest = inspectConsumerCi(consumer, { strict: false });
238
+ }
239
+ }
240
+
241
+ /**
242
+ * @param {{ name: string, root?: string|null, workflow?: string|null }} consumer
243
+ * @param {{ strict?: boolean }} [options]
244
+ * @returns {{ checked: boolean, ok: boolean|null, expectedWorkflow: string|null, expectedJobs: string[], headSha: string|null, run: AnyRecord|null, diagnostics: Array<AnyRecord> }}
245
+ */
246
+ export function inspectConsumerCi(consumer, options = {}) {
247
+ const diagnostics = [];
248
+ const expectedWorkflow = consumer.workflow || expectedConsumerWorkflowName(consumer.name);
249
+ const expectedJobs = expectedConsumerWorkflowJobs(consumer.name);
250
+ const repoSlug = consumerGithubRepoSlug(consumer);
251
+ if (!consumer.root || !fs.existsSync(consumer.root)) {
252
+ return {
253
+ checked: false,
254
+ ok: null,
255
+ expectedWorkflow,
256
+ expectedJobs,
257
+ headSha: null,
258
+ run: null,
259
+ diagnostics: []
260
+ };
261
+ }
262
+ const headSha = currentGitHead(consumer.root);
263
+ if (!headSha) {
264
+ diagnostics.push({
265
+ code: "release_consumer_head_unavailable",
266
+ severity: options.strict ? "error" : "warning",
267
+ message: `Could not inspect local HEAD for ${consumer.name}.`,
268
+ path: consumer.root,
269
+ suggestedFix: "Run from a checked-out consumer git repository."
270
+ });
271
+ }
272
+ if (!expectedWorkflow) {
273
+ diagnostics.push({
274
+ code: "release_consumer_workflow_unknown",
275
+ severity: options.strict ? "error" : "warning",
276
+ message: `No expected verification workflow is configured for ${consumer.name}.`,
277
+ path: consumer.name,
278
+ suggestedFix: "Add the consumer repo to topogram.config.json release.workflows or the built-in release workflow defaults."
279
+ });
280
+ return {
281
+ checked: true,
282
+ ok: false,
283
+ expectedWorkflow,
284
+ expectedJobs,
285
+ headSha,
286
+ run: null,
287
+ diagnostics
288
+ };
289
+ }
290
+ /** @type {AnyRecord|null} */
291
+ let run = null;
292
+ try {
293
+ run = latestWorkflowRun({
294
+ repoSlug,
295
+ branch: "main",
296
+ workflowName: expectedWorkflow,
297
+ cwd: consumer.root
298
+ });
299
+ } catch (error) {
300
+ diagnostics.push({
301
+ code: "release_consumer_ci_unavailable",
302
+ severity: options.strict ? "error" : "warning",
303
+ message: [`Could not inspect ${expectedWorkflow} for ${consumer.name}.`, messageFromError(error)].filter(Boolean).join("\n"),
304
+ path: repoSlug,
305
+ suggestedFix: "Set GITHUB_TOKEN or GH_TOKEN with Actions read access, or run `gh auth login` for local fallback; then rerun release status."
306
+ });
307
+ return {
308
+ checked: true,
309
+ ok: false,
310
+ expectedWorkflow,
311
+ expectedJobs,
312
+ headSha,
313
+ run: null,
314
+ diagnostics
315
+ };
316
+ }
317
+ if (!run) {
318
+ diagnostics.push({
319
+ code: "release_consumer_ci_missing",
320
+ severity: options.strict ? "error" : "warning",
321
+ message: `${consumer.name} has no ${expectedWorkflow} run on main.`,
322
+ path: repoSlug,
323
+ suggestedFix: "Push the consumer repo and wait for its verification workflow."
324
+ });
325
+ return {
326
+ checked: true,
327
+ ok: false,
328
+ expectedWorkflow,
329
+ expectedJobs,
330
+ headSha,
331
+ run: null,
332
+ diagnostics
333
+ };
334
+ }
335
+ if (headSha && run.headSha && run.headSha !== headSha) {
336
+ diagnostics.push({
337
+ code: "release_consumer_ci_head_mismatch",
338
+ severity: options.strict ? "error" : "warning",
339
+ message: `${consumer.name} latest ${expectedWorkflow} run is for ${run.headSha}, not checked-out HEAD ${headSha}.`,
340
+ path: run.url || repoSlug,
341
+ suggestedFix: "Wait for the verification workflow on the current consumer commit, then rerun release status."
342
+ });
343
+ }
344
+ if (run.status !== "completed" || run.conclusion !== "success") {
345
+ diagnostics.push({
346
+ code: "release_consumer_ci_not_successful",
347
+ severity: options.strict ? "error" : "warning",
348
+ message: `${consumer.name} ${expectedWorkflow} is ${run.status || "unknown"}/${run.conclusion || "unknown"}.`,
349
+ path: run.url || repoSlug,
350
+ suggestedFix: "Wait for or fix the consumer verification workflow, then rerun release status."
351
+ });
352
+ }
353
+ if (expectedJobs.length > 0 && run.databaseId) {
354
+ const jobResult = inspectConsumerWorkflowJobs(consumer, run.databaseId, expectedJobs, options);
355
+ if (jobResult.jobs) {
356
+ run.jobs = jobResult.jobs;
357
+ }
358
+ diagnostics.push(...jobResult.diagnostics);
359
+ } else if (expectedJobs.length > 0) {
360
+ diagnostics.push({
361
+ code: "release_consumer_ci_jobs_unavailable",
362
+ severity: options.strict ? "error" : "warning",
363
+ message: `${consumer.name} ${expectedWorkflow} run did not include a database id, so expected jobs could not be inspected.`,
364
+ path: run.url || repoSlug,
365
+ suggestedFix: "Rerun release status after GitHub exposes the workflow run id."
366
+ });
367
+ }
368
+ const errorCount = diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
369
+ return {
370
+ checked: true,
371
+ ok: errorCount === 0 &&
372
+ (!options.strict || (run.status === "completed" && run.conclusion === "success" && (!headSha || !run.headSha || run.headSha === headSha))),
373
+ expectedWorkflow,
374
+ expectedJobs,
375
+ headSha,
376
+ run,
377
+ diagnostics
378
+ };
379
+ }
380
+
381
+ /**
382
+ * @param {{ name: string, root?: string|null }} consumer
383
+ * @param {number|string} runId
384
+ * @param {string[]} expectedJobs
385
+ * @param {{ strict?: boolean }} [options]
386
+ * @returns {{ jobs: Array<AnyRecord>|null, diagnostics: Array<AnyRecord> }}
387
+ */
388
+ function inspectConsumerWorkflowJobs(consumer, runId, expectedJobs, options = {}) {
389
+ const diagnostics = [];
390
+ const repoSlug = consumerGithubRepoSlug(consumer);
391
+ let jobs = [];
392
+ try {
393
+ jobs = workflowRunJobs({
394
+ repoSlug,
395
+ runId,
396
+ cwd: consumer.root || process.cwd()
397
+ });
398
+ } catch (error) {
399
+ diagnostics.push({
400
+ code: "release_consumer_ci_jobs_unavailable",
401
+ severity: options.strict ? "error" : "warning",
402
+ message: [`Could not inspect expected jobs for ${consumer.name}.`, messageFromError(error)].filter(Boolean).join("\n"),
403
+ path: repoSlug,
404
+ suggestedFix: "Set GITHUB_TOKEN or GH_TOKEN with Actions read access, or run `gh auth login` for local fallback; then rerun release status."
405
+ });
406
+ return { jobs: null, diagnostics };
407
+ }
408
+ for (const expectedJob of expectedJobs) {
409
+ const job = jobs.find((candidate) => candidate?.name === expectedJob);
410
+ if (!job) {
411
+ diagnostics.push({
412
+ code: "release_consumer_ci_job_missing",
413
+ severity: options.strict ? "error" : "warning",
414
+ message: `${consumer.name} workflow is missing expected job '${expectedJob}'.`,
415
+ path: repoSlug,
416
+ suggestedFix: "Update the consumer workflow or the release-status expected job list, then rerun release status."
417
+ });
418
+ continue;
419
+ }
420
+ if (job.status !== "completed" || job.conclusion !== "success") {
421
+ diagnostics.push({
422
+ code: "release_consumer_ci_job_not_successful",
423
+ severity: options.strict ? "error" : "warning",
424
+ message: `${consumer.name} job '${expectedJob}' is ${job.status || "unknown"}/${job.conclusion || "unknown"}.`,
425
+ path: job.url || repoSlug,
426
+ suggestedFix: "Wait for or fix the expected workflow job, then rerun release status."
427
+ });
428
+ }
429
+ }
430
+ return { jobs, diagnostics };
431
+ }
432
+
433
+ /**
434
+ * @param {string} cwd
435
+ * @returns {Array<{ name: string, root: string|null, path: string, version: string|null, found: boolean }>}
436
+ */
437
+ export function discoverTopogramCliVersionConsumers(cwd) {
438
+ /** @type {string[]} */
439
+ const roots = [];
440
+ for (const root of [cwd, REPO_ROOT, path.dirname(REPO_ROOT)]) {
441
+ const resolved = path.resolve(root);
442
+ if (!roots.includes(resolved)) {
443
+ roots.push(resolved);
444
+ }
445
+ }
446
+ const consumers = [];
447
+ for (const name of releaseConsumerRepos(cwd)) {
448
+ let found = null;
449
+ for (const root of roots) {
450
+ const consumerRoot = path.join(root, name);
451
+ const versionPath = path.join(consumerRoot, "topogram-cli.version");
452
+ if (fs.existsSync(consumerRoot) && !fs.existsSync(versionPath)) {
453
+ found = {
454
+ name,
455
+ root: consumerRoot,
456
+ path: versionPath,
457
+ version: null,
458
+ found: false
459
+ };
460
+ break;
461
+ }
462
+ if (!fs.existsSync(versionPath)) {
463
+ continue;
464
+ }
465
+ found = {
466
+ name,
467
+ root: consumerRoot,
468
+ path: versionPath,
469
+ version: fs.readFileSync(versionPath, "utf8").trim() || null,
470
+ found: true
471
+ };
472
+ break;
473
+ }
474
+ consumers.push(found || {
475
+ name,
476
+ root: null,
477
+ path: path.join(roots[0], name, "topogram-cli.version"),
478
+ version: null,
479
+ found: false
480
+ });
481
+ }
482
+ return consumers;
483
+ }
484
+
485
+ /**
486
+ * @param {Array<any>} consumers
487
+ * @returns {{ known: number, pinned: number, matching: number, differing: number, missing: number, allKnownPinned: boolean, matchingNames: string[], differingNames: string[], missingNames: string[] }}
488
+ */
489
+ export function summarizeConsumerPins(consumers) {
490
+ const matchingNames = consumers.filter((consumer) => consumer.matchesLocal === true).map((consumer) => consumer.name);
491
+ const differingNames = consumers.filter((consumer) => consumer.matchesLocal === false).map((consumer) => consumer.name);
492
+ const missingNames = consumers.filter((consumer) => !consumer.found || !consumer.version).map((consumer) => consumer.name);
493
+ return {
494
+ known: consumers.length,
495
+ pinned: consumers.filter((consumer) => consumer.found && consumer.version).length,
496
+ matching: matchingNames.length,
497
+ differing: differingNames.length,
498
+ missing: missingNames.length,
499
+ allKnownPinned: consumers.length > 0 && differingNames.length === 0 && missingNames.length === 0,
500
+ matchingNames,
501
+ differingNames,
502
+ missingNames
503
+ };
504
+ }
505
+
506
+ /**
507
+ * @param {Array<any>} consumers
508
+ * @returns {{ checked: number, passing: number, failing: number, unavailable: number, skipped: number, allCheckedAndPassing: boolean, passingNames: string[], failingNames: string[], unavailableNames: string[], skippedNames: string[] }}
509
+ */
510
+ export function summarizeConsumerCi(consumers) {
511
+ const checked = consumers.filter((consumer) => consumer.ci?.checked);
512
+ const passingNames = checked.filter((consumer) => consumer.ci?.ok === true).map((consumer) => consumer.name);
513
+ const failingNames = checked.filter((consumer) => consumer.ci?.ok === false && consumer.ci?.run).map((consumer) => consumer.name);
514
+ const unavailableNames = checked.filter((consumer) => consumer.ci?.ok === false && !consumer.ci?.run).map((consumer) => consumer.name);
515
+ const skippedNames = consumers.filter((consumer) => !consumer.ci?.checked).map((consumer) => consumer.name);
516
+ return {
517
+ checked: checked.length,
518
+ passing: passingNames.length,
519
+ failing: failingNames.length,
520
+ unavailable: unavailableNames.length,
521
+ skipped: skippedNames.length,
522
+ allCheckedAndPassing: consumers.length > 0 && checked.length === consumers.length && failingNames.length === 0 && unavailableNames.length === 0,
523
+ passingNames,
524
+ failingNames,
525
+ unavailableNames,
526
+ skippedNames
527
+ };
528
+ }