@topogram/cli 0.3.63 → 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.
- package/package.json +1 -1
- package/src/adoption/plan.d.ts +6 -0
- package/src/adoption/reporting.d.ts +10 -0
- package/src/adoption/review-groups.d.ts +6 -0
- package/src/agent-brief.d.ts +3 -0
- package/src/agent-brief.js +495 -0
- package/src/agent-ops/query-builders.d.ts +26 -0
- package/src/archive/archive.d.ts +2 -0
- package/src/archive/compact.d.ts +1 -0
- package/src/archive/unarchive.d.ts +1 -0
- package/src/catalog.d.ts +10 -0
- package/src/catalog.js +62 -66
- package/src/cli/catalog-alias.d.ts +1 -0
- package/src/cli/command-parser.js +38 -0
- package/src/cli/command-parsers/core.js +102 -0
- package/src/cli/command-parsers/generator.js +39 -0
- package/src/cli/command-parsers/import.js +44 -0
- package/src/cli/command-parsers/legacy-workflow.js +21 -0
- package/src/cli/command-parsers/project.js +47 -0
- package/src/cli/command-parsers/sdlc.js +47 -0
- package/src/cli/command-parsers/shared.js +51 -0
- package/src/cli/command-parsers/template.js +48 -0
- package/src/cli/commands/agent.js +47 -0
- package/src/cli/commands/catalog.js +617 -0
- package/src/cli/commands/check.js +268 -0
- package/src/cli/commands/doctor.js +268 -0
- package/src/cli/commands/emit.js +149 -0
- package/src/cli/commands/generate.js +96 -0
- package/src/cli/commands/generator-policy.js +785 -0
- package/src/cli/commands/generator.js +443 -0
- package/src/cli/commands/import-runner.js +157 -0
- package/src/cli/commands/import.js +1734 -0
- package/src/cli/commands/inspect.js +55 -0
- package/src/cli/commands/new.js +94 -0
- package/src/cli/commands/package.js +815 -0
- package/src/cli/commands/query.js +1302 -0
- package/src/cli/commands/release-rollout.js +257 -0
- package/src/cli/commands/release-shared.js +528 -0
- package/src/cli/commands/release-status.js +429 -0
- package/src/cli/commands/release.js +107 -0
- package/src/cli/commands/sdlc.js +168 -0
- package/src/cli/commands/setup.js +76 -0
- package/src/cli/commands/source.js +291 -0
- package/src/cli/commands/template-runner.js +198 -0
- package/src/cli/commands/template.js +2145 -0
- package/src/cli/commands/trust.js +219 -0
- package/src/cli/commands/version.js +40 -0
- package/src/cli/commands/widget.js +168 -0
- package/src/cli/commands/workflow.js +63 -0
- package/src/cli/dispatcher.js +392 -0
- package/src/cli/help-dispatch.js +188 -0
- package/src/cli/help.js +296 -0
- package/src/cli/migration-guidance.js +59 -0
- package/src/cli/options.js +96 -0
- package/src/cli/output-safety.js +107 -0
- package/src/cli/path-normalization.js +29 -0
- package/src/cli.js +47 -11711
- package/src/example-implementation.d.ts +2 -0
- package/src/format.d.ts +1 -0
- package/src/generator/check.d.ts +1 -0
- package/src/generator/context/bundle.d.ts +1 -0
- package/src/generator/context/shared.d.ts +2 -0
- package/src/generator/native/parity-bundle.js +2 -1
- package/src/generator/surfaces/web/html-escape.js +22 -0
- package/src/generator/surfaces/web/react.js +10 -8
- package/src/generator/surfaces/web/sveltekit.js +7 -5
- package/src/generator/surfaces/web/vanilla.js +8 -4
- package/src/generator.d.ts +2 -0
- package/src/github-client.js +520 -0
- package/src/import/core/shared.js +20 -62
- package/src/import/extractors/api/flutter-dio.js +4 -8
- package/src/import/extractors/api/react-native-repository.js +4 -8
- package/src/import/index.d.ts +4 -0
- package/src/import/provenance.d.ts +4 -0
- package/src/new-project.js +100 -11
- package/src/npm-safety.js +79 -0
- package/src/parser.d.ts +1 -0
- package/src/path-helpers.d.ts +1 -0
- package/src/path-helpers.js +20 -0
- package/src/project-config.js +1 -0
- package/src/reconcile/docs.d.ts +8 -0
- package/src/reconcile/journeys.d.ts +1 -0
- package/src/resolver.d.ts +1 -0
- package/src/runtime-support.js +29 -0
- package/src/sdlc/adopt.d.ts +1 -0
- package/src/sdlc/check.d.ts +1 -0
- package/src/sdlc/explain.d.ts +1 -0
- package/src/sdlc/release.d.ts +1 -0
- package/src/sdlc/scaffold.d.ts +1 -0
- package/src/sdlc/transition.d.ts +1 -0
- package/src/text-helpers.d.ts +6 -0
- package/src/text-helpers.js +245 -0
- package/src/topogram-config.js +306 -0
- package/src/validator.d.ts +2 -0
- package/src/workflows/adoption/index.js +26 -0
- package/src/workflows/docs-generate.js +262 -0
- package/src/workflows/docs-scan.js +703 -0
- package/src/workflows/docs.js +15 -0
- package/src/workflows/import-app/api.js +799 -0
- package/src/workflows/import-app/db.js +538 -0
- package/src/workflows/import-app/index.js +30 -0
- package/src/workflows/import-app/shared.js +218 -0
- package/src/workflows/import-app/ui.js +443 -0
- package/src/workflows/import-app/workflow.js +159 -0
- package/src/workflows/reconcile/adoption-plan.js +742 -0
- package/src/workflows/reconcile/auth.js +692 -0
- package/src/workflows/reconcile/bundle-core.js +600 -0
- package/src/workflows/reconcile/bundle-shared.js +75 -0
- package/src/workflows/reconcile/candidate-model.js +477 -0
- package/src/workflows/reconcile/canonical-surface.js +264 -0
- package/src/workflows/reconcile/gap-report.js +333 -0
- package/src/workflows/reconcile/ids.js +6 -0
- package/src/workflows/reconcile/impacts.js +625 -0
- package/src/workflows/reconcile/index.js +7 -0
- package/src/workflows/reconcile/renderers.js +461 -0
- package/src/workflows/reconcile/summary.js +90 -0
- package/src/workflows/reconcile/workflow.js +309 -0
- package/src/workflows/shared.js +189 -0
- package/src/workflows/types.d.ts +93 -0
- package/src/workflows.d.ts +1 -0
- package/src/workflows.js +10 -7652
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { loadCatalog } from "../../catalog.js";
|
|
4
|
+
import { catalogRepoSlug } from "../../topogram-config.js";
|
|
5
|
+
import {
|
|
6
|
+
CLI_PACKAGE_NAME,
|
|
7
|
+
latestTopogramCliVersion,
|
|
8
|
+
readInstalledCliPackageVersion
|
|
9
|
+
} from "./package.js";
|
|
10
|
+
import {
|
|
11
|
+
discoverTopogramCliVersionConsumers,
|
|
12
|
+
expectedConsumerWorkflowName,
|
|
13
|
+
inspectConsumerCi,
|
|
14
|
+
inspectReleaseGitTag,
|
|
15
|
+
messageFromError,
|
|
16
|
+
summarizeConsumerCi,
|
|
17
|
+
summarizeConsumerPins
|
|
18
|
+
} from "./release-shared.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {Record<string, any>} AnyRecord
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {{ cwd?: string, strict?: boolean }} [options]
|
|
26
|
+
* @returns {{ ok: boolean, strict: boolean, packageName: string, localVersion: string, latestVersion: string|null, currentPublished: boolean|null, git: ReturnType<typeof inspectReleaseGitTag>, consumerPins: ReturnType<typeof summarizeConsumerPins>, consumerCi: ReturnType<typeof summarizeConsumerCi>, consumers: Array<AnyRecord>, diagnostics: Array<AnyRecord>, errors: string[] }}
|
|
27
|
+
*/
|
|
28
|
+
export function buildReleaseStatusPayload(options = {}) {
|
|
29
|
+
const cwd = options.cwd || process.cwd();
|
|
30
|
+
const strict = Boolean(options.strict);
|
|
31
|
+
const localVersion = readInstalledCliPackageVersion();
|
|
32
|
+
/** @type {Array<AnyRecord>} */
|
|
33
|
+
const diagnostics = [];
|
|
34
|
+
let latestVersion = null;
|
|
35
|
+
try {
|
|
36
|
+
latestVersion = latestTopogramCliVersion(cwd);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
diagnostics.push({
|
|
39
|
+
code: "release_latest_unavailable",
|
|
40
|
+
severity: "warning",
|
|
41
|
+
message: messageFromError(error),
|
|
42
|
+
path: CLI_PACKAGE_NAME,
|
|
43
|
+
suggestedFix: "Check npmjs access and rerun `topogram release status`."
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
const git = inspectReleaseGitTag(localVersion, cwd);
|
|
47
|
+
diagnostics.push(...git.diagnostics);
|
|
48
|
+
const consumers = discoverTopogramCliVersionConsumers(cwd).map((consumer) => /** @type {AnyRecord} */ ({
|
|
49
|
+
...consumer,
|
|
50
|
+
matchesLocal: consumer.version ? consumer.version === localVersion : null,
|
|
51
|
+
workflow: expectedConsumerWorkflowName(consumer.name),
|
|
52
|
+
ci: null
|
|
53
|
+
}));
|
|
54
|
+
if (strict) {
|
|
55
|
+
for (const consumer of /** @type {Array<any>} */ (consumers)) {
|
|
56
|
+
if (consumer.matchesLocal === true) {
|
|
57
|
+
consumer.ci = inspectConsumerCi(consumer, { strict: true });
|
|
58
|
+
diagnostics.push(...consumer.ci.diagnostics);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const consumerPins = summarizeConsumerPins(consumers);
|
|
63
|
+
const consumerCi = summarizeConsumerCi(consumers);
|
|
64
|
+
const currentPublished = latestVersion ? latestVersion === localVersion : null;
|
|
65
|
+
if (strict) {
|
|
66
|
+
diagnostics.push(...releaseStatusStrictDiagnostics({
|
|
67
|
+
localVersion,
|
|
68
|
+
latestVersion,
|
|
69
|
+
currentPublished,
|
|
70
|
+
git,
|
|
71
|
+
consumerPins,
|
|
72
|
+
consumerCi
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
const errors = diagnostics
|
|
76
|
+
.filter((diagnostic) => diagnostic.severity === "error")
|
|
77
|
+
.map((diagnostic) => diagnostic.message);
|
|
78
|
+
return {
|
|
79
|
+
ok: errors.length === 0,
|
|
80
|
+
strict,
|
|
81
|
+
packageName: CLI_PACKAGE_NAME,
|
|
82
|
+
localVersion,
|
|
83
|
+
latestVersion,
|
|
84
|
+
currentPublished,
|
|
85
|
+
git,
|
|
86
|
+
consumerPins,
|
|
87
|
+
consumerCi,
|
|
88
|
+
consumers,
|
|
89
|
+
diagnostics,
|
|
90
|
+
errors
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @param {{
|
|
96
|
+
* localVersion: string,
|
|
97
|
+
* latestVersion: string|null,
|
|
98
|
+
* currentPublished: boolean|null,
|
|
99
|
+
* git: ReturnType<typeof inspectReleaseGitTag>,
|
|
100
|
+
* consumerPins: ReturnType<typeof summarizeConsumerPins>,
|
|
101
|
+
* consumerCi: ReturnType<typeof summarizeConsumerCi>
|
|
102
|
+
* }} release
|
|
103
|
+
* @returns {Array<{ code: string, severity: "error", message: string, path: string, suggestedFix: string }>}
|
|
104
|
+
*/
|
|
105
|
+
function releaseStatusStrictDiagnostics(release) {
|
|
106
|
+
/** @type {Array<{ code: string, severity: "error", message: string, path: string, suggestedFix: string }>} */
|
|
107
|
+
const diagnostics = [];
|
|
108
|
+
if (release.currentPublished !== true) {
|
|
109
|
+
diagnostics.push({
|
|
110
|
+
code: "release_latest_not_current",
|
|
111
|
+
severity: "error",
|
|
112
|
+
message: release.latestVersion
|
|
113
|
+
? `${CLI_PACKAGE_NAME}@${release.localVersion} is not the latest published version (${release.latestVersion}).`
|
|
114
|
+
: `Latest published ${CLI_PACKAGE_NAME} version could not be verified.`,
|
|
115
|
+
path: CLI_PACKAGE_NAME,
|
|
116
|
+
suggestedFix: "Publish the current CLI package version or fix npm package registry auth, then rerun `topogram release status --strict`."
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
if (release.git.local !== true && release.git.remote !== true) {
|
|
120
|
+
diagnostics.push({
|
|
121
|
+
code: "release_local_tag_missing",
|
|
122
|
+
severity: "error",
|
|
123
|
+
message: `Release tag ${release.git.tag} is missing locally.`,
|
|
124
|
+
path: release.git.tag,
|
|
125
|
+
suggestedFix: `Fetch, create, or push ${release.git.tag} before treating this release as complete.`
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
if (release.git.remote !== true) {
|
|
129
|
+
diagnostics.push({
|
|
130
|
+
code: "release_remote_tag_missing",
|
|
131
|
+
severity: "error",
|
|
132
|
+
message: `Release tag ${release.git.tag} is missing on origin.`,
|
|
133
|
+
path: release.git.tag,
|
|
134
|
+
suggestedFix: `Push or create the remote ${release.git.tag} tag before treating this release as complete.`
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
if (release.consumerPins.allKnownPinned !== true) {
|
|
138
|
+
diagnostics.push({
|
|
139
|
+
code: "release_consumer_pins_not_current",
|
|
140
|
+
severity: "error",
|
|
141
|
+
message: `First-party consumers are not all pinned to ${CLI_PACKAGE_NAME}@${release.localVersion}.`,
|
|
142
|
+
path: "topogram-cli.version",
|
|
143
|
+
suggestedFix: "Roll first-party consumer repositories to the current CLI version before treating this release as complete."
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
if (release.consumerCi.allCheckedAndPassing !== true) {
|
|
147
|
+
diagnostics.push({
|
|
148
|
+
code: "release_consumer_ci_not_current",
|
|
149
|
+
severity: "error",
|
|
150
|
+
message: "First-party consumer verification workflows are not all passing on the checked-out consumer commits.",
|
|
151
|
+
path: "GitHub Actions",
|
|
152
|
+
suggestedFix: "Wait for or fix the consumer verification workflows, then rerun `topogram release status --strict`."
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return diagnostics;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @param {ReturnType<typeof buildReleaseStatusPayload>} payload
|
|
160
|
+
* @returns {void}
|
|
161
|
+
*/
|
|
162
|
+
export function printReleaseStatus(payload) {
|
|
163
|
+
console.log(payload.ok ? "Topogram release status passed." : "Topogram release status found issues.");
|
|
164
|
+
if (payload.strict) {
|
|
165
|
+
console.log("Strict: enabled");
|
|
166
|
+
}
|
|
167
|
+
console.log(`Package: ${payload.packageName}`);
|
|
168
|
+
console.log(`Local version: ${payload.localVersion}`);
|
|
169
|
+
console.log(`Latest published: ${payload.latestVersion || "unknown"}${payload.currentPublished === true ? " (current)" : payload.currentPublished === false ? " (differs)" : ""}`);
|
|
170
|
+
console.log(`Git tag: ${payload.git.tag} local=${labelBoolean(payload.git.local)} remote=${labelBoolean(payload.git.remote)}`);
|
|
171
|
+
console.log(
|
|
172
|
+
`Consumer pins: ${payload.consumerPins.pinned}/${payload.consumerPins.known} pinned, ` +
|
|
173
|
+
`${payload.consumerPins.matching} matching, ${payload.consumerPins.differing} differing, ${payload.consumerPins.missing} missing`
|
|
174
|
+
);
|
|
175
|
+
if (payload.strict) {
|
|
176
|
+
console.log(
|
|
177
|
+
`Consumer CI: ${payload.consumerCi.passing}/${payload.consumerCi.checked} passing, ` +
|
|
178
|
+
`${payload.consumerCi.failing} failing, ${payload.consumerCi.unavailable} unavailable, ${payload.consumerCi.skipped} skipped`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
for (const consumer of payload.consumers) {
|
|
182
|
+
const status = consumer.matchesLocal === true
|
|
183
|
+
? "matches"
|
|
184
|
+
: consumer.matchesLocal === false
|
|
185
|
+
? "differs"
|
|
186
|
+
: "missing";
|
|
187
|
+
const ciStatus = consumer.ci?.run
|
|
188
|
+
? `; ${consumer.ci.run.workflowName || consumer.workflow}: ${consumer.ci.run.status || "unknown"}/${consumer.ci.run.conclusion || "unknown"}`
|
|
189
|
+
: consumer.ci?.checked
|
|
190
|
+
? `; ${consumer.workflow || "workflow"} unavailable`
|
|
191
|
+
: "";
|
|
192
|
+
console.log(`- ${consumer.name}: ${consumer.version || "missing"} (${status})${ciStatus}`);
|
|
193
|
+
if (consumer.ci?.run?.url) {
|
|
194
|
+
console.log(` CI: ${consumer.ci.run.url}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (payload.diagnostics.length > 0) {
|
|
198
|
+
console.log("Diagnostics:");
|
|
199
|
+
for (const diagnostic of payload.diagnostics) {
|
|
200
|
+
const label = diagnostic.severity === "warning"
|
|
201
|
+
? "Warning"
|
|
202
|
+
: diagnostic.severity === "info"
|
|
203
|
+
? "Note"
|
|
204
|
+
: "Error";
|
|
205
|
+
console.log(`- ${label}: ${diagnostic.message}`);
|
|
206
|
+
if (diagnostic.suggestedFix) {
|
|
207
|
+
console.log(` Fix: ${diagnostic.suggestedFix}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* @param {ReturnType<typeof buildReleaseStatusPayload>} payload
|
|
215
|
+
* @returns {string}
|
|
216
|
+
*/
|
|
217
|
+
export function renderReleaseStatusMarkdown(payload) {
|
|
218
|
+
const matrix = buildReleaseMatrixCatalogPayload();
|
|
219
|
+
const catalogConsumer = payload.consumers.find((consumer) => consumer.name === "topograms");
|
|
220
|
+
const demoConsumer = payload.consumers.find((consumer) => consumer.name === "topogram-demo-todo");
|
|
221
|
+
const catalogSlug = catalogRepoSlug();
|
|
222
|
+
const lines = [
|
|
223
|
+
"# Known-Good Release Matrix",
|
|
224
|
+
"",
|
|
225
|
+
"This matrix is generated by `topogram release status --strict --write-report`.",
|
|
226
|
+
`Date checked: ${new Date().toISOString().slice(0, 10)}.`,
|
|
227
|
+
"Treat it as a dated release audit, not a floating compatibility promise.",
|
|
228
|
+
"",
|
|
229
|
+
"## Summary",
|
|
230
|
+
"",
|
|
231
|
+
`- Package: \`${payload.packageName}@${payload.localVersion}\``,
|
|
232
|
+
`- Latest published: \`${payload.latestVersion || "unknown"}\`${payload.currentPublished === true ? " (current)" : payload.currentPublished === false ? " (differs)" : ""}`,
|
|
233
|
+
`- Release tag: \`${payload.git.tag}\` (local=${labelBoolean(payload.git.local)}, remote=${labelBoolean(payload.git.remote)})`,
|
|
234
|
+
`- Consumer pins: ${payload.consumerPins.matching}/${payload.consumerPins.known} matching`,
|
|
235
|
+
`- Consumer CI: ${payload.consumerCi.passing}/${payload.consumerCi.checked} passing`,
|
|
236
|
+
`- Strict status: ${payload.ok ? "passed" : "failed"}`,
|
|
237
|
+
"",
|
|
238
|
+
"## Core",
|
|
239
|
+
"",
|
|
240
|
+
"| Package or Repo | Version or Commit | Verification |",
|
|
241
|
+
"| --- | --- | --- |",
|
|
242
|
+
`| \`${payload.packageName}\` | \`${payload.localVersion}\` | Publish CLI Package, strict release status, fresh npmjs smoke, and installed CLI smoke passed |`,
|
|
243
|
+
`| \`${catalogSlug}\` catalog | \`${releaseMatrixConsumerCommit(catalogConsumer)}\` | ${releaseMatrixConsumerVerification(catalogConsumer, "Catalog Verification", payload.localVersion)} |`,
|
|
244
|
+
`| \`topogram-demo-todo\` | \`${releaseMatrixConsumerCommit(demoConsumer)}\` | ${releaseMatrixConsumerVerification(demoConsumer, "Demo Verification", payload.localVersion)} |`,
|
|
245
|
+
"",
|
|
246
|
+
"## Catalog Entries",
|
|
247
|
+
"",
|
|
248
|
+
"| Catalog ID | Kind | Package | Version | Stack |",
|
|
249
|
+
"| --- | --- | --- | --- | --- |"
|
|
250
|
+
];
|
|
251
|
+
if (matrix.entries.length > 0) {
|
|
252
|
+
for (const entry of matrix.entries) {
|
|
253
|
+
lines.push(`| \`${entry.id}\` | ${entry.kind} | \`${entry.package}\` | \`${entry.defaultVersion}\` | ${escapeMarkdownTableCell(entry.stack || "not declared")} |`);
|
|
254
|
+
}
|
|
255
|
+
} else {
|
|
256
|
+
lines.push("| unavailable | unavailable | unavailable | unavailable | Catalog could not be loaded for this report |");
|
|
257
|
+
}
|
|
258
|
+
lines.push(
|
|
259
|
+
"",
|
|
260
|
+
"## Generator Packages",
|
|
261
|
+
"",
|
|
262
|
+
"| Generator Package | Surface | Catalog usage |",
|
|
263
|
+
"| --- | --- | --- |"
|
|
264
|
+
);
|
|
265
|
+
if (matrix.generators.length > 0) {
|
|
266
|
+
for (const generator of matrix.generators) {
|
|
267
|
+
lines.push(`| \`${generator.package}\` | ${escapeMarkdownTableCell(generator.surface)} | ${escapeMarkdownTableCell(generator.catalogIds.join(", "))} |`);
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
lines.push("| unavailable | unavailable | Catalog generator metadata could not be loaded for this report |");
|
|
271
|
+
}
|
|
272
|
+
lines.push(
|
|
273
|
+
"",
|
|
274
|
+
"## Consumers",
|
|
275
|
+
"",
|
|
276
|
+
"| Repo | Pin | Workflow | Status | Run |",
|
|
277
|
+
"| --- | --- | --- | --- | --- |"
|
|
278
|
+
);
|
|
279
|
+
for (const consumer of payload.consumers) {
|
|
280
|
+
const workflow = consumer.workflow || consumer.ci?.expectedWorkflow || "";
|
|
281
|
+
const run = consumer.ci?.run;
|
|
282
|
+
const status = run ? `${run.status || "unknown"}/${run.conclusion || "unknown"}` : consumer.ci?.checked ? "unavailable" : "not checked";
|
|
283
|
+
const url = run?.url ? `[${run.databaseId || "run"}](${run.url})` : "";
|
|
284
|
+
lines.push(`| \`${consumer.name}\` | \`${consumer.version || "missing"}\` | ${escapeMarkdownTableCell(workflow)} | ${escapeMarkdownTableCell(status)} | ${url} |`);
|
|
285
|
+
}
|
|
286
|
+
lines.push(
|
|
287
|
+
"",
|
|
288
|
+
"## Consumer Proofs",
|
|
289
|
+
"",
|
|
290
|
+
"The external Todo demo is the canonical end-to-end consumer proof for the current catalog-backed workflow:",
|
|
291
|
+
"",
|
|
292
|
+
"```bash",
|
|
293
|
+
"topogram new ./todo-demo --template todo",
|
|
294
|
+
"cd ./todo-demo",
|
|
295
|
+
"npm install",
|
|
296
|
+
"npm run check",
|
|
297
|
+
"npm run generate",
|
|
298
|
+
"npm run app:compile",
|
|
299
|
+
"npm run verify",
|
|
300
|
+
"npm run app:runtime",
|
|
301
|
+
"```",
|
|
302
|
+
"",
|
|
303
|
+
"The demo CI also verifies `topogram new` from the default public catalog and from the repo-local catalog fixture. That prevents local fixtures from masking a broken published catalog alias."
|
|
304
|
+
);
|
|
305
|
+
const reportDiagnostics = [...matrix.diagnostics];
|
|
306
|
+
if (reportDiagnostics.length > 0) {
|
|
307
|
+
lines.push("", "## Report Diagnostics", "");
|
|
308
|
+
for (const diagnostic of reportDiagnostics) {
|
|
309
|
+
lines.push(`- **${diagnostic.severity || "warning"}** \`${diagnostic.code || "release_report_catalog_unavailable"}\`: ${diagnostic.message}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (payload.diagnostics.length > 0) {
|
|
313
|
+
lines.push("", "## Diagnostics", "");
|
|
314
|
+
for (const diagnostic of payload.diagnostics) {
|
|
315
|
+
const label = diagnostic.severity === "warning"
|
|
316
|
+
? "Warning"
|
|
317
|
+
: diagnostic.severity === "info"
|
|
318
|
+
? "Note"
|
|
319
|
+
: "Error";
|
|
320
|
+
lines.push(`- **${label}** \`${diagnostic.code}\`: ${diagnostic.message}`);
|
|
321
|
+
if (diagnostic.suggestedFix) {
|
|
322
|
+
lines.push(` Fix: ${diagnostic.suggestedFix}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return `${lines.join("\n")}\n`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* @returns {{ entries: any[], generators: Array<{ package: string, surface: string, catalogIds: string[] }>, diagnostics: Array<AnyRecord> }}
|
|
331
|
+
*/
|
|
332
|
+
function buildReleaseMatrixCatalogPayload() {
|
|
333
|
+
try {
|
|
334
|
+
const loaded = loadCatalog(null);
|
|
335
|
+
const entries = [...loaded.catalog.entries].sort((left, right) => {
|
|
336
|
+
if (left.kind !== right.kind) {
|
|
337
|
+
return left.kind === "template" ? -1 : 1;
|
|
338
|
+
}
|
|
339
|
+
return left.id.localeCompare(right.id);
|
|
340
|
+
});
|
|
341
|
+
const generatorMap = new Map();
|
|
342
|
+
for (const entry of entries) {
|
|
343
|
+
for (const packageName of Array.isArray(entry.generators) ? entry.generators : []) {
|
|
344
|
+
if (!generatorMap.has(packageName)) {
|
|
345
|
+
generatorMap.set(packageName, {
|
|
346
|
+
package: packageName,
|
|
347
|
+
surface: releaseMatrixGeneratorSurface(packageName),
|
|
348
|
+
catalogIds: []
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
generatorMap.get(packageName).catalogIds.push(entry.id);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const generators = [...generatorMap.values()].sort((left, right) => left.package.localeCompare(right.package));
|
|
355
|
+
return {
|
|
356
|
+
entries,
|
|
357
|
+
generators,
|
|
358
|
+
diagnostics: loaded.diagnostics || []
|
|
359
|
+
};
|
|
360
|
+
} catch (error) {
|
|
361
|
+
return {
|
|
362
|
+
entries: [],
|
|
363
|
+
generators: [],
|
|
364
|
+
diagnostics: [{
|
|
365
|
+
code: "release_report_catalog_unavailable",
|
|
366
|
+
severity: "warning",
|
|
367
|
+
message: messageFromError(error)
|
|
368
|
+
}]
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* @param {string} packageName
|
|
375
|
+
* @returns {string}
|
|
376
|
+
*/
|
|
377
|
+
function releaseMatrixGeneratorSurface(packageName) {
|
|
378
|
+
if (packageName.includes("-web")) return "web";
|
|
379
|
+
if (packageName.includes("-api")) return "api";
|
|
380
|
+
if (packageName.includes("-db")) return "database";
|
|
381
|
+
if (packageName.includes("-native")) return "native";
|
|
382
|
+
return "not declared";
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* @param {any} consumer
|
|
387
|
+
* @returns {string}
|
|
388
|
+
*/
|
|
389
|
+
function releaseMatrixConsumerCommit(consumer) {
|
|
390
|
+
return shortSha(consumer?.ci?.headSha || consumer?.ci?.run?.headSha || null) || "unknown";
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* @param {any} consumer
|
|
395
|
+
* @param {string} workflowName
|
|
396
|
+
* @param {string} version
|
|
397
|
+
* @returns {string}
|
|
398
|
+
*/
|
|
399
|
+
function releaseMatrixConsumerVerification(consumer, workflowName, version) {
|
|
400
|
+
const status = consumer?.ci?.run
|
|
401
|
+
? `${consumer.ci.run.status || "unknown"}/${consumer.ci.run.conclusion || "unknown"}`
|
|
402
|
+
: "not checked";
|
|
403
|
+
return `${workflowName}: ${status}; pinned ${CLI_PACKAGE_NAME}@${version}`;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* @param {string|null|undefined} value
|
|
408
|
+
* @returns {string|null}
|
|
409
|
+
*/
|
|
410
|
+
function shortSha(value) {
|
|
411
|
+
const text = String(value || "").trim();
|
|
412
|
+
return text ? text.slice(0, 7) : null;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* @param {string|null|undefined} value
|
|
417
|
+
* @returns {string}
|
|
418
|
+
*/
|
|
419
|
+
function escapeMarkdownTableCell(value) {
|
|
420
|
+
return String(value || "").replace(/\|/g, "\\|").replace(/\n/g, "<br>");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* @param {boolean|null} value
|
|
425
|
+
* @returns {string}
|
|
426
|
+
*/
|
|
427
|
+
function labelBoolean(value) {
|
|
428
|
+
return value === true ? "yes" : value === false ? "no" : "unknown";
|
|
429
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { stableStringify } from "../../format.js";
|
|
7
|
+
import {
|
|
8
|
+
buildReleaseRollConsumersPayload,
|
|
9
|
+
printReleaseRollConsumers
|
|
10
|
+
} from "./release-rollout.js";
|
|
11
|
+
import {
|
|
12
|
+
buildReleaseStatusPayload,
|
|
13
|
+
printReleaseStatus,
|
|
14
|
+
renderReleaseStatusMarkdown
|
|
15
|
+
} from "./release-status.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @returns {void}
|
|
19
|
+
*/
|
|
20
|
+
export function printReleaseHelp() {
|
|
21
|
+
console.log("Usage: topogram release status [--json] [--strict] [--markdown|--write-report <path>]");
|
|
22
|
+
console.log(" or: topogram release roll-consumers <version|--latest> [--json] [--no-push] [--watch]");
|
|
23
|
+
console.log("");
|
|
24
|
+
console.log("Checks the local CLI version, latest published package version, release tag, first-party consumer pins, and strict consumer CI state.");
|
|
25
|
+
console.log("Rolls first-party consumers to a published CLI version, runs their checks, commits, pushes, and can wait for current workflow runs.");
|
|
26
|
+
console.log("");
|
|
27
|
+
console.log("Examples:");
|
|
28
|
+
console.log(" topogram release status");
|
|
29
|
+
console.log(" topogram release status --json");
|
|
30
|
+
console.log(" topogram release status --strict");
|
|
31
|
+
console.log(" topogram release status --strict --write-report ./docs/release-matrix.md");
|
|
32
|
+
console.log(" topogram release roll-consumers 0.3.46 --watch");
|
|
33
|
+
console.log(" topogram release roll-consumers --latest --watch");
|
|
34
|
+
console.log("");
|
|
35
|
+
console.log("Release preparation and publishing are repo-level tasks in the Topogram source checkout:");
|
|
36
|
+
console.log(" npm run release:prepare -- <version>");
|
|
37
|
+
console.log(" npm run release:check");
|
|
38
|
+
console.log(" GitHub Actions: Publish CLI Package");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export {
|
|
42
|
+
buildReleaseRollConsumersPayload,
|
|
43
|
+
printReleaseRollConsumers
|
|
44
|
+
};
|
|
45
|
+
export {
|
|
46
|
+
buildReleaseStatusPayload,
|
|
47
|
+
printReleaseStatus,
|
|
48
|
+
renderReleaseStatusMarkdown
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {{ commandArgs: Record<string, any>, args: string[], json?: boolean }} context
|
|
53
|
+
* @returns {number}
|
|
54
|
+
*/
|
|
55
|
+
export function runReleaseCommand(context) {
|
|
56
|
+
const { commandArgs, args, json = false } = context;
|
|
57
|
+
const command = commandArgs.releaseCommand;
|
|
58
|
+
const reportIndex = args.indexOf("--write-report");
|
|
59
|
+
const reportPath = reportIndex >= 0 &&
|
|
60
|
+
args[reportIndex + 1] &&
|
|
61
|
+
!args[reportIndex + 1].startsWith("-")
|
|
62
|
+
? args[reportIndex + 1]
|
|
63
|
+
: null;
|
|
64
|
+
if (command === "status") {
|
|
65
|
+
if (args.includes("--write-report") && !reportPath) {
|
|
66
|
+
console.error("Missing required --write-report <path>.");
|
|
67
|
+
printReleaseHelp();
|
|
68
|
+
return 1;
|
|
69
|
+
}
|
|
70
|
+
const payload = buildReleaseStatusPayload({ strict: args.includes("--strict") });
|
|
71
|
+
if (reportPath) {
|
|
72
|
+
const target = path.resolve(reportPath);
|
|
73
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
74
|
+
fs.writeFileSync(target, renderReleaseStatusMarkdown(payload), "utf8");
|
|
75
|
+
}
|
|
76
|
+
if (json) {
|
|
77
|
+
console.log(stableStringify(payload));
|
|
78
|
+
} else if (args.includes("--markdown")) {
|
|
79
|
+
console.log(renderReleaseStatusMarkdown(payload).trimEnd());
|
|
80
|
+
} else {
|
|
81
|
+
printReleaseStatus(payload);
|
|
82
|
+
if (reportPath) {
|
|
83
|
+
console.log(`Report: ${path.resolve(reportPath)}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return payload.ok ? 0 : 1;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (command === "roll-consumers") {
|
|
90
|
+
const push = !args.includes("--no-push");
|
|
91
|
+
const watch = args.includes("--watch");
|
|
92
|
+
if (watch && !push) {
|
|
93
|
+
console.error("Use either --watch or --no-push, not both.");
|
|
94
|
+
printReleaseHelp();
|
|
95
|
+
return 1;
|
|
96
|
+
}
|
|
97
|
+
const payload = buildReleaseRollConsumersPayload(commandArgs.releaseRollVersion, { push, watch });
|
|
98
|
+
if (json) {
|
|
99
|
+
console.log(stableStringify(payload));
|
|
100
|
+
} else {
|
|
101
|
+
printReleaseRollConsumers(payload);
|
|
102
|
+
}
|
|
103
|
+
return payload.ok ? 0 : 1;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
throw new Error(`Unknown release command '${command}'`);
|
|
107
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { stableStringify } from "../../format.js";
|
|
6
|
+
import { parsePath } from "../../parser.js";
|
|
7
|
+
import { resolveWorkspace } from "../../resolver.js";
|
|
8
|
+
import { formatValidationErrors } from "../../validator.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {Record<string, any>} AnyRecord
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {string[]} args
|
|
16
|
+
* @param {string} flag
|
|
17
|
+
* @returns {string|null}
|
|
18
|
+
*/
|
|
19
|
+
function flagValue(args, flag) {
|
|
20
|
+
const index = args.indexOf(flag);
|
|
21
|
+
return index >= 0 ? args[index + 1] || null : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {string[]} args
|
|
26
|
+
* @returns {boolean}
|
|
27
|
+
*/
|
|
28
|
+
function includeHistory(args) {
|
|
29
|
+
return args.includes("--history") || args.includes("--include-history");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} sdlcRoot
|
|
34
|
+
* @returns {AnyRecord|null}
|
|
35
|
+
*/
|
|
36
|
+
function resolveSdlcWorkspace(sdlcRoot) {
|
|
37
|
+
const ast = parsePath(sdlcRoot);
|
|
38
|
+
const resolved = resolveWorkspace(ast);
|
|
39
|
+
if (!resolved.ok) {
|
|
40
|
+
console.error(formatValidationErrors(resolved.validation));
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return resolved;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Runs `topogram sdlc ...` commands and the legacy top-level `topogram release`
|
|
48
|
+
* command.
|
|
49
|
+
*
|
|
50
|
+
* @param {{
|
|
51
|
+
* commandArgs: AnyRecord,
|
|
52
|
+
* args: string[],
|
|
53
|
+
* inputPath: string|null|undefined
|
|
54
|
+
* }} context
|
|
55
|
+
* @returns {Promise<number>}
|
|
56
|
+
*/
|
|
57
|
+
export async function runSdlcCommand(context) {
|
|
58
|
+
const { commandArgs, args } = context;
|
|
59
|
+
const sdlcRoot = path.resolve(context.inputPath || ".");
|
|
60
|
+
const actor = flagValue(args, "--actor");
|
|
61
|
+
const note = flagValue(args, "--note");
|
|
62
|
+
const status = flagValue(args, "--status");
|
|
63
|
+
const before = flagValue(args, "--before");
|
|
64
|
+
const appVersion = flagValue(args, "--app-version");
|
|
65
|
+
const sinceTag = flagValue(args, "--since-tag");
|
|
66
|
+
const dryRun = args.includes("--dry-run");
|
|
67
|
+
const strict = args.includes("--strict");
|
|
68
|
+
|
|
69
|
+
if (commandArgs.sdlcCommand === "transition") {
|
|
70
|
+
const { transitionStatement } = await import("../../sdlc/transition.js");
|
|
71
|
+
const result = transitionStatement(sdlcRoot, commandArgs.sdlcId, commandArgs.sdlcTargetStatus, {
|
|
72
|
+
actor,
|
|
73
|
+
note,
|
|
74
|
+
dryRun
|
|
75
|
+
});
|
|
76
|
+
console.log(stableStringify(result));
|
|
77
|
+
return result.ok ? 0 : 1;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (commandArgs.sdlcCommand === "check") {
|
|
81
|
+
const { checkWorkspace } = await import("../../sdlc/check.js");
|
|
82
|
+
const resolved = resolveSdlcWorkspace(sdlcRoot);
|
|
83
|
+
if (!resolved) {
|
|
84
|
+
return 1;
|
|
85
|
+
}
|
|
86
|
+
const result = checkWorkspace(sdlcRoot, resolved);
|
|
87
|
+
console.log(stableStringify(result));
|
|
88
|
+
return strict && (!result.ok || result.warnings.length > 0) ? 1 : 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (commandArgs.sdlcCommand === "explain") {
|
|
92
|
+
const { explain } = await import("../../sdlc/explain.js");
|
|
93
|
+
const resolved = resolveSdlcWorkspace(sdlcRoot);
|
|
94
|
+
if (!resolved) {
|
|
95
|
+
return 1;
|
|
96
|
+
}
|
|
97
|
+
const result = explain(sdlcRoot, resolved, commandArgs.sdlcId, {
|
|
98
|
+
includeHistory: includeHistory(args)
|
|
99
|
+
});
|
|
100
|
+
if (args.includes("--brief") && result.ok) {
|
|
101
|
+
console.log(stableStringify({
|
|
102
|
+
id: result.id,
|
|
103
|
+
status: result.status,
|
|
104
|
+
next_action: result.next_action
|
|
105
|
+
}));
|
|
106
|
+
} else {
|
|
107
|
+
console.log(stableStringify(result));
|
|
108
|
+
}
|
|
109
|
+
return result.ok ? 0 : 1;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (commandArgs.sdlcCommand === "archive") {
|
|
113
|
+
const { archiveBatch, archiveEligibleStatements } = await import("../../archive/archive.js");
|
|
114
|
+
const resolved = resolveSdlcWorkspace(sdlcRoot);
|
|
115
|
+
if (!resolved) {
|
|
116
|
+
return 1;
|
|
117
|
+
}
|
|
118
|
+
const ids = archiveEligibleStatements(resolved, {
|
|
119
|
+
before,
|
|
120
|
+
statuses: status ? status.split(",") : null
|
|
121
|
+
});
|
|
122
|
+
const result = archiveBatch(sdlcRoot, ids, { dryRun, by: actor, reason: note });
|
|
123
|
+
console.log(stableStringify({ candidates: ids, ...result }));
|
|
124
|
+
return result.ok ? 0 : 1;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (commandArgs.sdlcCommand === "unarchive") {
|
|
128
|
+
const { unarchive } = await import("../../archive/unarchive.js");
|
|
129
|
+
const result = unarchive(sdlcRoot, commandArgs.sdlcId, {});
|
|
130
|
+
console.log(stableStringify(result));
|
|
131
|
+
return result.ok ? 0 : 1;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (commandArgs.sdlcCommand === "compact") {
|
|
135
|
+
const { compact } = await import("../../archive/compact.js");
|
|
136
|
+
const result = compact(path.resolve(commandArgs.sdlcArchiveFile));
|
|
137
|
+
console.log(stableStringify(result));
|
|
138
|
+
return result.ok ? 0 : 1;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (commandArgs.sdlcCommand === "new") {
|
|
142
|
+
const { scaffoldNew } = await import("../../sdlc/scaffold.js");
|
|
143
|
+
const result = scaffoldNew(sdlcRoot, commandArgs.sdlcNewKind, commandArgs.sdlcNewSlug);
|
|
144
|
+
console.log(stableStringify(result));
|
|
145
|
+
return result.ok ? 0 : 1;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (commandArgs.sdlcCommand === "adopt") {
|
|
149
|
+
const { sdlcAdopt } = await import("../../sdlc/adopt.js");
|
|
150
|
+
const result = sdlcAdopt(sdlcRoot);
|
|
151
|
+
console.log(stableStringify(result));
|
|
152
|
+
return result.ok ? 0 : 1;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (commandArgs.sdlcCommand === "release") {
|
|
156
|
+
const { runRelease } = await import("../../sdlc/release.js");
|
|
157
|
+
const result = runRelease(sdlcRoot, {
|
|
158
|
+
appVersion,
|
|
159
|
+
sinceTag,
|
|
160
|
+
dryRun,
|
|
161
|
+
actor
|
|
162
|
+
});
|
|
163
|
+
console.log(stableStringify(result));
|
|
164
|
+
return result.ok ? 0 : 1;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
throw new Error(`Unknown sdlc command '${commandArgs.sdlcCommand}'`);
|
|
168
|
+
}
|