@topogram/cli 0.3.64 → 0.3.66

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 (278) hide show
  1. package/package.json +1 -1
  2. package/src/adoption/plan/index.js +716 -0
  3. package/src/adoption/plan.js +12 -703
  4. package/src/adoption/reporting.js +1 -1
  5. package/src/agent-brief.js +7 -21
  6. package/src/agent-ops/query-builders/auth.js +375 -0
  7. package/src/agent-ops/query-builders/change-risk/change-plan.js +123 -0
  8. package/src/agent-ops/query-builders/change-risk/import-plan.js +49 -0
  9. package/src/agent-ops/query-builders/change-risk/maintained.js +286 -0
  10. package/src/agent-ops/query-builders/change-risk/review-packets.js +123 -0
  11. package/src/agent-ops/query-builders/change-risk/risk.js +189 -0
  12. package/src/agent-ops/query-builders/change-risk.js +25 -0
  13. package/src/agent-ops/query-builders/common.js +149 -0
  14. package/src/agent-ops/query-builders/maintained-risk.js +539 -0
  15. package/src/agent-ops/query-builders/maintained-shared.js +120 -0
  16. package/src/agent-ops/query-builders/multi-agent.js +547 -0
  17. package/src/agent-ops/query-builders/projection-impacts.js +514 -0
  18. package/src/agent-ops/query-builders/work-packets.js +417 -0
  19. package/src/agent-ops/query-builders/workflow-context-shared.js +300 -0
  20. package/src/agent-ops/query-builders/workflow-context.js +398 -0
  21. package/src/agent-ops/query-builders/workflow-presets-core.js +677 -0
  22. package/src/agent-ops/query-builders/workflow-presets.js +341 -0
  23. package/src/agent-ops/query-builders.d.ts +26 -26
  24. package/src/agent-ops/query-builders.js +42 -5021
  25. package/src/archive/jsonl.js +2 -2
  26. package/src/archive/resolver-bridge.js +1 -1
  27. package/src/archive/unarchive.js +2 -1
  28. package/src/catalog/constants.js +10 -0
  29. package/src/catalog/copy.js +65 -0
  30. package/src/catalog/diagnostics.js +15 -0
  31. package/src/catalog/entries.js +42 -0
  32. package/src/catalog/files.js +67 -0
  33. package/src/catalog/provenance.js +123 -0
  34. package/src/catalog/source.js +150 -0
  35. package/src/catalog/validation.js +252 -0
  36. package/src/catalog.d.ts +2 -0
  37. package/src/catalog.js +18 -746
  38. package/src/cli/command-parsers/project.js +3 -0
  39. package/src/cli/command-parsers/shared.js +1 -1
  40. package/src/cli/commands/agent.js +2 -2
  41. package/src/cli/commands/catalog/check.js +31 -0
  42. package/src/cli/commands/catalog/copy.js +59 -0
  43. package/src/cli/commands/catalog/doctor.js +248 -0
  44. package/src/cli/commands/catalog/help.js +21 -0
  45. package/src/cli/commands/catalog/list.js +52 -0
  46. package/src/cli/commands/catalog/runner.js +92 -0
  47. package/src/cli/commands/catalog/shared.js +17 -0
  48. package/src/cli/commands/catalog/show.js +134 -0
  49. package/src/cli/commands/catalog.js +30 -615
  50. package/src/cli/commands/check.js +3 -3
  51. package/src/cli/commands/doctor.js +2 -9
  52. package/src/cli/commands/generator-policy/package-info.js +162 -0
  53. package/src/cli/commands/generator-policy/payloads.js +372 -0
  54. package/src/cli/commands/generator-policy/printers.js +159 -0
  55. package/src/cli/commands/generator-policy/runner.js +81 -0
  56. package/src/cli/commands/generator-policy/shared.js +39 -0
  57. package/src/cli/commands/generator-policy.js +15 -783
  58. package/src/cli/commands/import/adopt.js +170 -0
  59. package/src/cli/commands/import/check.js +91 -0
  60. package/src/cli/commands/import/diff.js +84 -0
  61. package/src/cli/commands/import/help.js +47 -0
  62. package/src/cli/commands/import/paths.js +269 -0
  63. package/src/cli/commands/import/plan.js +292 -0
  64. package/src/cli/commands/import/refresh.js +471 -0
  65. package/src/cli/commands/import/status-history.js +196 -0
  66. package/src/cli/commands/import/workspace.js +233 -0
  67. package/src/cli/commands/import.js +33 -1732
  68. package/src/cli/commands/migrate.js +153 -0
  69. package/src/cli/commands/package/constants.js +17 -0
  70. package/src/cli/commands/package/doctor.js +240 -0
  71. package/src/cli/commands/package/help.js +27 -0
  72. package/src/cli/commands/package/lockfile.js +135 -0
  73. package/src/cli/commands/package/npm.js +97 -0
  74. package/src/cli/commands/package/reporting.js +35 -0
  75. package/src/cli/commands/package/runner.js +33 -0
  76. package/src/cli/commands/package/shared.js +9 -0
  77. package/src/cli/commands/package/update-cli.js +252 -0
  78. package/src/cli/commands/package/versions.js +35 -0
  79. package/src/cli/commands/package.js +29 -813
  80. package/src/cli/commands/query/change-plan.js +68 -0
  81. package/src/cli/commands/query/definitions.js +202 -0
  82. package/src/cli/commands/query/import-adopt.js +121 -0
  83. package/src/cli/commands/query/runner/artifacts.js +102 -0
  84. package/src/cli/commands/query/runner/boundaries.js +211 -0
  85. package/src/cli/commands/query/runner/change.js +182 -0
  86. package/src/cli/commands/query/runner/import-adopt.js +111 -0
  87. package/src/cli/commands/query/runner/index.js +31 -0
  88. package/src/cli/commands/query/runner/output.js +12 -0
  89. package/src/cli/commands/query/runner/workflow.js +241 -0
  90. package/src/cli/commands/query/runner.js +3 -0
  91. package/src/cli/commands/query/workflow-context.js +5 -0
  92. package/src/cli/commands/query/workspace.js +270 -0
  93. package/src/cli/commands/query.js +9 -1300
  94. package/src/cli/commands/source.js +3 -12
  95. package/src/cli/commands/template/baseline.js +100 -0
  96. package/src/cli/commands/template/check.js +467 -0
  97. package/src/cli/commands/template/constants.js +8 -0
  98. package/src/cli/commands/template/diagnostics.js +26 -0
  99. package/src/cli/commands/template/help.js +28 -0
  100. package/src/cli/commands/template/lifecycle.js +404 -0
  101. package/src/cli/commands/template/list-show.js +287 -0
  102. package/src/cli/commands/template/policy.js +422 -0
  103. package/src/cli/commands/template/shared.js +127 -0
  104. package/src/cli/commands/template/updates.js +352 -0
  105. package/src/cli/commands/template-runner.js +6 -6
  106. package/src/cli/commands/template.js +41 -2143
  107. package/src/cli/commands/trust.js +1 -1
  108. package/src/cli/commands/workflow.js +6 -1
  109. package/src/cli/dispatcher.js +6 -1
  110. package/src/cli/help.js +15 -14
  111. package/src/cli/migration-guidance.js +1 -1
  112. package/src/cli/output-safety.js +2 -1
  113. package/src/cli/path-normalization.js +3 -13
  114. package/src/generator/api/contracts.js +497 -0
  115. package/src/generator/api/metadata.js +221 -0
  116. package/src/generator/api/openapi.js +559 -0
  117. package/src/generator/api/schema.js +124 -0
  118. package/src/generator/api/types.d.ts +98 -0
  119. package/src/generator/api.js +3 -1195
  120. package/src/generator/context/domain-page.js +1 -1
  121. package/src/generator/context/shared/domain-sdlc.js +282 -0
  122. package/src/generator/context/shared/maintained-boundary.js +665 -0
  123. package/src/generator/context/shared/metrics.js +85 -0
  124. package/src/generator/context/shared/primitives.js +64 -0
  125. package/src/generator/context/shared/relationships.js +453 -0
  126. package/src/generator/context/shared/summaries.js +263 -0
  127. package/src/generator/context/shared/types.d.ts +207 -0
  128. package/src/generator/context/shared.d.ts +42 -0
  129. package/src/generator/context/shared.js +80 -1390
  130. package/src/generator/context/slice/core.js +397 -0
  131. package/src/generator/context/slice/sdlc.js +417 -0
  132. package/src/generator/context/slice/ui-packets.js +183 -0
  133. package/src/generator/context/slice.js +2 -859
  134. package/src/generator/context/task-mode.js +2 -2
  135. package/src/generator/registry/index.js +507 -0
  136. package/src/generator/registry.js +18 -504
  137. package/src/generator/runtime/environment/index.js +666 -0
  138. package/src/generator/runtime/environment.js +4 -666
  139. package/src/generator/runtime/runtime-check/index.js +554 -0
  140. package/src/generator/runtime/runtime-check.js +4 -554
  141. package/src/generator/runtime/shared/index.js +572 -0
  142. package/src/generator/runtime/shared.js +19 -570
  143. package/src/generator/sdlc/doc-page.js +1 -1
  144. package/src/generator/shared.d.ts +2 -0
  145. package/src/generator/surfaces/databases/lifecycle-shared.js +1 -1
  146. package/src/generator/surfaces/native/swiftui-templates/README.generated.md +1 -1
  147. package/src/generator/surfaces/shared.d.ts +3 -0
  148. package/src/generator/widget-conformance/behavior-report.js +258 -0
  149. package/src/generator/widget-conformance/checks.js +371 -0
  150. package/src/generator/widget-conformance/projection-context.js +200 -0
  151. package/src/generator/widget-conformance/report.js +166 -0
  152. package/src/generator/widget-conformance/types.d.ts +121 -0
  153. package/src/generator/widget-conformance.js +3 -824
  154. package/src/import/core/context.d.ts +3 -0
  155. package/src/import/core/context.js +5 -7
  156. package/src/import/core/contracts.d.ts +1 -0
  157. package/src/import/core/registry.d.ts +4 -0
  158. package/src/import/core/runner/candidates.js +337 -0
  159. package/src/import/core/runner/options.js +22 -0
  160. package/src/import/core/runner/reports.js +51 -0
  161. package/src/import/core/runner/run.js +79 -0
  162. package/src/import/core/runner/tracks.js +150 -0
  163. package/src/import/core/runner/ui-drafts.js +393 -0
  164. package/src/import/core/runner.js +3 -698
  165. package/src/import/core/shared/api-routes.js +221 -0
  166. package/src/import/core/shared/candidates.js +97 -0
  167. package/src/import/core/shared/files.js +177 -0
  168. package/src/import/core/shared/next-app.js +389 -0
  169. package/src/import/core/shared/types.d.ts +51 -0
  170. package/src/import/core/shared/ui-routes.js +230 -0
  171. package/src/import/core/shared.js +60 -861
  172. package/src/new-project/constants.js +128 -0
  173. package/src/new-project/create.js +90 -0
  174. package/src/new-project/json.js +28 -0
  175. package/src/new-project/metadata.js +96 -0
  176. package/src/new-project/package-spec.js +161 -0
  177. package/src/new-project/project-files.js +351 -0
  178. package/src/new-project/template-policy.js +269 -0
  179. package/src/new-project/template-resolution.js +370 -0
  180. package/src/new-project/template-snapshots.js +442 -0
  181. package/src/new-project/template-updates.js +512 -0
  182. package/src/new-project/types.d.ts +83 -0
  183. package/src/new-project.js +6 -2277
  184. package/src/parser.d.ts +87 -1
  185. package/src/parser.js +118 -0
  186. package/src/policy/review-boundaries.d.ts +15 -0
  187. package/src/project-config/index.js +591 -0
  188. package/src/project-config.js +19 -561
  189. package/src/resolver/enrich/acceptance-criterion.js +2 -0
  190. package/src/resolver/enrich/bug.js +2 -0
  191. package/src/resolver/enrich/pitch.js +2 -0
  192. package/src/resolver/enrich/requirement.js +2 -0
  193. package/src/resolver/enrich/task.js +2 -0
  194. package/src/resolver/index.js +19 -2089
  195. package/src/resolver/normalize.js +384 -1
  196. package/src/resolver/plans.js +168 -0
  197. package/src/resolver/projections-api.js +494 -0
  198. package/src/resolver/projections-db.js +133 -0
  199. package/src/resolver/projections-ui.js +317 -0
  200. package/src/resolver/shapes.js +251 -0
  201. package/src/resolver/shared.js +278 -0
  202. package/src/resolver/widgets.js +132 -0
  203. package/src/sdlc/adopt.js +6 -5
  204. package/src/sdlc/paths.js +3 -5
  205. package/src/sdlc/scaffold.js +2 -1
  206. package/src/template-trust/constants.js +62 -0
  207. package/src/template-trust/content.js +258 -0
  208. package/src/template-trust/diff.js +92 -0
  209. package/src/template-trust/policy.js +61 -0
  210. package/src/template-trust/record.js +90 -0
  211. package/src/template-trust/status.js +182 -0
  212. package/src/template-trust.js +24 -687
  213. package/src/text-helpers.d.ts +1 -0
  214. package/src/topogram-types.d.ts +69 -0
  215. package/src/validator/common.js +488 -0
  216. package/src/validator/data-model.js +237 -0
  217. package/src/validator/docs.js +167 -0
  218. package/src/validator/expressions.js +146 -1
  219. package/src/validator/index.d.ts +23 -0
  220. package/src/validator/index.js +32 -3585
  221. package/src/validator/kinds.d.ts +41 -0
  222. package/src/validator/kinds.js +2 -0
  223. package/src/validator/model-helpers.js +46 -0
  224. package/src/validator/per-kind/acceptance-criterion.js +5 -0
  225. package/src/validator/per-kind/bug.js +6 -0
  226. package/src/validator/per-kind/domain.js +15 -2
  227. package/src/validator/per-kind/pitch.js +7 -0
  228. package/src/validator/per-kind/requirement.js +5 -0
  229. package/src/validator/per-kind/task.js +7 -0
  230. package/src/validator/per-kind/widget.js +14 -0
  231. package/src/validator/projections/api-http-async.js +410 -0
  232. package/src/validator/projections/api-http-authz.js +88 -0
  233. package/src/validator/projections/api-http-core.js +205 -0
  234. package/src/validator/projections/api-http-policies.js +339 -0
  235. package/src/validator/projections/api-http-responses.js +233 -0
  236. package/src/validator/projections/api-http.js +44 -0
  237. package/src/validator/projections/db.js +353 -0
  238. package/src/validator/projections/generator-defaults.js +45 -0
  239. package/src/validator/projections/helpers.js +87 -0
  240. package/src/validator/projections/ui-helpers.js +214 -0
  241. package/src/validator/projections/ui-navigation.js +344 -0
  242. package/src/validator/projections/ui-structure.js +364 -0
  243. package/src/validator/projections/ui-widgets.js +493 -0
  244. package/src/validator/projections/ui.js +46 -0
  245. package/src/validator/registry.js +48 -1
  246. package/src/validator/utils.d.ts +20 -0
  247. package/src/validator/utils.js +115 -12
  248. package/src/widget-behavior.d.ts +1 -0
  249. package/src/workflows/import-app/api/collect.js +221 -0
  250. package/src/workflows/import-app/api/openapi.js +257 -0
  251. package/src/workflows/import-app/api/routes.js +327 -0
  252. package/src/workflows/import-app/api/sources.js +22 -0
  253. package/src/workflows/import-app/api.js +2 -797
  254. package/src/workflows/reconcile/adoption-plan/build.js +212 -0
  255. package/src/workflows/reconcile/adoption-plan/dependencies.js +75 -0
  256. package/src/workflows/reconcile/adoption-plan/outputs.js +153 -0
  257. package/src/workflows/reconcile/adoption-plan/paths.js +58 -0
  258. package/src/workflows/reconcile/adoption-plan/projection-patches.js +177 -0
  259. package/src/workflows/reconcile/adoption-plan/reasons.js +107 -0
  260. package/src/workflows/reconcile/adoption-plan.js +30 -740
  261. package/src/workflows/reconcile/auth/closures.js +115 -0
  262. package/src/workflows/reconcile/auth/formatters.js +142 -0
  263. package/src/workflows/reconcile/auth/inference.js +330 -0
  264. package/src/workflows/reconcile/auth/roles.js +122 -0
  265. package/src/workflows/reconcile/auth.js +35 -690
  266. package/src/workflows/reconcile/bundle-core/index.js +600 -0
  267. package/src/workflows/reconcile/bundle-core.js +12 -598
  268. package/src/workflows/reconcile/candidate-model.js +18 -2
  269. package/src/workflows/reconcile/canonical-surface.js +1 -1
  270. package/src/workflows/reconcile/impacts/adoption-plan.js +196 -0
  271. package/src/workflows/reconcile/impacts/indexes.js +105 -0
  272. package/src/workflows/reconcile/impacts/patches.js +252 -0
  273. package/src/workflows/reconcile/impacts/reports.js +80 -0
  274. package/src/workflows/reconcile/impacts.js +14 -623
  275. package/src/workflows/reconcile/renderers.js +41 -6
  276. package/src/workflows/shared.js +5 -11
  277. package/src/workspace-docs.d.ts +29 -0
  278. package/src/workspace-paths.js +328 -0
