@topogram/cli 0.3.64 → 0.3.65

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 (245) hide show
  1. package/package.json +1 -1
  2. package/src/adoption/plan/index.js +703 -0
  3. package/src/adoption/plan.js +12 -703
  4. package/src/agent-ops/query-builders/auth.js +375 -0
  5. package/src/agent-ops/query-builders/change-risk/change-plan.js +123 -0
  6. package/src/agent-ops/query-builders/change-risk/import-plan.js +49 -0
  7. package/src/agent-ops/query-builders/change-risk/maintained.js +286 -0
  8. package/src/agent-ops/query-builders/change-risk/review-packets.js +123 -0
  9. package/src/agent-ops/query-builders/change-risk/risk.js +189 -0
  10. package/src/agent-ops/query-builders/change-risk.js +25 -0
  11. package/src/agent-ops/query-builders/common.js +149 -0
  12. package/src/agent-ops/query-builders/maintained-risk.js +539 -0
  13. package/src/agent-ops/query-builders/maintained-shared.js +120 -0
  14. package/src/agent-ops/query-builders/multi-agent.js +547 -0
  15. package/src/agent-ops/query-builders/projection-impacts.js +514 -0
  16. package/src/agent-ops/query-builders/work-packets.js +417 -0
  17. package/src/agent-ops/query-builders/workflow-context-shared.js +300 -0
  18. package/src/agent-ops/query-builders/workflow-context.js +398 -0
  19. package/src/agent-ops/query-builders/workflow-presets-core.js +676 -0
  20. package/src/agent-ops/query-builders/workflow-presets.js +341 -0
  21. package/src/agent-ops/query-builders.d.ts +26 -26
  22. package/src/agent-ops/query-builders.js +42 -5021
  23. package/src/catalog/constants.js +10 -0
  24. package/src/catalog/copy.js +60 -0
  25. package/src/catalog/diagnostics.js +15 -0
  26. package/src/catalog/entries.js +42 -0
  27. package/src/catalog/files.js +67 -0
  28. package/src/catalog/provenance.js +122 -0
  29. package/src/catalog/source.js +150 -0
  30. package/src/catalog/validation.js +252 -0
  31. package/src/catalog.d.ts +2 -0
  32. package/src/catalog.js +18 -746
  33. package/src/cli/commands/catalog/check.js +31 -0
  34. package/src/cli/commands/catalog/copy.js +59 -0
  35. package/src/cli/commands/catalog/doctor.js +248 -0
  36. package/src/cli/commands/catalog/help.js +21 -0
  37. package/src/cli/commands/catalog/list.js +52 -0
  38. package/src/cli/commands/catalog/runner.js +92 -0
  39. package/src/cli/commands/catalog/shared.js +17 -0
  40. package/src/cli/commands/catalog/show.js +134 -0
  41. package/src/cli/commands/catalog.js +30 -615
  42. package/src/cli/commands/generator-policy/package-info.js +162 -0
  43. package/src/cli/commands/generator-policy/payloads.js +372 -0
  44. package/src/cli/commands/generator-policy/printers.js +159 -0
  45. package/src/cli/commands/generator-policy/runner.js +81 -0
  46. package/src/cli/commands/generator-policy/shared.js +39 -0
  47. package/src/cli/commands/generator-policy.js +15 -783
  48. package/src/cli/commands/import/adopt.js +170 -0
  49. package/src/cli/commands/import/check.js +91 -0
  50. package/src/cli/commands/import/diff.js +84 -0
  51. package/src/cli/commands/import/help.js +47 -0
  52. package/src/cli/commands/import/paths.js +277 -0
  53. package/src/cli/commands/import/plan.js +284 -0
  54. package/src/cli/commands/import/refresh.js +470 -0
  55. package/src/cli/commands/import/status-history.js +196 -0
  56. package/src/cli/commands/import/workspace.js +230 -0
  57. package/src/cli/commands/import.js +33 -1732
  58. package/src/cli/commands/package/constants.js +17 -0
  59. package/src/cli/commands/package/doctor.js +240 -0
  60. package/src/cli/commands/package/help.js +27 -0
  61. package/src/cli/commands/package/lockfile.js +135 -0
  62. package/src/cli/commands/package/npm.js +97 -0
  63. package/src/cli/commands/package/reporting.js +35 -0
  64. package/src/cli/commands/package/runner.js +33 -0
  65. package/src/cli/commands/package/shared.js +9 -0
  66. package/src/cli/commands/package/update-cli.js +252 -0
  67. package/src/cli/commands/package/versions.js +35 -0
  68. package/src/cli/commands/package.js +29 -813
  69. package/src/cli/commands/query/change-plan.js +68 -0
  70. package/src/cli/commands/query/definitions.js +202 -0
  71. package/src/cli/commands/query/import-adopt.js +121 -0
  72. package/src/cli/commands/query/runner/artifacts.js +102 -0
  73. package/src/cli/commands/query/runner/boundaries.js +211 -0
  74. package/src/cli/commands/query/runner/change.js +182 -0
  75. package/src/cli/commands/query/runner/import-adopt.js +111 -0
  76. package/src/cli/commands/query/runner/index.js +31 -0
  77. package/src/cli/commands/query/runner/output.js +12 -0
  78. package/src/cli/commands/query/runner/workflow.js +241 -0
  79. package/src/cli/commands/query/runner.js +3 -0
  80. package/src/cli/commands/query/workflow-context.js +5 -0
  81. package/src/cli/commands/query/workspace.js +274 -0
  82. package/src/cli/commands/query.js +9 -1300
  83. package/src/cli/commands/template/baseline.js +100 -0
  84. package/src/cli/commands/template/check.js +466 -0
  85. package/src/cli/commands/template/constants.js +8 -0
  86. package/src/cli/commands/template/diagnostics.js +26 -0
  87. package/src/cli/commands/template/help.js +28 -0
  88. package/src/cli/commands/template/lifecycle.js +404 -0
  89. package/src/cli/commands/template/list-show.js +287 -0
  90. package/src/cli/commands/template/policy.js +422 -0
  91. package/src/cli/commands/template/shared.js +127 -0
  92. package/src/cli/commands/template/updates.js +352 -0
  93. package/src/cli/commands/template.js +41 -2143
  94. package/src/generator/api/contracts.js +497 -0
  95. package/src/generator/api/metadata.js +221 -0
  96. package/src/generator/api/openapi.js +559 -0
  97. package/src/generator/api/schema.js +124 -0
  98. package/src/generator/api/types.d.ts +98 -0
  99. package/src/generator/api.js +3 -1195
  100. package/src/generator/context/shared/domain-sdlc.js +282 -0
  101. package/src/generator/context/shared/maintained-boundary.js +665 -0
  102. package/src/generator/context/shared/metrics.js +85 -0
  103. package/src/generator/context/shared/primitives.js +64 -0
  104. package/src/generator/context/shared/relationships.js +453 -0
  105. package/src/generator/context/shared/summaries.js +263 -0
  106. package/src/generator/context/shared/types.d.ts +207 -0
  107. package/src/generator/context/shared.d.ts +42 -0
  108. package/src/generator/context/shared.js +80 -1390
  109. package/src/generator/context/slice/core.js +397 -0
  110. package/src/generator/context/slice/sdlc.js +417 -0
  111. package/src/generator/context/slice/ui-packets.js +183 -0
  112. package/src/generator/context/slice.js +2 -859
  113. package/src/generator/registry/index.js +507 -0
  114. package/src/generator/registry.js +18 -504
  115. package/src/generator/runtime/environment/index.js +666 -0
  116. package/src/generator/runtime/environment.js +4 -666
  117. package/src/generator/runtime/runtime-check/index.js +554 -0
  118. package/src/generator/runtime/runtime-check.js +4 -554
  119. package/src/generator/runtime/shared/index.js +572 -0
  120. package/src/generator/runtime/shared.js +19 -570
  121. package/src/generator/shared.d.ts +2 -0
  122. package/src/generator/surfaces/shared.d.ts +3 -0
  123. package/src/generator/widget-conformance/behavior-report.js +258 -0
  124. package/src/generator/widget-conformance/checks.js +371 -0
  125. package/src/generator/widget-conformance/projection-context.js +200 -0
  126. package/src/generator/widget-conformance/report.js +166 -0
  127. package/src/generator/widget-conformance/types.d.ts +121 -0
  128. package/src/generator/widget-conformance.js +3 -824
  129. package/src/import/core/context.d.ts +3 -0
  130. package/src/import/core/contracts.d.ts +1 -0
  131. package/src/import/core/registry.d.ts +4 -0
  132. package/src/import/core/runner/candidates.js +217 -0
  133. package/src/import/core/runner/options.js +22 -0
  134. package/src/import/core/runner/reports.js +50 -0
  135. package/src/import/core/runner/run.js +79 -0
  136. package/src/import/core/runner/tracks.js +150 -0
  137. package/src/import/core/runner/ui-drafts.js +337 -0
  138. package/src/import/core/runner.js +3 -698
  139. package/src/import/core/shared/api-routes.js +221 -0
  140. package/src/import/core/shared/candidates.js +97 -0
  141. package/src/import/core/shared/files.js +177 -0
  142. package/src/import/core/shared/next-app.js +389 -0
  143. package/src/import/core/shared/types.d.ts +51 -0
  144. package/src/import/core/shared/ui-routes.js +230 -0
  145. package/src/import/core/shared.js +60 -861
  146. package/src/new-project/constants.js +128 -0
  147. package/src/new-project/create.js +83 -0
  148. package/src/new-project/json.js +28 -0
  149. package/src/new-project/metadata.js +96 -0
  150. package/src/new-project/package-spec.js +161 -0
  151. package/src/new-project/project-files.js +348 -0
  152. package/src/new-project/template-policy.js +269 -0
  153. package/src/new-project/template-resolution.js +368 -0
  154. package/src/new-project/template-snapshots.js +430 -0
  155. package/src/new-project/template-updates.js +512 -0
  156. package/src/new-project/types.d.ts +83 -0
  157. package/src/new-project.js +6 -2277
  158. package/src/parser.d.ts +87 -1
  159. package/src/parser.js +118 -0
  160. package/src/policy/review-boundaries.d.ts +15 -0
  161. package/src/project-config/index.js +564 -0
  162. package/src/project-config.js +19 -561
  163. package/src/resolver/enrich/acceptance-criterion.js +2 -0
  164. package/src/resolver/enrich/bug.js +2 -0
  165. package/src/resolver/enrich/pitch.js +2 -0
  166. package/src/resolver/enrich/requirement.js +2 -0
  167. package/src/resolver/enrich/task.js +2 -0
  168. package/src/resolver/index.js +19 -2089
  169. package/src/resolver/normalize.js +384 -1
  170. package/src/resolver/plans.js +168 -0
  171. package/src/resolver/projections-api.js +494 -0
  172. package/src/resolver/projections-db.js +133 -0
  173. package/src/resolver/projections-ui.js +317 -0
  174. package/src/resolver/shapes.js +251 -0
  175. package/src/resolver/shared.js +278 -0
  176. package/src/resolver/widgets.js +132 -0
  177. package/src/template-trust/constants.js +62 -0
  178. package/src/template-trust/content.js +258 -0
  179. package/src/template-trust/diff.js +92 -0
  180. package/src/template-trust/policy.js +61 -0
  181. package/src/template-trust/record.js +90 -0
  182. package/src/template-trust/status.js +182 -0
  183. package/src/template-trust.js +24 -687
  184. package/src/text-helpers.d.ts +1 -0
  185. package/src/topogram-types.d.ts +69 -0
  186. package/src/validator/common.js +488 -0
  187. package/src/validator/data-model.js +237 -0
  188. package/src/validator/docs.js +167 -0
  189. package/src/validator/expressions.js +146 -1
  190. package/src/validator/index.d.ts +23 -0
  191. package/src/validator/index.js +32 -3585
  192. package/src/validator/kinds.d.ts +41 -0
  193. package/src/validator/kinds.js +2 -0
  194. package/src/validator/model-helpers.js +46 -0
  195. package/src/validator/per-kind/acceptance-criterion.js +5 -0
  196. package/src/validator/per-kind/bug.js +6 -0
  197. package/src/validator/per-kind/domain.js +15 -2
  198. package/src/validator/per-kind/pitch.js +7 -0
  199. package/src/validator/per-kind/requirement.js +5 -0
  200. package/src/validator/per-kind/task.js +7 -0
  201. package/src/validator/per-kind/widget.js +14 -0
  202. package/src/validator/projections/api-http-async.js +410 -0
  203. package/src/validator/projections/api-http-authz.js +88 -0
  204. package/src/validator/projections/api-http-core.js +205 -0
  205. package/src/validator/projections/api-http-policies.js +339 -0
  206. package/src/validator/projections/api-http-responses.js +233 -0
  207. package/src/validator/projections/api-http.js +44 -0
  208. package/src/validator/projections/db.js +353 -0
  209. package/src/validator/projections/generator-defaults.js +45 -0
  210. package/src/validator/projections/helpers.js +87 -0
  211. package/src/validator/projections/ui-helpers.js +214 -0
  212. package/src/validator/projections/ui-navigation.js +344 -0
  213. package/src/validator/projections/ui-structure.js +364 -0
  214. package/src/validator/projections/ui-widgets.js +493 -0
  215. package/src/validator/projections/ui.js +46 -0
  216. package/src/validator/registry.js +48 -1
  217. package/src/validator/utils.d.ts +20 -0
  218. package/src/validator/utils.js +115 -12
  219. package/src/widget-behavior.d.ts +1 -0
  220. package/src/workflows/import-app/api/collect.js +221 -0
  221. package/src/workflows/import-app/api/openapi.js +257 -0
  222. package/src/workflows/import-app/api/routes.js +327 -0
  223. package/src/workflows/import-app/api/sources.js +22 -0
  224. package/src/workflows/import-app/api.js +2 -797
  225. package/src/workflows/reconcile/adoption-plan/build.js +208 -0
  226. package/src/workflows/reconcile/adoption-plan/dependencies.js +75 -0
  227. package/src/workflows/reconcile/adoption-plan/outputs.js +143 -0
  228. package/src/workflows/reconcile/adoption-plan/paths.js +58 -0
  229. package/src/workflows/reconcile/adoption-plan/projection-patches.js +177 -0
  230. package/src/workflows/reconcile/adoption-plan/reasons.js +107 -0
  231. package/src/workflows/reconcile/adoption-plan.js +30 -740
  232. package/src/workflows/reconcile/auth/closures.js +115 -0
  233. package/src/workflows/reconcile/auth/formatters.js +142 -0
  234. package/src/workflows/reconcile/auth/inference.js +330 -0
  235. package/src/workflows/reconcile/auth/roles.js +122 -0
  236. package/src/workflows/reconcile/auth.js +35 -690
  237. package/src/workflows/reconcile/bundle-core/index.js +600 -0
  238. package/src/workflows/reconcile/bundle-core.js +12 -598
  239. package/src/workflows/reconcile/canonical-surface.js +1 -1
  240. package/src/workflows/reconcile/impacts/adoption-plan.js +192 -0
  241. package/src/workflows/reconcile/impacts/indexes.js +101 -0
  242. package/src/workflows/reconcile/impacts/patches.js +252 -0
  243. package/src/workflows/reconcile/impacts/reports.js +80 -0
  244. package/src/workflows/reconcile/impacts.js +14 -623
  245. package/src/workspace-docs.d.ts +29 -0