@@ -1,7 +1,7 @@
1
1
  // Year-bucketed JSONL archive I/O.
2
2
  //
3
- // File layout: `<project-or-topogram-root>/topogram/_archive/{kind}s-{year}.jsonl`
4
- // or `<topogram-root>/_archive/{kind}s-{year}.jsonl`
3
+ // File layout: `<project-root>/topo/_archive/{kind}s-{year}.jsonl`
4
+ // or `<workspace-root>/_archive/{kind}s-{year}.jsonl`
5
5
  // (e.g. `tasks-2026.jsonl`, `bugs-2026.jsonl`).
6
6
  //
7
7
  // Each line is a self-contained archived statement. The format is JSONL so
@@ -1,7 +1,7 @@
1
1
  // Bridge between archived JSONL entries and the live resolver graph.
2
2
  //
3
3
  // At workspace load time the resolver bridge:
4
- // 1. Walks `topogram/_archive/*.jsonl`
4
+ // 1. Walks the workspace `_archive/*.jsonl`
5
5
  // 2. Builds a flat list of frozen entries (each with `archived: true`)
6
6
  // 3. Returns `{ entries, byId }` so the caller can merge them into the
7
7
  // registry / graph
@@ -19,6 +19,7 @@ import {
19
19
  parseArchiveFile,
20
20
  rewriteArchiveFile
21
21
  } from "./jsonl.js";
22
+ import { resolveTopoRoot } from "../workspace-paths.js";
22
23
 
23
24
  const REOPEN_STATUSES = {
24
25
  bug: "open",
@@ -83,7 +84,7 @@ export function unarchive(workspaceRoot, id, options = {}) {
83
84
 
84
85
  const { file, entries, entry } = found;
85
86
  const reopenStatus = options.status || REOPEN_STATUSES[entry.kind] || "draft";
86
- const targetDir = options.targetDir || path.join(workspaceRoot, "topogram", `${entry.kind}s`);
87
+ const targetDir = options.targetDir || path.join(resolveTopoRoot(workspaceRoot), `${entry.kind}s`);
87
88
  if (!existsSync(targetDir)) mkdirSync(targetDir, { recursive: true });
88
89
  const targetFile = path.join(targetDir, `${entry.id}.tg`);
89
90
 
@@ -0,0 +1,10 @@
1
+ // @ts-check
2
+
3
+ export const CATALOG_FILE_NAME = "topograms.catalog.json";
4
+ export const TOPOGRAM_SOURCE_FILE = ".topogram-source.json";
5
+ export const KNOWN_CATALOG_SURFACES = new Set(["web", "api", "database", "native"]);
6
+ export const GITHUB_TOKEN_HOSTS = new Set([
7
+ "github.com",
8
+ "api.github.com",
9
+ "raw.githubusercontent.com"
10
+ ]);
@@ -0,0 +1,65 @@
1
+ // @ts-check
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+
6
+ import { installPackageSpec } from "../new-project.js";
7
+ import { DEFAULT_TOPO_FOLDER_NAME, DEFAULT_WORKSPACE_PATH, resolvePackageWorkspace } from "../workspace-paths.js";
8
+ import { catalogEntryPackageSpec } from "./entries.js";
9
+ import { copyPath, ensureEmptyDirectory } from "./files.js";
10
+ import { writeTopogramSourceRecord } from "./provenance.js";
11
+
12
+ /**
13
+ * @param {any} entry
14
+ * @param {string} targetPath
15
+ * @param {{ version?: string|null, catalogSource?: string|null }} [options]
16
+ * @returns {{ ok: boolean, id: string, kind: "topogram", packageSpec: string, targetPath: string, provenancePath: string, files: string[] }}
17
+ */
18
+ export function copyCatalogTopogramEntry(entry, targetPath, options = {}) {
19
+ if (entry.kind !== "topogram") {
20
+ throw new Error(`Catalog entry '${entry.id}' is a ${entry.kind}, not a topogram.`);
21
+ }
22
+ const packageSpec = catalogEntryPackageSpec(entry, options.version || null);
23
+ const packageRoot = installPackageSpec(packageSpec);
24
+ const implementationRoot = path.join(packageRoot, "implementation");
25
+ if (fs.existsSync(implementationRoot)) {
26
+ throw new Error(
27
+ `Catalog topogram entry '${entry.id}' package '${packageSpec}' contains implementation/, which is not allowed for v1 topogram entries.`
28
+ );
29
+ }
30
+ const packageWorkspace = resolvePackageWorkspace(packageRoot);
31
+
32
+ const resolvedTarget = path.resolve(targetPath);
33
+ ensureEmptyDirectory(resolvedTarget);
34
+ /** @type {string[]} */
35
+ const files = [];
36
+ copyPath(packageWorkspace.root, path.join(resolvedTarget, DEFAULT_TOPO_FOLDER_NAME), DEFAULT_TOPO_FOLDER_NAME, files);
37
+ for (const fileName of ["topogram.project.json", "README.md"]) {
38
+ const sourcePath = path.join(packageRoot, fileName);
39
+ if (fs.existsSync(sourcePath) && fs.statSync(sourcePath).isFile()) {
40
+ if (fileName === "topogram.project.json") {
41
+ const projectConfig = JSON.parse(fs.readFileSync(sourcePath, "utf8"));
42
+ projectConfig.workspace = DEFAULT_WORKSPACE_PATH;
43
+ fs.writeFileSync(path.join(resolvedTarget, fileName), `${JSON.stringify(projectConfig, null, 2)}\n`, "utf8");
44
+ files.push(fileName);
45
+ } else {
46
+ copyPath(sourcePath, path.join(resolvedTarget, fileName), fileName, files);
47
+ }
48
+ }
49
+ }
50
+ const provenance = writeTopogramSourceRecord(resolvedTarget, {
51
+ catalogSource: options.catalogSource || null,
52
+ entry,
53
+ packageSpec,
54
+ version: options.version || entry.defaultVersion
55
+ });
56
+ return {
57
+ ok: true,
58
+ id: entry.id,
59
+ kind: "topogram",
60
+ packageSpec,
61
+ targetPath: resolvedTarget,
62
+ provenancePath: provenance.path,
63
+ files: files.sort((a, b) => a.localeCompare(b))
64
+ };
65
+ }
@@ -0,0 +1,15 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * @param {Record<string, unknown>} input
5
+ * @returns {{ code: string, severity: "error"|"warning", message: string, path: string|null, suggestedFix: string|null }}
6
+ */
7
+ export function catalogDiagnostic(input) {
8
+ return {
9
+ code: String(input.code || "catalog_invalid"),
10
+ severity: input.severity === "warning" ? "warning" : "error",
11
+ message: String(input.message || "Catalog is invalid."),
12
+ path: typeof input.path === "string" ? input.path : null,
13
+ suggestedFix: typeof input.suggestedFix === "string" ? input.suggestedFix : null
14
+ };
15
+ }
@@ -0,0 +1,42 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * @param {any} catalog
5
+ * @param {string} id
6
+ * @param {"template"|"topogram"|null} [kind]
7
+ * @returns {any|null}
8
+ */
9
+ export function findCatalogEntry(catalog, id, kind = null) {
10
+ return catalog.entries.find((/** @type {any} */ entry) => entry.id === id && (!kind || entry.kind === kind)) || null;
11
+ }
12
+
13
+ /**
14
+ * @param {any} entry
15
+ * @param {string|null|undefined} version
16
+ * @returns {string}
17
+ */
18
+ export function catalogEntryPackageSpec(entry, version = null) {
19
+ return `${entry.package}@${version || entry.defaultVersion}`;
20
+ }
21
+
22
+ /**
23
+ * @param {any} entry
24
+ * @returns {any}
25
+ */
26
+ export function catalogTemplateListItem(entry) {
27
+ return {
28
+ id: entry.id,
29
+ version: entry.defaultVersion,
30
+ source: "catalog",
31
+ name: entry.id,
32
+ package: entry.package,
33
+ defaultVersion: entry.defaultVersion,
34
+ description: entry.description,
35
+ tags: entry.tags,
36
+ ...(entry.surfaces ? { surfaces: entry.surfaces } : {}),
37
+ ...(entry.generators ? { generators: entry.generators } : {}),
38
+ ...(entry.stack ? { stack: entry.stack } : {}),
39
+ includesExecutableImplementation: entry.trust.includesExecutableImplementation,
40
+ trust: entry.trust
41
+ };
42
+ }
@@ -0,0 +1,67 @@
1
+ // @ts-check
2
+
3
+ import crypto from "node:crypto";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+
7
+ /**
8
+ * @param {string} currentPath
9
+ * @param {string} relativePath
10
+ * @param {string[]} files
11
+ * @returns {void}
12
+ */
13
+ export function collectFiles(currentPath, relativePath, files) {
14
+ const stat = fs.statSync(currentPath);
15
+ if (stat.isFile()) {
16
+ files.push(relativePath.replace(/\\/g, "/"));
17
+ return;
18
+ }
19
+ if (!stat.isDirectory()) {
20
+ return;
21
+ }
22
+ for (const entry of fs.readdirSync(currentPath)) {
23
+ collectFiles(path.join(currentPath, entry), path.join(relativePath, entry), files);
24
+ }
25
+ }
26
+
27
+ /**
28
+ * @param {string} filePath
29
+ * @returns {{ sha256: string, size: number }}
30
+ */
31
+ export function fileHash(filePath) {
32
+ const bytes = fs.readFileSync(filePath);
33
+ return {
34
+ sha256: crypto.createHash("sha256").update(bytes).digest("hex"),
35
+ size: bytes.length
36
+ };
37
+ }
38
+
39
+ /**
40
+ * @param {string} targetPath
41
+ * @returns {void}
42
+ */
43
+ export function ensureEmptyDirectory(targetPath) {
44
+ if (!fs.existsSync(targetPath)) {
45
+ fs.mkdirSync(targetPath, { recursive: true });
46
+ return;
47
+ }
48
+ if (!fs.statSync(targetPath).isDirectory()) {
49
+ throw new Error(`Cannot copy catalog topogram into non-directory path '${targetPath}'.`);
50
+ }
51
+ const entries = fs.readdirSync(targetPath).filter((/** @type {string} */ entry) => entry !== ".DS_Store");
52
+ if (entries.length > 0) {
53
+ throw new Error(`Refusing to copy catalog topogram into non-empty directory '${targetPath}'.`);
54
+ }
55
+ }
56
+
57
+ /**
58
+ * @param {string} sourcePath
59
+ * @param {string} targetPath
60
+ * @param {string} relativePath
61
+ * @param {string[]} files
62
+ * @returns {void}
63
+ */
64
+ export function copyPath(sourcePath, targetPath, relativePath, files) {
65
+ fs.cpSync(sourcePath, targetPath, { recursive: true });
66
+ collectFiles(targetPath, relativePath, files);
67
+ }
@@ -0,0 +1,123 @@
1
+ // @ts-check
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+
6
+ import { TOPOGRAM_SOURCE_FILE } from "./constants.js";
7
+ import { collectFiles, fileHash } from "./files.js";
8
+ import { DEFAULT_TOPO_FOLDER_NAME } from "../workspace-paths.js";
9
+
10
+ /**
11
+ * @param {string} projectRoot
12
+ * @param {{ catalogSource: string|null, entry: any, packageSpec: string, version: string }} input
13
+ * @returns {{ path: string, record: Record<string, any> }}
14
+ */
15
+ export function writeTopogramSourceRecord(projectRoot, input) {
16
+ const record = {
17
+ version: "0.1",
18
+ kind: "topogram",
19
+ copiedAt: new Date().toISOString(),
20
+ catalog: {
21
+ id: input.entry.id,
22
+ source: input.catalogSource
23
+ },
24
+ package: {
25
+ name: input.entry.package,
26
+ version: input.version,
27
+ spec: input.packageSpec
28
+ },
29
+ trust: {
30
+ includesExecutableImplementation: false
31
+ },
32
+ files: collectSourceFileRecords(projectRoot)
33
+ };
34
+ const sourcePath = path.join(projectRoot, TOPOGRAM_SOURCE_FILE);
35
+ fs.writeFileSync(sourcePath, `${JSON.stringify(record, null, 2)}\n`, "utf8");
36
+ return { path: sourcePath, record };
37
+ }
38
+
39
+ /**
40
+ * @param {string} projectRoot
41
+ * @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: [] }}
42
+ */
43
+ export function buildTopogramSourceStatus(projectRoot) {
44
+ const resolvedRoot = path.resolve(projectRoot);
45
+ const sourcePath = path.join(resolvedRoot, TOPOGRAM_SOURCE_FILE);
46
+ if (!fs.existsSync(sourcePath)) {
47
+ return {
48
+ ok: true,
49
+ exists: false,
50
+ path: sourcePath,
51
+ status: "missing",
52
+ source: null,
53
+ content: { changed: [], added: [], removed: [] },
54
+ diagnostics: [{
55
+ code: "topogram_source_missing",
56
+ severity: "warning",
57
+ message: `${TOPOGRAM_SOURCE_FILE} was not found. This project may not have been copied from a catalog topogram entry.`,
58
+ path: sourcePath,
59
+ suggestedFix: "Run `topogram catalog copy <id> <target>` to create a project with source provenance."
60
+ }],
61
+ errors: []
62
+ };
63
+ }
64
+ const source = JSON.parse(fs.readFileSync(sourcePath, "utf8"));
65
+ const trustedFiles = Array.isArray(source.files) ? source.files : [];
66
+ const trustedByPath = new Map(trustedFiles.map((/** @type {any} */ file) => [String(file.path), file]));
67
+ const currentByPath = new Map(collectSourceFileRecords(resolvedRoot).map((file) => [file.path, file]));
68
+ /** @type {string[]} */
69
+ const changed = [];
70
+ /** @type {string[]} */
71
+ const added = [];
72
+ /** @type {string[]} */
73
+ const removed = [];
74
+ for (const [filePath, current] of currentByPath) {
75
+ const trusted = trustedByPath.get(filePath);
76
+ if (!trusted) {
77
+ added.push(filePath);
78
+ } else if (trusted.sha256 !== current.sha256 || trusted.size !== current.size) {
79
+ changed.push(filePath);
80
+ }
81
+ }
82
+ for (const filePath of trustedByPath.keys()) {
83
+ if (!currentByPath.has(filePath)) {
84
+ removed.push(filePath);
85
+ }
86
+ }
87
+ const content = {
88
+ changed: changed.sort((a, b) => a.localeCompare(b)),
89
+ added: added.sort((a, b) => a.localeCompare(b)),
90
+ removed: removed.sort((a, b) => a.localeCompare(b))
91
+ };
92
+ return {
93
+ ok: true,
94
+ exists: true,
95
+ path: sourcePath,
96
+ status: content.changed.length || content.added.length || content.removed.length ? "changed" : "clean",
97
+ source,
98
+ content,
99
+ diagnostics: [],
100
+ errors: []
101
+ };
102
+ }
103
+
104
+ /**
105
+ * @param {string} projectRoot
106
+ * @returns {Array<{ path: string, sha256: string, size: number }>}
107
+ */
108
+ function collectSourceFileRecords(projectRoot) {
109
+ /** @type {string[]} */
110
+ const files = [];
111
+ for (const sourceRoot of [DEFAULT_TOPO_FOLDER_NAME, "topogram.project.json", "README.md"]) {
112
+ const sourcePath = path.join(projectRoot, sourceRoot);
113
+ if (fs.existsSync(sourcePath)) {
114
+ collectFiles(sourcePath, sourceRoot, files);
115
+ }
116
+ }
117
+ return files
118
+ .sort((a, b) => a.localeCompare(b))
119
+ .map((relativePath) => ({
120
+ path: relativePath,
121
+ ...fileHash(path.join(projectRoot, relativePath))
122
+ }));
123
+ }
@@ -0,0 +1,150 @@
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 { readGithubCatalogSourceText } from "../github-client.js";
8
+ import { defaultCatalogSource } from "../topogram-config.js";
9
+ import { GITHUB_TOKEN_HOSTS } from "./constants.js";
10
+ import { validateCatalog } from "./validation.js";
11
+
12
+ const FETCH_URL_SCRIPT = `
13
+ const source = process.argv[1];
14
+ const token = process.env.TOPOGRAM_FETCH_TOKEN || "";
15
+ const tokenHosts = new Set(["github.com", "api.github.com", "raw.githubusercontent.com"]);
16
+ function tokenAllowed(url) {
17
+ const hostname = new URL(url).hostname.toLowerCase();
18
+ return tokenHosts.has(hostname) || hostname.endsWith(".github.com");
19
+ }
20
+ async function readUrl(url, redirects = 0) {
21
+ if (redirects > 5) {
22
+ throw new Error("Too many redirects.");
23
+ }
24
+ const headers = {};
25
+ if (token && tokenAllowed(url)) {
26
+ headers.authorization = "Bearer " + token;
27
+ }
28
+ const response = await fetch(url, { headers, redirect: "manual" });
29
+ if (response.status >= 300 && response.status < 400 && response.headers.get("location")) {
30
+ const next = new URL(response.headers.get("location"), url).toString();
31
+ return readUrl(next, redirects + 1);
32
+ }
33
+ const text = await response.text();
34
+ if (!response.ok) {
35
+ const preview = text.trim().slice(0, 400);
36
+ throw new Error(String(response.status) + " " + response.statusText + (preview ? "\\n" + preview : ""));
37
+ }
38
+ return text;
39
+ }
40
+ try {
41
+ process.stdout.write(await readUrl(source));
42
+ } catch (error) {
43
+ console.error(error instanceof Error ? error.message : String(error));
44
+ process.exit(1);
45
+ }
46
+ `;
47
+
48
+ /**
49
+ * @param {string|undefined|null} source
50
+ * @returns {string}
51
+ */
52
+ export function catalogSourceOrDefault(source = null) {
53
+ return source || process.env.TOPOGRAM_CATALOG_SOURCE || defaultCatalogSource();
54
+ }
55
+
56
+ /**
57
+ * @param {string|undefined|null} source
58
+ * @returns {boolean}
59
+ */
60
+ export function isCatalogSourceDisabled(source) {
61
+ const normalized = String(source || "").trim().toLowerCase();
62
+ return normalized === "none" || normalized === "off" || normalized === "false";
63
+ }
64
+
65
+ /**
66
+ * @param {string|undefined|null} sourceInput
67
+ * @returns {{ source: string, catalog: any, diagnostics: any[] }}
68
+ */
69
+ export function loadCatalog(sourceInput = null) {
70
+ const source = catalogSourceOrDefault(sourceInput);
71
+ if (isCatalogSourceDisabled(source)) {
72
+ throw new Error("Catalog source is disabled.");
73
+ }
74
+ const text = readCatalogText(source);
75
+ const parsed = JSON.parse(text);
76
+ const validation = validateCatalog(parsed, source);
77
+ if (!validation.ok || !validation.catalog) {
78
+ throw new Error(validation.errors.join("\n") || `Catalog '${source}' is invalid.`);
79
+ }
80
+ return {
81
+ source,
82
+ catalog: validation.catalog,
83
+ diagnostics: validation.diagnostics
84
+ };
85
+ }
86
+
87
+ /**
88
+ * @param {string} source
89
+ * @returns {{ source: string, ok: boolean, catalog: any|null, diagnostics: any[], errors: string[] }}
90
+ */
91
+ export function checkCatalogSource(source) {
92
+ const text = readCatalogText(source);
93
+ const parsed = JSON.parse(text);
94
+ return {
95
+ source,
96
+ ...validateCatalog(parsed, source)
97
+ };
98
+ }
99
+
100
+ /**
101
+ * @param {string} source
102
+ * @returns {string}
103
+ */
104
+ function readCatalogText(source) {
105
+ if (source.startsWith("github:")) {
106
+ return readGithubCatalogSourceText(source);
107
+ }
108
+ if (source.startsWith("https://") || source.startsWith("http://")) {
109
+ return readUrlText(source);
110
+ }
111
+ const resolvedPath = path.resolve(source);
112
+ return fs.readFileSync(resolvedPath, "utf8");
113
+ }
114
+
115
+ /**
116
+ * @param {string} source
117
+ * @returns {string}
118
+ */
119
+ function readUrlText(source) {
120
+ const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN || "";
121
+ const tokenEnv = token && githubTokenAllowedForCatalogUrl(source)
122
+ ? { TOPOGRAM_FETCH_TOKEN: token }
123
+ : {};
124
+ const result = childProcess.spawnSync(process.execPath, ["--input-type=module", "-e", FETCH_URL_SCRIPT, source], {
125
+ encoding: "utf8",
126
+ env: {
127
+ ...process.env,
128
+ ...tokenEnv,
129
+ PATH: process.env.PATH || ""
130
+ }
131
+ });
132
+ if (result.status !== 0) {
133
+ const reason = result.error?.message || result.stderr || result.stdout || "unknown error";
134
+ throw new Error(`Failed to read catalog URL '${source}'.\n${reason}`.trim());
135
+ }
136
+ return result.stdout;
137
+ }
138
+
139
+ /**
140
+ * @param {string} source
141
+ * @returns {boolean}
142
+ */
143
+ function githubTokenAllowedForCatalogUrl(source) {
144
+ try {
145
+ const hostname = new URL(source).hostname.toLowerCase();
146
+ return GITHUB_TOKEN_HOSTS.has(hostname) || hostname.endsWith(".github.com");
147
+ } catch {
148
+ return false;
149
+ }
150
+ }