package/src/catalog.js CHANGED
@@ -1,748 +1,20 @@
1
1
  // @ts-check
2
2
 
3
- import childProcess from "node:child_process";
4
- import crypto from "node:crypto";
5
- import fs from "node:fs";
6
- import path from "node:path";
7
-
8
- import { installPackageSpec } from "./new-project.js";
9
- import { DEFAULT_CATALOG_SOURCE, defaultCatalogSource } from "./topogram-config.js";
10
- import { readGithubCatalogSourceText } from "./github-client.js";
11
-
12
- export const CATALOG_FILE_NAME = "topograms.catalog.json";
13
- export const TOPOGRAM_SOURCE_FILE = ".topogram-source.json";
14
- const KNOWN_CATALOG_SURFACES = new Set(["web", "api", "database", "native"]);
15
- const GITHUB_TOKEN_HOSTS = new Set([
16
- "github.com",
17
- "api.github.com",
18
- "raw.githubusercontent.com"
19
- ]);
20
- const FETCH_URL_SCRIPT = `
21
- const source = process.argv[1];
22
- const token = process.env.TOPOGRAM_FETCH_TOKEN || "";
23
- const tokenHosts = new Set(["github.com", "api.github.com", "raw.githubusercontent.com"]);
24
- function tokenAllowed(url) {
25
- const hostname = new URL(url).hostname.toLowerCase();
26
- return tokenHosts.has(hostname) || hostname.endsWith(".github.com");
27
- }
28
- async function readUrl(url, redirects = 0) {
29
- if (redirects > 5) {
30
- throw new Error("Too many redirects.");
31
- }
32
- const headers = {};
33
- if (token && tokenAllowed(url)) {
34
- headers.authorization = "Bearer " + token;
35
- }
36
- const response = await fetch(url, { headers, redirect: "manual" });
37
- if (response.status >= 300 && response.status < 400 && response.headers.get("location")) {
38
- const next = new URL(response.headers.get("location"), url).toString();
39
- return readUrl(next, redirects + 1);
40
- }
41
- const text = await response.text();
42
- if (!response.ok) {
43
- const preview = text.trim().slice(0, 400);
44
- throw new Error(String(response.status) + " " + response.statusText + (preview ? "\\n" + preview : ""));
45
- }
46
- return text;
47
- }
48
- try {
49
- process.stdout.write(await readUrl(source));
50
- } catch (error) {
51
- console.error(error instanceof Error ? error.message : String(error));
52
- process.exit(1);
53
- }
54
- `;
55
-
56
- /**
57
- * @typedef {Object} CatalogTrust
58
- * @property {string} scope
59
- * @property {boolean} includesExecutableImplementation
60
- * @property {string} [notes]
61
- */
62
-
63
- /**
64
- * @typedef {Object} CatalogEntry
65
- * @property {string} id
66
- * @property {"template"|"topogram"} kind
67
- * @property {string} package
68
- * @property {string} defaultVersion
69
- * @property {string} description
70
- * @property {string[]} tags
71
- * @property {string[]} [surfaces]
72
- * @property {string[]} [generators]
73
- * @property {string} [stack]
74
- * @property {CatalogTrust} trust
75
- */
76
-
77
- /**
78
- * @typedef {Object} TopogramCatalog
79
- * @property {string} version
80
- * @property {CatalogEntry[]} entries
81
- */
82
-
83
- /**
84
- * @typedef {Object} CatalogDiagnostic
85
- * @property {string} code
86
- * @property {"error"|"warning"} severity
87
- * @property {string} message
88
- * @property {string|null} path
89
- * @property {string|null} suggestedFix
90
- */
91
-
92
- /**
93
- * @typedef {Object} CatalogValidationResult
94
- * @property {boolean} ok
95
- * @property {TopogramCatalog|null} catalog
96
- * @property {CatalogDiagnostic[]} diagnostics
97
- * @property {string[]} errors
98
- */
99
-
100
- /**
101
- * @typedef {Object} CatalogLoadResult
102
- * @property {string} source
103
- * @property {TopogramCatalog} catalog
104
- * @property {CatalogDiagnostic[]} diagnostics
105
- */
106
-
107
- /**
108
- * @param {Record<string, unknown>} input
109
- * @returns {CatalogDiagnostic}
110
- */
111
- function catalogDiagnostic(input) {
112
- return {
113
- code: String(input.code || "catalog_invalid"),
114
- severity: input.severity === "warning" ? "warning" : "error",
115
- message: String(input.message || "Catalog is invalid."),
116
- path: typeof input.path === "string" ? input.path : null,
117
- suggestedFix: typeof input.suggestedFix === "string" ? input.suggestedFix : null
118
- };
119
- }
120
-
121
- /**
122
- * @param {string|undefined|null} source
123
- * @returns {string}
124
- */
125
- export function catalogSourceOrDefault(source = null) {
126
- return source || process.env.TOPOGRAM_CATALOG_SOURCE || defaultCatalogSource();
127
- }
128
-
129
- /**
130
- * @param {string|undefined|null} source
131
- * @returns {boolean}
132
- */
133
- export function isCatalogSourceDisabled(source) {
134
- const normalized = String(source || "").trim().toLowerCase();
135
- return normalized === "none" || normalized === "off" || normalized === "false";
136
- }
137
-
138
- /**
139
- * @param {string} value
140
- * @returns {boolean}
141
- */
142
- function isPackageName(value) {
143
- return /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i.test(value);
144
- }
145
-
146
- /**
147
- * @param {unknown} value
148
- * @param {string} source
149
- * @returns {CatalogValidationResult}
150
- */
151
- export function validateCatalog(value, source = "") {
152
- /** @type {CatalogDiagnostic[]} */
153
- const diagnostics = [];
154
- if (!value || typeof value !== "object" || Array.isArray(value)) {
155
- diagnostics.push(catalogDiagnostic({
156
- code: "catalog_not_object",
157
- message: "Catalog must contain a JSON object.",
158
- path: source || null,
159
- suggestedFix: `Create ${CATALOG_FILE_NAME} with version and entries[].`
160
- }));
161
- return validationResult(null, diagnostics);
162
- }
163
-
164
- const input = /** @type {Record<string, unknown>} */ (value);
165
- const version = typeof input.version === "string" && input.version ? input.version : "";
166
- if (!version) {
167
- diagnostics.push(catalogDiagnostic({
168
- code: "catalog_version_missing",
169
- message: "Catalog is missing required string field 'version'.",
170
- path: source || null,
171
- suggestedFix: "Add a version string such as \"0.1\"."
172
- }));
173
- }
174
- if (!Array.isArray(input.entries)) {
175
- diagnostics.push(catalogDiagnostic({
176
- code: "catalog_entries_missing",
177
- message: "Catalog is missing required array field 'entries'.",
178
- path: source || null,
179
- suggestedFix: "Add entries[] with template and topogram package references."
180
- }));
181
- return validationResult({ version, entries: [] }, diagnostics);
182
- }
183
-
184
- /** @type {CatalogEntry[]} */
185
- const entries = [];
186
- const ids = new Set();
187
- input.entries.forEach((entryValue, index) => {
188
- const entryPath = `entries[${index}]`;
189
- if (!entryValue || typeof entryValue !== "object" || Array.isArray(entryValue)) {
190
- diagnostics.push(catalogDiagnostic({
191
- code: "catalog_entry_not_object",
192
- message: `Catalog ${entryPath} must be an object.`,
193
- path: source || null,
194
- suggestedFix: "Replace the entry with an object containing id, kind, package, defaultVersion, description, tags, and trust."
195
- }));
196
- return;
197
- }
198
- const entry = /** @type {Record<string, unknown>} */ (entryValue);
199
- const id = stringField(entry, "id");
200
- const kind = stringField(entry, "kind");
201
- const packageName = stringField(entry, "package");
202
- const defaultVersion = stringField(entry, "defaultVersion");
203
- const description = stringField(entry, "description");
204
- const tags = Array.isArray(entry.tags) ? entry.tags.map(String).filter(Boolean) : [];
205
- const surfaces = Array.isArray(entry.surfaces) ? entry.surfaces.map(String).filter(Boolean) : [];
206
- const generators = Array.isArray(entry.generators) ? entry.generators.map(String).filter(Boolean) : [];
207
- const stack = stringField(entry, "stack");
208
- const trust = trustField(entry.trust);
209
-
210
- for (const field of ["id", "kind", "package", "defaultVersion", "description"]) {
211
- if (!stringField(entry, field)) {
212
- diagnostics.push(catalogDiagnostic({
213
- code: "catalog_entry_field_missing",
214
- message: `Catalog ${entryPath} is missing required string field '${field}'.`,
215
- path: source || null,
216
- suggestedFix: `Add ${field} to ${entryPath}.`
217
- }));
218
- }
219
- }
220
- if (id && ids.has(id)) {
221
- diagnostics.push(catalogDiagnostic({
222
- code: "catalog_duplicate_id",
223
- message: `Catalog entry id '${id}' is duplicated.`,
224
- path: source || null,
225
- suggestedFix: "Use stable unique ids for catalog entries."
226
- }));
227
- }
228
- if (id) {
229
- ids.add(id);
230
- }
231
- if (kind && kind !== "template" && kind !== "topogram") {
232
- diagnostics.push(catalogDiagnostic({
233
- code: "catalog_invalid_kind",
234
- message: `Catalog entry '${id || entryPath}' has invalid kind '${kind}'.`,
235
- path: source || null,
236
- suggestedFix: "Use kind \"template\" or \"topogram\"."
237
- }));
238
- }
239
- if (packageName && !isPackageName(packageName)) {
240
- diagnostics.push(catalogDiagnostic({
241
- code: "catalog_invalid_package",
242
- message: `Catalog entry '${id || entryPath}' package must be an npm package name, not '${packageName}'.`,
243
- path: source || null,
244
- suggestedFix: "Use package plus defaultVersion separately, for example @scope/topogram-template-name and 0.1.0."
245
- }));
246
- }
247
- if (defaultVersion && /\s/.test(defaultVersion)) {
248
- diagnostics.push(catalogDiagnostic({
249
- code: "catalog_invalid_default_version",
250
- message: `Catalog entry '${id || entryPath}' defaultVersion must not contain whitespace.`,
251
- path: source || null,
252
- suggestedFix: "Use an exact version or npm dist-tag."
253
- }));
254
- }
255
- if (!Array.isArray(entry.tags)) {
256
- diagnostics.push(catalogDiagnostic({
257
- code: "catalog_tags_missing",
258
- message: `Catalog entry '${id || entryPath}' is missing required tags array.`,
259
- path: source || null,
260
- suggestedFix: "Add tags as an array of strings."
261
- }));
262
- }
263
- if (Object.prototype.hasOwnProperty.call(entry, "surfaces") && !Array.isArray(entry.surfaces)) {
264
- diagnostics.push(catalogDiagnostic({
265
- code: "catalog_optional_surfaces_invalid",
266
- severity: "warning",
267
- message: `Catalog entry '${id || entryPath}' surfaces should be an array of surface ids.`,
268
- path: source || null,
269
- suggestedFix: "Use surfaces such as [\"web\"], [\"api\"], [\"database\"], or [\"native\"]."
270
- }));
271
- }
272
- for (const surface of surfaces) {
273
- if (!KNOWN_CATALOG_SURFACES.has(surface)) {
274
- diagnostics.push(catalogDiagnostic({
275
- code: "catalog_optional_surface_unknown",
276
- severity: "warning",
277
- message: `Catalog entry '${id || entryPath}' has unknown surface '${surface}'.`,
278
- path: source || null,
279
- suggestedFix: "Use known surface ids: web, api, database, native."
280
- }));
281
- }
282
- }
283
- if (Object.prototype.hasOwnProperty.call(entry, "generators") && !Array.isArray(entry.generators)) {
284
- diagnostics.push(catalogDiagnostic({
285
- code: "catalog_optional_generators_invalid",
286
- severity: "warning",
287
- message: `Catalog entry '${id || entryPath}' generators should be an array of generator ids.`,
288
- path: source || null,
289
- suggestedFix: "Use package-backed generator ids such as [\"@topogram/generator-sveltekit-web\", \"@topogram/generator-hono-api\"]."
290
- }));
291
- }
292
- if (Object.prototype.hasOwnProperty.call(entry, "stack") && typeof entry.stack !== "string") {
293
- diagnostics.push(catalogDiagnostic({
294
- code: "catalog_optional_stack_invalid",
295
- severity: "warning",
296
- message: `Catalog entry '${id || entryPath}' stack should be a string.`,
297
- path: source || null,
298
- suggestedFix: "Use a short stack label such as \"SvelteKit + Hono + Postgres\"."
299
- }));
300
- }
301
- if (!trust) {
302
- diagnostics.push(catalogDiagnostic({
303
- code: "catalog_trust_missing",
304
- message: `Catalog entry '${id || entryPath}' is missing required trust metadata.`,
305
- path: source || null,
306
- suggestedFix: "Add trust.scope and trust.includesExecutableImplementation."
307
- }));
308
- } else if (kind === "topogram" && trust.includesExecutableImplementation) {
309
- diagnostics.push(catalogDiagnostic({
310
- code: "catalog_topogram_executable_not_supported",
311
- message: `Catalog topogram entry '${id || entryPath}' cannot include executable implementation in v1.`,
312
- path: source || null,
313
- suggestedFix: "Move executable code into a template package, or set includesExecutableImplementation to false."
314
- }));
315
- }
316
-
317
- entries.push({
318
- id,
319
- kind: kind === "topogram" ? "topogram" : "template",
320
- package: packageName,
321
- defaultVersion,
322
- description,
323
- tags,
324
- ...(surfaces.length > 0 ? { surfaces } : {}),
325
- ...(generators.length > 0 ? { generators } : {}),
326
- ...(stack ? { stack } : {}),
327
- trust: trust || { scope: "", includesExecutableImplementation: false }
328
- });
329
- });
330
-
331
- return validationResult({ version, entries }, diagnostics);
332
- }
333
-
334
- /**
335
- * @param {TopogramCatalog|null} catalog
336
- * @param {CatalogDiagnostic[]} diagnostics
337
- * @returns {CatalogValidationResult}
338
- */
339
- function validationResult(catalog, diagnostics) {
340
- const errors = diagnostics
341
- .filter((diagnostic) => diagnostic.severity === "error")
342
- .map((diagnostic) => diagnostic.message);
343
- return {
344
- ok: errors.length === 0,
345
- catalog: errors.length === 0 ? catalog : null,
346
- diagnostics,
347
- errors
348
- };
349
- }
350
-
351
- /**
352
- * @param {Record<string, unknown>} input
353
- * @param {string} field
354
- * @returns {string}
355
- */
356
- function stringField(input, field) {
357
- const value = input[field];
358
- return typeof value === "string" ? value.trim() : "";
359
- }
360
-
361
- /**
362
- * @param {unknown} value
363
- * @returns {CatalogTrust|null}
364
- */
365
- function trustField(value) {
366
- if (!value || typeof value !== "object" || Array.isArray(value)) {
367
- return null;
368
- }
369
- const trust = /** @type {Record<string, unknown>} */ (value);
370
- if (typeof trust.scope !== "string" || !trust.scope) {
371
- return null;
372
- }
373
- if (typeof trust.includesExecutableImplementation !== "boolean") {
374
- return null;
375
- }
376
- const result = {
377
- scope: trust.scope,
378
- includesExecutableImplementation: trust.includesExecutableImplementation
379
- };
380
- if (typeof trust.notes === "string" && trust.notes) {
381
- return { ...result, notes: trust.notes };
382
- }
383
- return result;
384
- }
385
-
386
- /**
387
- * @param {string|undefined|null} sourceInput
388
- * @returns {CatalogLoadResult}
389
- */
390
- export function loadCatalog(sourceInput = null) {
391
- const source = catalogSourceOrDefault(sourceInput);
392
- if (isCatalogSourceDisabled(source)) {
393
- throw new Error("Catalog source is disabled.");
394
- }
395
- const text = readCatalogText(source);
396
- const parsed = JSON.parse(text);
397
- const validation = validateCatalog(parsed, source);
398
- if (!validation.ok || !validation.catalog) {
399
- throw new Error(validation.errors.join("\n") || `Catalog '${source}' is invalid.`);
400
- }
401
- return {
402
- source,
403
- catalog: validation.catalog,
404
- diagnostics: validation.diagnostics
405
- };
406
- }
407
-
408
- /**
409
- * @param {string} source
410
- * @returns {CatalogValidationResult & { source: string }}
411
- */
412
- export function checkCatalogSource(source) {
413
- const text = readCatalogText(source);
414
- const parsed = JSON.parse(text);
415
- return {
416
- source,
417
- ...validateCatalog(parsed, source)
418
- };
419
- }
420
-
421
- /**
422
- * @param {string} source
423
- * @returns {string}
424
- */
425
- function readCatalogText(source) {
426
- if (source.startsWith("github:")) {
427
- return readGithubCatalogText(source);
428
- }
429
- if (source.startsWith("https://") || source.startsWith("http://")) {
430
- return readUrlText(source);
431
- }
432
- const resolvedPath = path.resolve(source);
433
- return fs.readFileSync(resolvedPath, "utf8");
434
- }
435
-
436
- /**
437
- * @param {string} source
438
- * @returns {string}
439
- */
440
- function readGithubCatalogText(source) {
441
- return readGithubCatalogSourceText(source);
442
- }
443
-
444
- /**
445
- * @param {string} source
446
- * @returns {string}
447
- */
448
- function readUrlText(source) {
449
- const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN || "";
450
- const tokenEnv = token && githubTokenAllowedForCatalogUrl(source)
451
- ? { TOPOGRAM_FETCH_TOKEN: token }
452
- : {};
453
- const result = childProcess.spawnSync(process.execPath, ["--input-type=module", "-e", FETCH_URL_SCRIPT, source], {
454
- encoding: "utf8",
455
- env: {
456
- ...process.env,
457
- ...tokenEnv,
458
- PATH: process.env.PATH || ""
459
- }
460
- });
461
- if (result.status !== 0) {
462
- const reason = result.error?.message || result.stderr || result.stdout || "unknown error";
463
- throw new Error(`Failed to read catalog URL '${source}'.\n${reason}`.trim());
464
- }
465
- return result.stdout;
466
- }
467
-
468
- /**
469
- * @param {string} source
470
- * @returns {boolean}
471
- */
472
- function githubTokenAllowedForCatalogUrl(source) {
473
- try {
474
- const hostname = new URL(source).hostname.toLowerCase();
475
- return GITHUB_TOKEN_HOSTS.has(hostname) || hostname.endsWith(".github.com");
476
- } catch {
477
- return false;
478
- }
479
- }
480
-
481
- /**
482
- * @param {TopogramCatalog} catalog
483
- * @param {string} id
484
- * @param {"template"|"topogram"|null} [kind]
485
- * @returns {CatalogEntry|null}
486
- */
487
- export function findCatalogEntry(catalog, id, kind = null) {
488
- return catalog.entries.find((entry) => entry.id === id && (!kind || entry.kind === kind)) || null;
489
- }
490
-
491
- /**
492
- * @param {CatalogEntry} entry
493
- * @param {string|null|undefined} version
494
- * @returns {string}
495
- */
496
- export function catalogEntryPackageSpec(entry, version = null) {
497
- return `${entry.package}@${version || entry.defaultVersion}`;
498
- }
499
-
500
- /**
501
- * @param {CatalogEntry} entry
502
- * @returns {{ id: string, version: string, source: "catalog", name: string, package: string, defaultVersion: string, description: string, tags: string[], surfaces?: string[], generators?: string[], stack?: string, includesExecutableImplementation: boolean, trust: CatalogTrust }}
503
- */
504
- export function catalogTemplateListItem(entry) {
505
- return {
506
- id: entry.id,
507
- version: entry.defaultVersion,
508
- source: "catalog",
509
- name: entry.id,
510
- package: entry.package,
511
- defaultVersion: entry.defaultVersion,
512
- description: entry.description,
513
- tags: entry.tags,
514
- ...(entry.surfaces ? { surfaces: entry.surfaces } : {}),
515
- ...(entry.generators ? { generators: entry.generators } : {}),
516
- ...(entry.stack ? { stack: entry.stack } : {}),
517
- includesExecutableImplementation: entry.trust.includesExecutableImplementation,
518
- trust: entry.trust
519
- };
520
- }
521
-
522
- /**
523
- * @param {CatalogEntry} entry
524
- * @param {string} targetPath
525
- * @param {{ version?: string|null, catalogSource?: string|null }} [options]
526
- * @returns {{ ok: boolean, id: string, kind: "topogram", packageSpec: string, targetPath: string, provenancePath: string, files: string[] }}
527
- */
528
- export function copyCatalogTopogramEntry(entry, targetPath, options = {}) {
529
- if (entry.kind !== "topogram") {
530
- throw new Error(`Catalog entry '${entry.id}' is a ${entry.kind}, not a topogram.`);
531
- }
532
- const packageSpec = catalogEntryPackageSpec(entry, options.version || null);
533
- const packageRoot = installPackageSpec(packageSpec);
534
- const implementationRoot = path.join(packageRoot, "implementation");
535
- if (fs.existsSync(implementationRoot)) {
536
- throw new Error(
537
- `Catalog topogram entry '${entry.id}' package '${packageSpec}' contains implementation/, which is not allowed for v1 topogram entries.`
538
- );
539
- }
540
- const topogramRoot = path.join(packageRoot, "topogram");
541
- if (!fs.existsSync(topogramRoot) || !fs.statSync(topogramRoot).isDirectory()) {
542
- throw new Error(`Catalog topogram entry '${entry.id}' package '${packageSpec}' is missing topogram/.`);
543
- }
544
-
545
- const resolvedTarget = path.resolve(targetPath);
546
- ensureEmptyDirectory(resolvedTarget);
547
- /** @type {string[]} */
548
- const files = [];
549
- copyPath(topogramRoot, path.join(resolvedTarget, "topogram"), "topogram", files);
550
- for (const fileName of ["topogram.project.json", "README.md"]) {
551
- const sourcePath = path.join(packageRoot, fileName);
552
- if (fs.existsSync(sourcePath) && fs.statSync(sourcePath).isFile()) {
553
- copyPath(sourcePath, path.join(resolvedTarget, fileName), fileName, files);
554
- }
555
- }
556
- const provenance = writeTopogramSourceRecord(resolvedTarget, {
557
- catalogSource: options.catalogSource || null,
558
- entry,
559
- packageSpec,
560
- version: options.version || entry.defaultVersion
561
- });
562
- return {
563
- ok: true,
564
- id: entry.id,
565
- kind: "topogram",
566
- packageSpec,
567
- targetPath: resolvedTarget,
568
- provenancePath: provenance.path,
569
- files: files.sort((a, b) => a.localeCompare(b))
570
- };
571
- }
572
-
573
- /**
574
- * @param {string} projectRoot
575
- * @param {{ catalogSource: string|null, entry: CatalogEntry, packageSpec: string, version: string }} input
576
- * @returns {{ path: string, record: Record<string, any> }}
577
- */
578
- function writeTopogramSourceRecord(projectRoot, input) {
579
- const record = {
580
- version: "0.1",
581
- kind: "topogram",
582
- copiedAt: new Date().toISOString(),
583
- catalog: {
584
- id: input.entry.id,
585
- source: input.catalogSource
586
- },
587
- package: {
588
- name: input.entry.package,
589
- version: input.version,
590
- spec: input.packageSpec
591
- },
592
- trust: {
593
- includesExecutableImplementation: false
594
- },
595
- files: collectSourceFileRecords(projectRoot)
596
- };
597
- const sourcePath = path.join(projectRoot, TOPOGRAM_SOURCE_FILE);
598
- fs.writeFileSync(sourcePath, `${JSON.stringify(record, null, 2)}\n`, "utf8");
599
- return { path: sourcePath, record };
600
- }
601
-
602
- /**
603
- * @param {string} projectRoot
604
- * @returns {{ ok: true, exists: boolean, path: string, status: "missing"|"clean"|"changed", source: Record<string, any>|null, content: { changed: string[], added: string[], removed: string[] }, diagnostics: any[], errors: [] }}
605
- */
606
- export function buildTopogramSourceStatus(projectRoot) {
607
- const resolvedRoot = path.resolve(projectRoot);
608
- const sourcePath = path.join(resolvedRoot, TOPOGRAM_SOURCE_FILE);
609
- if (!fs.existsSync(sourcePath)) {
610
- return {
611
- ok: true,
612
- exists: false,
613
- path: sourcePath,
614
- status: "missing",
615
- source: null,
616
- content: { changed: [], added: [], removed: [] },
617
- diagnostics: [{
618
- code: "topogram_source_missing",
619
- severity: "warning",
620
- message: `${TOPOGRAM_SOURCE_FILE} was not found. This project may not have been copied from a catalog topogram entry.`,
621
- path: sourcePath,
622
- suggestedFix: "Run `topogram catalog copy <id> <target>` to create a project with source provenance."
623
- }],
624
- errors: []
625
- };
626
- }
627
- const source = JSON.parse(fs.readFileSync(sourcePath, "utf8"));
628
- const trustedFiles = Array.isArray(source.files) ? source.files : [];
629
- const trustedByPath = new Map(trustedFiles.map((file) => [String(file.path), file]));
630
- const currentByPath = new Map(collectSourceFileRecords(resolvedRoot).map((file) => [file.path, file]));
631
- /** @type {string[]} */
632
- const changed = [];
633
- /** @type {string[]} */
634
- const added = [];
635
- /** @type {string[]} */
636
- const removed = [];
637
- for (const [filePath, current] of currentByPath) {
638
- const trusted = trustedByPath.get(filePath);
639
- if (!trusted) {
640
- added.push(filePath);
641
- } else if (trusted.sha256 !== current.sha256 || trusted.size !== current.size) {
642
- changed.push(filePath);
643
- }
644
- }
645
- for (const filePath of trustedByPath.keys()) {
646
- if (!currentByPath.has(filePath)) {
647
- removed.push(filePath);
648
- }
649
- }
650
- const content = {
651
- changed: changed.sort((a, b) => a.localeCompare(b)),
652
- added: added.sort((a, b) => a.localeCompare(b)),
653
- removed: removed.sort((a, b) => a.localeCompare(b))
654
- };
655
- return {
656
- ok: true,
657
- exists: true,
658
- path: sourcePath,
659
- status: content.changed.length || content.added.length || content.removed.length ? "changed" : "clean",
660
- source,
661
- content,
662
- diagnostics: [],
663
- errors: []
664
- };
665
- }
666
-
667
- /**
668
- * @param {string} projectRoot
669
- * @returns {Array<{ path: string, sha256: string, size: number }>}
670
- */
671
- function collectSourceFileRecords(projectRoot) {
672
- /** @type {string[]} */
673
- const files = [];
674
- for (const sourceRoot of ["topogram", "topogram.project.json", "README.md"]) {
675
- const sourcePath = path.join(projectRoot, sourceRoot);
676
- if (fs.existsSync(sourcePath)) {
677
- collectFiles(sourcePath, sourceRoot, files);
678
- }
679
- }
680
- return files
681
- .sort((a, b) => a.localeCompare(b))
682
- .map((relativePath) => ({
683
- path: relativePath,
684
- ...fileHash(path.join(projectRoot, relativePath))
685
- }));
686
- }
687
-
688
- /**
689
- * @param {string} filePath
690
- * @returns {{ sha256: string, size: number }}
691
- */
692
- function fileHash(filePath) {
693
- const bytes = fs.readFileSync(filePath);
694
- return {
695
- sha256: crypto.createHash("sha256").update(bytes).digest("hex"),
696
- size: bytes.length
697
- };
698
- }
699
-
700
- /**
701
- * @param {string} targetPath
702
- * @returns {void}
703
- */
704
- function ensureEmptyDirectory(targetPath) {
705
- if (!fs.existsSync(targetPath)) {
706
- fs.mkdirSync(targetPath, { recursive: true });
707
- return;
708
- }
709
- if (!fs.statSync(targetPath).isDirectory()) {
710
- throw new Error(`Cannot copy catalog topogram into non-directory path '${targetPath}'.`);
711
- }
712
- const entries = fs.readdirSync(targetPath).filter((entry) => entry !== ".DS_Store");
713
- if (entries.length > 0) {
714
- throw new Error(`Refusing to copy catalog topogram into non-empty directory '${targetPath}'.`);
715
- }
716
- }
717
-
718
- /**
719
- * @param {string} sourcePath
720
- * @param {string} targetPath
721
- * @param {string} relativePath
722
- * @param {string[]} files
723
- * @returns {void}
724
- */
725
- function copyPath(sourcePath, targetPath, relativePath, files) {
726
- fs.cpSync(sourcePath, targetPath, { recursive: true });
727
- collectFiles(targetPath, relativePath, files);
728
- }
729
-
730
- /**
731
- * @param {string} currentPath
732
- * @param {string} relativePath
733
- * @param {string[]} files
734
- * @returns {void}
735
- */
736
- function collectFiles(currentPath, relativePath, files) {
737
- const stat = fs.statSync(currentPath);
738
- if (stat.isFile()) {
739
- files.push(relativePath.replace(/\\/g, "/"));
740
- return;
741
- }
742
- if (!stat.isDirectory()) {
743
- return;
744
- }
745
- for (const entry of fs.readdirSync(currentPath)) {
746
- collectFiles(path.join(currentPath, entry), path.join(relativePath, entry), files);
747
- }
748
- }
3
+ export {
4
+ CATALOG_FILE_NAME,
5
+ TOPOGRAM_SOURCE_FILE
6
+ } from "./catalog/constants.js";
7
+ export {
8
+ catalogEntryPackageSpec,
9
+ catalogTemplateListItem,
10
+ findCatalogEntry
11
+ } from "./catalog/entries.js";
12
+ export {
13
+ catalogSourceOrDefault,
14
+ checkCatalogSource,
15
+ isCatalogSourceDisabled,
16
+ loadCatalog
17
+ } from "./catalog/source.js";
18
+ export { buildTopogramSourceStatus } from "./catalog/provenance.js";
19
+ export { copyCatalogTopogramEntry } from "./catalog/copy.js";
20
+ export { validateCatalog } from "./catalog/validation.js";