@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,2145 +1,43 @@
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 os from "node:os";
7
- import path from "node:path";
8
-
9
- import {
10
- buildTopogramSourceStatus,
11
- catalogSourceOrDefault,
12
- catalogTemplateListItem,
13
- isCatalogSourceDisabled,
14
- loadCatalog
15
- } from "../../catalog.js";
16
- import { stableStringify } from "../../format.js";
17
- import { parsePath } from "../../parser.js";
18
- import { assertSafeNpmSpec, localNpmrcEnv } from "../../npm-safety.js";
19
- import {
20
- createNewProject,
21
- applyTemplateUpdate,
22
- applyTemplateUpdateFileAction,
23
- buildTemplateUpdateCheck,
24
- buildTemplateUpdatePlan,
25
- buildTemplateUpdateStatus,
26
- loadTemplatePolicy,
27
- packageScopeFromSpec,
28
- resolveTemplate,
29
- templatePolicyDiagnosticsForTemplate,
30
- writeTemplatePolicy,
31
- writeTemplatePolicyForProject
32
- } from "../../new-project.js";
33
- import {
34
- formatProjectConfigErrors,
35
- loadProjectConfig,
36
- validateProjectConfig,
37
- validateProjectOutputOwnership
38
- } from "../../project-config.js";
39
- import { resolveWorkspace } from "../../resolver.js";
40
- import {
41
- getTemplateTrustStatus,
42
- implementationRequiresTrust,
43
- TEMPLATE_TRUST_FILE,
44
- templateTrustRecoveryGuidance,
45
- validateProjectImplementationTrust
46
- } from "../../template-trust.js";
47
- import {
48
- buildCatalogShowPayload,
49
- catalogShowCommands,
50
- shellCommandArg
51
- } from "./catalog.js";
52
- import { runNpmForPackageUpdate } from "./package.js";
53
-
54
- const TEMPLATE_FILES_MANIFEST = ".topogram-template-files.json";
55
- const TEMPLATE_POLICY_FILE = "topogram.template-policy.json";
56
- const ENGINE_ROOT = decodeURIComponent(new URL("../../../", import.meta.url).pathname);
57
- const TEMPLATES_ROOT = path.join(ENGINE_ROOT, "templates");
58
-
59
- /**
60
- * @returns {void}
61
- */
62
- export function printTemplateHelp() {
63
- console.log("Usage: topogram template list [--json] [--catalog <path-or-source>]");
64
- console.log(" or: topogram template explain [path] [--json]");
65
- console.log(" or: topogram template status [path] [--latest] [--json]");
66
- console.log(" or: topogram template detach [path] [--dry-run] [--remove-policy] [--json]");
67
- console.log(" or: topogram template check <template-spec-or-path> [--json]");
68
- console.log(" or: topogram template policy init [path] [--json]");
69
- console.log(" or: topogram template policy check [path] [--json]");
70
- console.log(" or: topogram template policy explain [path] [--json]");
71
- console.log(" or: topogram template policy pin <template-id@version> [path] [--json]");
72
- console.log(" or: topogram template update [path] --status|--recommend|--plan|--check|--apply [--template <spec>|--latest] [--json] [--out <path>]");
73
- console.log("");
74
- console.log("Template commands inspect catalog-backed starters, project provenance, trust policy, and update plans.");
75
- console.log("");
76
- console.log("Examples:");
77
- console.log(" topogram template list");
78
- console.log(" topogram template explain");
79
- console.log(" topogram template status");
80
- console.log(" topogram template status --latest");
81
- console.log(" topogram template policy check");
82
- console.log(" topogram template check ./local-template");
83
- console.log(" topogram template update --recommend");
84
- }
85
-
86
- /**
87
- * @param {unknown} error
88
- * @returns {string}
89
- */
90
- function messageFromError(error) {
91
- return error instanceof Error ? error.message : String(error);
92
- }
93
-
94
- /**
95
- * @param {...{ ok: boolean, errors?: any[] }|null|undefined} results
96
- * @returns {{ ok: boolean, errors: any[] }}
97
- */
98
- function combineProjectValidationResults(...results) {
99
- const errors = [];
100
- for (const result of results) {
101
- errors.push(...(result?.errors || []));
102
- }
103
- return {
104
- ok: errors.length === 0,
105
- errors
106
- };
107
- }
108
-
109
- /**
110
- * @param {string} spec
111
- * @returns {string}
112
- */
113
- function packageNameFromPackageSpec(spec) {
114
- if (spec.startsWith("@")) {
115
- const segments = spec.split("/");
116
- if (segments.length < 2) {
117
- throw new Error(`Invalid scoped package spec '${spec}'.`);
118
- }
119
- const scope = segments[0];
120
- const nameAndVersion = segments.slice(1).join("/");
121
- const versionIndex = nameAndVersion.indexOf("@");
122
- return `${scope}/${versionIndex >= 0 ? nameAndVersion.slice(0, versionIndex) : nameAndVersion}`;
123
- }
124
- const versionIndex = spec.indexOf("@");
125
- return versionIndex >= 0 ? spec.slice(0, versionIndex) : spec;
126
- }
127
-
128
- /**
129
- * @param {{ catalogSource?: string|null }} [options]
130
- * @returns {{ ok: boolean, catalog: { source: string|null, loaded: boolean }, templates: Array<Record<string, any>>, diagnostics: Array<Record<string, any>>, errors: string[] }}
131
- */
132
- export function buildTemplateListPayload(options = {}) {
133
- const catalogSource = catalogSourceOrDefault(options.catalogSource || null);
134
- /** @type {Array<Record<string, any>>} */
135
- const templates = [];
136
- /** @type {Array<Record<string, any>>} */
137
- const diagnostics = [];
138
- let catalogLoaded = false;
139
- if (!isCatalogSourceDisabled(catalogSource)) {
140
- try {
141
- const loaded = loadCatalog(catalogSource);
142
- catalogLoaded = true;
143
- const entries = /** @type {any[]} */ (loaded.catalog.entries || []);
144
- templates.push(
145
- ...entries
146
- .filter((entry) => entry.kind === "template")
147
- .map((entry) => templateListItemFromCatalogEntry(entry, loaded.source))
148
- );
149
- } catch (error) {
150
- diagnostics.push({
151
- code: "catalog_unavailable",
152
- severity: "warning",
153
- message: messageFromError(error),
154
- path: catalogSource,
155
- suggestedFix: "Run `topogram catalog list` after authenticating, or pass a local template path/package spec directly."
156
- });
157
- }
158
- }
159
- return {
160
- ok: true,
161
- catalog: {
162
- source: isCatalogSourceDisabled(catalogSource) ? null : catalogSource,
163
- loaded: catalogLoaded
164
- },
165
- templates,
166
- diagnostics,
167
- errors: []
168
- };
169
- }
170
-
171
- /**
172
- * @param {any} entry
173
- * @param {string} source
174
- * @returns {Record<string, any>}
175
- */
176
- function templateListItemFromCatalogEntry(entry, source) {
177
- const item = catalogTemplateListItem(entry);
178
- const commands = catalogShowCommands(entry, source);
179
- return {
180
- ...item,
181
- surfaces: Array.isArray(item.surfaces) ? item.surfaces : [],
182
- generators: Array.isArray(item.generators) ? item.generators : [],
183
- stack: typeof item.stack === "string" ? item.stack : null,
184
- isDefault: item.id === "hello-web",
185
- recommendedCommand: commands.primary,
186
- commands
187
- };
188
- }
189
-
190
- /**
191
- * @param {ReturnType<typeof buildTemplateListPayload>} payload
192
- * @returns {void}
193
- */
194
- export function printTemplateList(payload) {
195
- console.log("Template starters:");
196
- console.log("Catalog aliases resolve to versioned package installs. Local paths and full package specs can also be used with `topogram new`.");
197
- if (payload.catalog.source) {
198
- console.log(`Catalog: ${payload.catalog.source} (${payload.catalog.loaded ? "loaded" : "unavailable"})`);
199
- } else {
200
- console.log("Catalog: disabled");
201
- }
202
- for (const template of payload.templates) {
203
- const defaultLabel = template.isDefault ? " (default)" : "";
204
- const stack = template.stack || "not declared";
205
- const surfaces = Array.isArray(template.surfaces) && template.surfaces.length > 0
206
- ? template.surfaces.join(", ")
207
- : "not declared";
208
- const command = template.recommendedCommand || `topogram new ./my-app --template ${shellCommandArg(template.id)}`;
209
- console.log(`- ${template.id}@${template.version}${defaultLabel}`);
210
- console.log(` Source: ${template.source} | Surfaces: ${surfaces} | Stack: ${stack} | Executable implementation: ${template.includesExecutableImplementation ? "yes" : "no"}`);
211
- console.log(` New: ${command}`);
212
- }
213
- for (const diagnostic of payload.diagnostics) {
214
- console.warn(`Warning: ${diagnostic.message}`);
215
- }
216
- }
217
-
218
- /**
219
- * @param {Record<string, any>} template
220
- * @param {"catalog"} sourceKind
221
- * @param {string|null} packageSpec
222
- * @param {{ primary: string|null, followUp: string[] }} commands
223
- * @returns {{ surfaces: string[], generators: string[], stack: string|null, packageSpec: string|null, packageName: string|null, version: string|null, executableImplementation: boolean, policyImpact: string, recommendedCommand: string|null, followUp: string[], notes: string[] }}
224
- */
225
- function templateDecisionSummary(template, sourceKind, packageSpec, commands) {
226
- const trust = template.trust && typeof template.trust === "object" ? template.trust : null;
227
- const executable = trust
228
- ? Boolean(trust.includesExecutableImplementation)
229
- : Boolean(template.includesExecutableImplementation);
230
- const surfaces = Array.isArray(template.surfaces) ? template.surfaces : [];
231
- const generators = Array.isArray(template.generators) ? template.generators : [];
232
- const stack = typeof template.stack === "string" && template.stack ? template.stack : null;
233
- const notes = [];
234
- if (sourceKind === "catalog") {
235
- notes.push("Catalog templates resolve to versioned package installs; the catalog is an index, not the template payload.");
236
- }
237
- if (surfaces.length === 0) {
238
- notes.push("Surface metadata is not declared in this catalog entry.");
239
- }
240
- if (generators.length === 0) {
241
- notes.push("Generator metadata is not declared in this catalog entry.");
242
- }
243
- return {
244
- surfaces,
245
- generators,
246
- stack,
247
- packageSpec,
248
- packageName: template.package || (packageSpec ? packageNameFromPackageSpec(packageSpec) : null),
249
- version: template.defaultVersion || template.version || null,
250
- executableImplementation: executable,
251
- policyImpact: executable
252
- ? "Copies implementation/ code into the project; topogram new does not execute it, but topogram generate may load it after local trust is recorded."
253
- : "No executable implementation trust is required for this template.",
254
- recommendedCommand: commands.primary,
255
- followUp: commands.followUp,
256
- notes
257
- };
258
- }
259
-
260
- /**
261
- * @param {string} id
262
- * @param {string|null} source
263
- * @returns {{ ok: boolean, source: "catalog"|null, catalog: { source: string|null, version: string|null }, template: Record<string, any>|null, packageSpec: string|null, decision: ReturnType<typeof templateDecisionSummary>|null, commands: { primary: string|null, followUp: string[] }, diagnostics: any[], errors: string[] }}
264
- */
265
- export function buildTemplateShowPayload(id, source) {
266
- if (!id || id.startsWith("-")) {
267
- throw new Error("topogram template show requires <id>.");
268
- }
269
- const catalogPayload = buildCatalogShowPayload(id, source);
270
- if (!catalogPayload.ok || !catalogPayload.entry) {
271
- return {
272
- ok: false,
273
- source: "catalog",
274
- catalog: {
275
- source: catalogPayload.source,
276
- version: catalogPayload.catalog.version
277
- },
278
- template: null,
279
- packageSpec: null,
280
- decision: null,
281
- commands: { primary: null, followUp: [] },
282
- diagnostics: catalogPayload.diagnostics,
283
- errors: catalogPayload.errors
284
- };
285
- }
286
- if (catalogPayload.entry.kind !== "template") {
287
- const diagnostic = {
288
- code: "catalog_entry_not_template",
289
- severity: "error",
290
- message: `Catalog entry '${id}' is a ${catalogPayload.entry.kind}, not a template.`,
291
- path: catalogPayload.source,
292
- suggestedFix: "Use `topogram catalog show` for non-template catalog entries."
293
- };
294
- return {
295
- ok: false,
296
- source: "catalog",
297
- catalog: {
298
- source: catalogPayload.source,
299
- version: catalogPayload.catalog.version
300
- },
301
- template: catalogPayload.entry,
302
- packageSpec: catalogPayload.packageSpec,
303
- decision: null,
304
- commands: catalogPayload.commands,
305
- diagnostics: [...catalogPayload.diagnostics, diagnostic],
306
- errors: [diagnostic.message]
307
- };
308
- }
309
- return {
310
- ok: true,
311
- source: "catalog",
312
- catalog: {
313
- source: catalogPayload.source,
314
- version: catalogPayload.catalog.version
315
- },
316
- template: catalogPayload.entry,
317
- packageSpec: catalogPayload.packageSpec,
318
- decision: templateDecisionSummary(catalogPayload.entry, "catalog", catalogPayload.packageSpec, catalogPayload.commands),
319
- commands: catalogPayload.commands,
320
- diagnostics: catalogPayload.diagnostics,
321
- errors: []
322
- };
323
- }
324
-
325
- /**
326
- * @param {ReturnType<typeof buildTemplateShowPayload>} payload
327
- * @returns {void}
328
- */
329
- export function printTemplateShow(payload) {
330
- if (!payload.ok || !payload.template) {
331
- console.log("Template not found.");
332
- if (payload.catalog.source) {
333
- console.log(`Catalog: ${payload.catalog.source}`);
334
- }
335
- for (const diagnostic of payload.diagnostics) {
336
- const label = diagnostic.severity === "warning" ? "Warning" : "Error";
337
- console.log(`${label}: ${diagnostic.message}`);
338
- }
339
- return;
340
- }
341
- const template = payload.template;
342
- console.log(`Template: ${template.id}`);
343
- console.log(`Source: ${payload.source}`);
344
- if (template.name) {
345
- const defaultLabel = template.isDefault ? " (default)" : "";
346
- console.log(`Name: ${template.name}${defaultLabel}`);
347
- }
348
- if (payload.catalog.source) {
349
- console.log(`Catalog: ${payload.catalog.source}`);
350
- }
351
- if (payload.packageSpec) {
352
- console.log(`Package: ${payload.packageSpec}`);
353
- }
354
- if (template.description) {
355
- console.log(`Description: ${template.description}`);
356
- }
357
- if (payload.decision) {
358
- console.log("");
359
- console.log("What it creates:");
360
- console.log(` Surfaces: ${payload.decision.surfaces.join(", ") || "not declared"}`);
361
- console.log(` Stack: ${payload.decision.stack || "not declared"}`);
362
- console.log(` Generators: ${payload.decision.generators.join(", ") || "not declared"}`);
363
- console.log(` Package: ${payload.decision.packageSpec || "not declared"}`);
364
- console.log(` Executable implementation: ${payload.decision.executableImplementation ? "yes" : "no"}`);
365
- console.log(` Policy impact: ${payload.decision.policyImpact}`);
366
- for (const note of payload.decision.notes) {
367
- console.log(` Note: ${note}`);
368
- }
369
- }
370
- console.log("");
371
- console.log("Details:");
372
- if (Array.isArray(template.tags) && template.tags.length > 0) {
373
- console.log(`Tags: ${template.tags.join(", ")}`);
374
- }
375
- if (template.trust?.scope) {
376
- console.log(`Trust scope: ${template.trust.scope}`);
377
- }
378
- const executable = template.trust
379
- ? template.trust.includesExecutableImplementation
380
- : template.includesExecutableImplementation;
381
- console.log(`Executable implementation: ${executable ? "yes" : "no"}`);
382
- if (template.trust?.notes) {
383
- console.log(`Trust notes: ${template.trust.notes}`);
384
- }
385
- console.log("");
386
- console.log("Recommended command:");
387
- console.log(` ${payload.commands.primary}`);
388
- if (payload.commands.followUp.length > 0) {
389
- console.log("Follow-up:");
390
- for (const command of payload.commands.followUp) {
391
- console.log(` ${command}`);
392
- }
393
- }
394
- for (const diagnostic of payload.diagnostics) {
395
- if (diagnostic.severity === "warning") {
396
- console.warn(`Warning: ${diagnostic.message}`);
397
- }
398
- }
399
- }
400
-
401
- /**
402
- * @param {Record<string, any>|null|undefined} projectConfig
403
- * @returns {{ id: string|null, version: string|null, source: string|null, sourceSpec: string|null, requested: string|null, sourceRoot: string|null, catalog: Record<string, any>|null, includesExecutableImplementation: boolean|null }}
404
- */
405
- export function templateMetadataFromProjectConfig(projectConfig) {
406
- const template = projectConfig?.template || {};
407
- return {
408
- id: typeof template.id === "string" ? template.id : null,
409
- version: typeof template.version === "string" ? template.version : null,
410
- source: typeof template.source === "string" ? template.source : null,
411
- sourceSpec: typeof template.sourceSpec === "string" ? template.sourceSpec : null,
412
- requested: typeof template.requested === "string" ? template.requested : null,
413
- sourceRoot: typeof template.sourceRoot === "string" ? template.sourceRoot : null,
414
- catalog: template.catalog && typeof template.catalog === "object" && !Array.isArray(template.catalog)
415
- ? template.catalog
416
- : null,
417
- includesExecutableImplementation: typeof template.includesExecutableImplementation === "boolean"
418
- ? template.includesExecutableImplementation
419
- : null
420
- };
421
- }
422
-
423
- /**
424
- * @param {string} packageName
425
- * @returns {string}
426
- */
427
- function latestVersionForPackage(packageName) {
428
- assertSafeNpmSpec(packageName);
429
- const npmBin = process.platform === "win32" ? "npm.cmd" : "npm";
430
- const result = childProcess.spawnSync(npmBin, ["view", "--json", "--", packageName, "version"], {
431
- encoding: "utf8",
432
- env: {
433
- ...process.env,
434
- ...localNpmrcEnv(process.cwd()),
435
- PATH: process.env.PATH || ""
436
- }
437
- });
438
- if (result.status !== 0) {
439
- throw new Error(`Failed to inspect latest version for '${packageName}'.\n${result.stderr || result.stdout}`.trim());
440
- }
441
- const raw = (result.stdout || "").trim();
442
- if (!raw) {
443
- throw new Error(`npm view returned no version for '${packageName}'.`);
444
- }
445
- const parsed = JSON.parse(raw);
446
- if (typeof parsed !== "string" || !parsed) {
447
- throw new Error(`npm view returned an invalid version for '${packageName}'.`);
448
- }
449
- return parsed;
450
- }
451
-
452
- /**
453
- * @param {ReturnType<typeof templateMetadataFromProjectConfig>} template
454
- * @returns {{ checked: boolean, supported: boolean, packageName: string|null, version: string|null, isCurrent: boolean|null, candidateSpec: string|null, reason: string|null }}
455
- */
456
- export function latestTemplateInfo(template) {
457
- if (template.source !== "package") {
458
- return {
459
- checked: true,
460
- supported: false,
461
- packageName: null,
462
- version: null,
463
- isCurrent: null,
464
- candidateSpec: null,
465
- reason: "Latest-version lookup is only supported for package-backed templates."
466
- };
467
- }
468
- const packageName = packageNameFromPackageSpec(template.sourceSpec || template.requested || template.id || "");
469
- const version = latestVersionForPackage(packageName);
470
- return {
471
- checked: true,
472
- supported: true,
473
- packageName,
474
- version,
475
- isCurrent: template.version === version,
476
- candidateSpec: `${packageName}@${version}`,
477
- reason: null
478
- };
479
- }
480
-
481
- /**
482
- * @param {{ config: Record<string, any>, configPath: string|null, configDir: string }} projectConfigInfo
483
- * @param {{ latest?: boolean }} [options]
484
- * @returns {{ ok: boolean, template: ReturnType<typeof templateMetadataFromProjectConfig>, trust: ReturnType<typeof getTemplateTrustStatus>|null, latest: { checked: boolean, supported?: boolean, packageName?: string|null, version?: string|null, isCurrent?: boolean|null, candidateSpec?: string|null, reason: string|null }, recommendations: string[] }}
485
- */
486
- export function buildTemplateStatusPayload(projectConfigInfo, options = {}) {
487
- const template = templateMetadataFromProjectConfig(projectConfigInfo.config);
488
- const recommendations = [];
489
- /** @type {ReturnType<typeof getTemplateTrustStatus>|null} */
490
- let trust = null;
491
- if (projectConfigInfo.config.implementation) {
492
- trust = getTemplateTrustStatus({
493
- config: projectConfigInfo.config.implementation,
494
- configPath: projectConfigInfo.configPath,
495
- configDir: projectConfigInfo.configDir
496
- }, projectConfigInfo.config);
497
- if (!trust.ok) {
498
- recommendations.push("Run `topogram trust diff` to review implementation changes, then `topogram trust template` to trust the current files.");
499
- }
500
- }
501
- if (!template.id) {
502
- recommendations.push("No template metadata found in topogram.project.json.");
503
- }
504
- const latest = options.latest
505
- ? latestTemplateInfo(template)
506
- : {
507
- checked: false,
508
- supported: false,
509
- packageName: null,
510
- version: null,
511
- isCurrent: null,
512
- candidateSpec: null,
513
- reason: "Registry lookups are not performed by default."
514
- };
515
- if (latest.checked && latest.supported && latest.candidateSpec && latest.isCurrent === false) {
516
- recommendations.push(`Run \`topogram template update --recommend --template ${latest.candidateSpec}\` to review the latest template.`);
517
- }
518
- return {
519
- ok: trust ? trust.ok : true,
520
- template,
521
- trust,
522
- latest,
523
- recommendations
524
- };
525
- }
526
-
527
- /**
528
- * @param {ReturnType<typeof buildTemplateStatusPayload>} payload
529
- * @returns {void}
530
- */
531
- export function printTemplateStatus(payload) {
532
- if (!payload.template.id) {
533
- console.log("Template status: detached");
534
- } else if (payload.trust?.requiresTrust) {
535
- console.log(`Template status: attached; implementation trust: ${payload.ok ? "trusted" : "review required"}`);
536
- } else {
537
- console.log("Template status: attached; implementation trust: not required");
538
- }
539
- if (payload.template.id) {
540
- console.log(`Template: ${payload.template.id}@${payload.template.version || "unknown"}`);
541
- }
542
- if (payload.template.source) {
543
- console.log(`Source: ${payload.template.source}`);
544
- }
545
- if (payload.template.sourceSpec) {
546
- console.log(`Source spec: ${payload.template.sourceSpec}`);
547
- }
548
- if (payload.template.requested) {
549
- console.log(`Requested: ${payload.template.requested}`);
550
- }
551
- if (payload.template.catalog) {
552
- console.log(`Catalog: ${payload.template.catalog.id || "unknown"} from ${payload.template.catalog.source || "unknown"}`);
553
- }
554
- if (payload.template.sourceRoot) {
555
- console.log(`Source root: ${payload.template.sourceRoot}`);
556
- }
557
- if (!payload.latest.checked) {
558
- console.log("Latest version: not checked");
559
- } else if (!payload.latest.supported) {
560
- console.log(`Latest version: not checked (${payload.latest.reason})`);
561
- } else {
562
- console.log(`Latest version: ${payload.latest.version}`);
563
- if (payload.latest.packageName) {
564
- console.log(`Latest package: ${payload.latest.packageName}`);
565
- }
566
- if (payload.latest.candidateSpec) {
567
- console.log(`Latest candidate: ${payload.latest.candidateSpec}`);
568
- }
569
- console.log(`Latest status: ${payload.latest.isCurrent ? "current" : "update available"}`);
570
- }
571
- if (payload.trust) {
572
- if (payload.trust.trustRecord?.trustedAt) {
573
- console.log(`Trusted at: ${payload.trust.trustRecord.trustedAt}`);
574
- }
575
- if (payload.trust.implementation.module) {
576
- console.log(`Implementation: ${payload.trust.implementation.module}`);
577
- }
578
- if (payload.trust.content.trustedDigest) {
579
- console.log(`Trusted digest: ${payload.trust.content.trustedDigest}`);
580
- }
581
- if (payload.trust.content.currentDigest) {
582
- console.log(`Current digest: ${payload.trust.content.currentDigest}`);
583
- }
584
- for (const issue of payload.trust.issues) {
585
- console.log(`Issue: ${issue}`);
586
- }
587
- for (const filePath of payload.trust.content.changed) {
588
- console.log(`Changed: ${filePath}`);
589
- }
590
- for (const filePath of payload.trust.content.added) {
591
- console.log(`Added: ${filePath}`);
592
- }
593
- for (const filePath of payload.trust.content.removed) {
594
- console.log(`Removed: ${filePath}`);
595
- }
596
- }
597
- for (const recommendation of payload.recommendations) {
598
- console.log(recommendation);
599
- }
600
- }
601
-
602
- /**
603
- * @param {{ config: Record<string, any>, configPath: string|null, configDir: string }} projectConfigInfo
604
- * @returns {{ ok: boolean, projectRoot: string, projectConfigPath: string|null, attached: boolean, ownership: "template-attached"|"project-owned", template: ReturnType<typeof templateMetadataFromProjectConfig>, trust: ReturnType<typeof getTemplateTrustStatus>|null, baseline: ReturnType<typeof buildTemplateOwnedBaselineStatus>, source: ReturnType<typeof buildTopogramSourceStatus>, commands: { status: string, detachDryRun: string|null, detach: string|null, updateCheck: string|null, trustStatus: string|null, trustTemplate: string|null, check: string, generate: string }, summary: string[], diagnostics: any[], errors: string[] }}
605
- */
606
- export function buildTemplateExplainPayload(projectConfigInfo) {
607
- const template = templateMetadataFromProjectConfig(projectConfigInfo.config);
608
- const attached = Boolean(template.id);
609
- const projectRoot = projectConfigInfo.configDir;
610
- const baseline = buildTemplateOwnedBaselineStatus(projectRoot);
611
- const source = buildTopogramSourceStatus(projectRoot);
612
- /** @type {ReturnType<typeof getTemplateTrustStatus>|null} */
613
- let trust = null;
614
- if (projectConfigInfo.config.implementation) {
615
- trust = getTemplateTrustStatus({
616
- config: projectConfigInfo.config.implementation,
617
- configPath: projectConfigInfo.configPath,
618
- configDir: projectConfigInfo.configDir
619
- }, projectConfigInfo.config);
620
- }
621
- const summary = [];
622
- if (attached) {
623
- summary.push("This project is still attached to its starter template.");
624
- summary.push("Local edits are allowed; template update checks are opt-in.");
625
- } else {
626
- summary.push("This project is detached from starter-template update tracking.");
627
- summary.push("The project owns its Topogram files and template updates no longer apply.");
628
- }
629
- if (baseline.state === "diverged") {
630
- summary.push("Template-derived files have local changes; those changes are project-owned.");
631
- } else if (baseline.state === "matches-template") {
632
- summary.push("Template-derived files still match the recorded template baseline.");
633
- }
634
- if (trust?.requiresTrust && trust.ok) {
635
- summary.push("Executable implementation trust is retained and currently matches reviewed files.");
636
- } else if (trust?.requiresTrust && !trust.ok) {
637
- summary.push("Executable implementation changed since it was trusted and needs review.");
638
- } else {
639
- summary.push("No executable implementation trust review is required.");
640
- }
641
- return {
642
- ok: trust ? trust.ok : true,
643
- projectRoot,
644
- projectConfigPath: projectConfigInfo.configPath,
645
- attached,
646
- ownership: attached ? "template-attached" : "project-owned",
647
- template,
648
- trust,
649
- baseline,
650
- source,
651
- commands: {
652
- status: "topogram source status --local",
653
- detachDryRun: attached ? "topogram template detach --dry-run" : null,
654
- detach: attached ? "topogram template detach" : null,
655
- updateCheck: attached ? "topogram template update --check" : null,
656
- trustStatus: trust?.requiresTrust ? "topogram trust status" : null,
657
- trustTemplate: trust?.requiresTrust && !trust.ok ? "topogram trust template" : null,
658
- check: "topogram check",
659
- generate: "topogram generate"
660
- },
661
- summary,
662
- diagnostics: source.diagnostics,
663
- errors: trust && !trust.ok ? trust.issues : []
664
- };
665
- }
666
-
667
- /**
668
- * @param {ReturnType<typeof buildTemplateExplainPayload>} payload
669
- * @returns {void}
670
- */
671
- export function printTemplateExplain(payload) {
672
- console.log(`Template lifecycle: ${payload.attached ? "attached" : "detached"}`);
673
- console.log(`Ownership: ${payload.ownership}`);
674
- console.log(`Project: ${payload.projectRoot}`);
675
- if (payload.projectConfigPath) {
676
- console.log(`Project config: ${payload.projectConfigPath}`);
677
- }
678
- if (payload.template.id) {
679
- console.log(`Template: ${payload.template.id}@${payload.template.version || "unknown"}`);
680
- console.log(`Requested: ${payload.template.requested || "unknown"}`);
681
- console.log(`Source: ${payload.template.sourceSpec || payload.template.source || "unknown"}`);
682
- if (payload.template.catalog) {
683
- console.log(`Catalog: ${payload.template.catalog.id || "unknown"} from ${payload.template.catalog.source || "unknown"}`);
684
- }
685
- } else {
686
- console.log("Template: none");
687
- }
688
- console.log(`Template baseline: ${payload.baseline.state}`);
689
- console.log(`Template baseline meaning: ${payload.baseline.meaning}`);
690
- if (payload.baseline.content.changed.length > 0) {
691
- console.log(`Template baseline changed files: ${payload.baseline.content.changed.length}`);
692
- }
693
- if (payload.baseline.content.removed.length > 0) {
694
- console.log(`Template baseline removed files: ${payload.baseline.content.removed.length}`);
695
- }
696
- if (payload.trust) {
697
- console.log(`Implementation trust: ${payload.trust.requiresTrust ? (payload.trust.ok ? "trusted" : "review required") : "not required"}`);
698
- if (payload.trust.implementation.module) {
699
- console.log(`Implementation: ${payload.trust.implementation.module}`);
700
- }
701
- } else {
702
- console.log("Implementation trust: not required");
703
- }
704
- console.log("");
705
- console.log("Summary:");
706
- for (const line of payload.summary) {
707
- console.log(`- ${line}`);
708
- }
709
- console.log("");
710
- console.log("Useful commands:");
711
- console.log(` ${payload.commands.status}`);
712
- if (payload.commands.detachDryRun) {
713
- console.log(` ${payload.commands.detachDryRun}`);
714
- }
715
- if (payload.commands.detach) {
716
- console.log(` ${payload.commands.detach}`);
717
- }
718
- if (payload.commands.updateCheck) {
719
- console.log(` ${payload.commands.updateCheck}`);
720
- }
721
- if (payload.commands.trustStatus) {
722
- console.log(` ${payload.commands.trustStatus}`);
723
- }
724
- if (payload.commands.trustTemplate) {
725
- console.log(` ${payload.commands.trustTemplate}`);
726
- }
727
- console.log(` ${payload.commands.check}`);
728
- console.log(` ${payload.commands.generate}`);
729
- for (const diagnostic of payload.diagnostics) {
730
- const label = diagnostic.severity === "warning" ? "Warning" : "Error";
731
- console.log(`${label}: ${diagnostic.message}`);
732
- }
733
- }
734
-
735
- /**
736
- * @param {{ config: Record<string, any>, configPath: string|null, configDir: string }} projectConfigInfo
737
- * @param {{ dryRun?: boolean, removePolicy?: boolean }} [options]
738
- * @returns {{ ok: boolean, detached: boolean, dryRun: boolean, projectConfigPath: string, removedTemplate: Record<string, any>|null, implementationTrust: { retained: boolean, removed: boolean, path: string, reason: string }, removedFiles: string[], plannedRemovals: string[], preservedFiles: string[], diagnostics: any[], errors: any[] }}
739
- */
740
- export function buildTemplateDetachPayload(projectConfigInfo, options = {}) {
741
- const dryRun = Boolean(options.dryRun);
742
- const removePolicy = Boolean(options.removePolicy);
743
- const projectRoot = projectConfigInfo.configDir;
744
- const projectConfigPath = projectConfigInfo.configPath || path.join(projectRoot, "topogram.project.json");
745
- const nextConfig = JSON.parse(JSON.stringify(projectConfigInfo.config || {}));
746
- const removedTemplate = nextConfig.template && typeof nextConfig.template === "object" && !Array.isArray(nextConfig.template)
747
- ? nextConfig.template
748
- : null;
749
- const removedFiles = [];
750
- const plannedRemovals = [];
751
- const preservedFiles = [];
752
- const diagnostics = [];
753
-
754
- if (removedTemplate) {
755
- delete nextConfig.template;
756
- }
757
-
758
- const manifestPath = path.join(projectRoot, TEMPLATE_FILES_MANIFEST);
759
- const policyPath = path.join(projectRoot, TEMPLATE_POLICY_FILE);
760
- const trustPath = path.join(projectRoot, TEMPLATE_TRUST_FILE);
761
- const implementationRemains = Boolean(projectConfigInfo.config?.implementation);
762
-
763
- /** @param {string} filePath */
764
- const maybeRemove = (filePath) => {
765
- if (!fs.existsSync(filePath)) {
766
- return;
767
- }
768
- plannedRemovals.push(filePath);
769
- if (!dryRun) {
770
- fs.rmSync(filePath);
771
- removedFiles.push(filePath);
772
- }
773
- };
774
-
775
- maybeRemove(manifestPath);
776
- if (removePolicy) {
777
- maybeRemove(policyPath);
778
- } else if (fs.existsSync(policyPath)) {
779
- preservedFiles.push(policyPath);
780
- }
781
-
782
- const implementationTrust = {
783
- retained: false,
784
- removed: false,
785
- path: trustPath,
786
- reason: "not-present"
787
- };
788
- if (fs.existsSync(trustPath)) {
789
- if (implementationRemains) {
790
- implementationTrust.retained = true;
791
- implementationTrust.reason = "implementation-remains";
792
- preservedFiles.push(trustPath);
793
- } else {
794
- implementationTrust.removed = !dryRun;
795
- implementationTrust.reason = "no-implementation-config";
796
- plannedRemovals.push(trustPath);
797
- if (!dryRun) {
798
- fs.rmSync(trustPath);
799
- removedFiles.push(trustPath);
800
- }
801
- }
802
- }
803
-
804
- if (!removedTemplate) {
805
- diagnostics.push({
806
- code: "template_already_detached",
807
- severity: "warning",
808
- message: "topogram.project.json has no template metadata.",
809
- path: projectConfigPath,
810
- suggestedFix: "No detach action is required."
811
- });
812
- }
813
-
814
- if (!dryRun && removedTemplate) {
815
- fs.writeFileSync(projectConfigPath, `${stableStringify(nextConfig)}\n`, "utf8");
816
- }
817
-
818
- return {
819
- ok: true,
820
- detached: Boolean(removedTemplate),
821
- dryRun,
822
- projectConfigPath,
823
- removedTemplate,
824
- implementationTrust,
825
- removedFiles,
826
- plannedRemovals,
827
- preservedFiles,
828
- diagnostics,
829
- errors: []
830
- };
831
- }
832
-
833
- /**
834
- * @param {ReturnType<typeof buildTemplateDetachPayload>} payload
835
- * @returns {void}
836
- */
837
- export function printTemplateDetachPayload(payload) {
838
- if (payload.dryRun) {
839
- console.log(payload.detached ? "Template detach plan ready." : "Template detach plan: already detached.");
840
- } else {
841
- console.log(payload.detached ? "Template detached." : "Template already detached.");
842
- }
843
- console.log(`Project config: ${payload.projectConfigPath}`);
844
- if (payload.removedTemplate?.id) {
845
- console.log(`Removed template metadata: ${payload.removedTemplate.id}@${payload.removedTemplate.version || "unknown"}`);
846
- }
847
- if (payload.plannedRemovals.length > 0) {
848
- console.log(payload.dryRun ? "Would remove:" : "Removed:");
849
- for (const filePath of (payload.dryRun ? payload.plannedRemovals : payload.removedFiles)) {
850
- console.log(`- ${filePath}`);
851
- }
852
- }
853
- if (payload.preservedFiles.length > 0) {
854
- console.log("Preserved:");
855
- for (const filePath of payload.preservedFiles) {
856
- console.log(`- ${filePath}`);
857
- }
858
- }
859
- if (payload.implementationTrust.retained) {
860
- console.log("Implementation trust retained because implementation config remains.");
861
- } else if (payload.implementationTrust.removed) {
862
- console.log("Implementation trust removed because no implementation config remains.");
863
- }
864
- for (const diagnostic of payload.diagnostics) {
865
- const label = diagnostic.severity === "warning" ? "Warning" : "Error";
866
- console.log(`${label}: ${diagnostic.message}`);
867
- }
868
- console.log("Next: run `topogram source status --local`, then `topogram check`.");
869
- }
870
-
871
- /**
872
- * @param {any} plan
873
- * @returns {void}
874
- */
875
- export function printTemplateUpdatePlan(plan) {
876
- const isApply = plan.mode === "apply";
877
- const isCheck = plan.mode === "check";
878
- const isStatus = plan.mode === "status";
879
- const isFileAction = ["accept-current", "accept-candidate", "delete-current"].includes(plan.mode);
880
- if (isApply) {
881
- console.log(plan.ok ? "Template update apply: complete" : "Template update apply: refused");
882
- } else if (isStatus) {
883
- console.log(plan.ok ? "Template update status: aligned" : "Template update status: action needed");
884
- } else if (isCheck) {
885
- console.log(plan.ok ? "Template update check: aligned" : "Template update check: out of date");
886
- } else if (isFileAction) {
887
- console.log(plan.ok ? `Template update ${plan.mode}: complete` : `Template update ${plan.mode}: refused`);
888
- } else {
889
- console.log(plan.ok ? "Template update plan: ready for review" : "Template update plan: incompatible");
890
- }
891
- console.log(`Current: ${plan.current?.id || "unknown"}@${plan.current?.version || "unknown"}`);
892
- console.log(`Candidate: ${plan.candidate?.id || "unknown"}@${plan.candidate?.version || "unknown"}`);
893
- console.log(`Writes: ${plan.writes ? "applied" : "none"}`);
894
- if (plan.reportPath) {
895
- console.log(`Report: ${plan.reportPath}`);
896
- }
897
- console.log(`Added: ${plan.summary.added}`);
898
- console.log(`Changed: ${plan.summary.changed}`);
899
- console.log(`Current-only: ${plan.summary.currentOnly}`);
900
- console.log(`Unchanged: ${plan.summary.unchanged}`);
901
- if (isApply || isStatus || isFileAction) {
902
- const appliedCount = (plan.applied || []).length;
903
- const acceptedCount = (plan.accepted || []).length;
904
- const deletedCount = (plan.deleted || []).length;
905
- const skippedCount = (plan.skipped || []).length;
906
- const conflictCount = (plan.conflicts || []).length;
907
- if (isApply && appliedCount === 0 && skippedCount === 0 && conflictCount === 0 && plan.files.length === 0) {
908
- console.log("No changes to apply.");
909
- }
910
- if (isStatus && plan.files.length === 0 && conflictCount === 0 && skippedCount === 0 && (plan.diagnostics || []).length === 0) {
911
- console.log("No template update action needed.");
912
- }
913
- if (isApply && appliedCount > 0) {
914
- console.log(`Applied ${appliedCount} file(s).`);
915
- }
916
- if (isFileAction && appliedCount > 0) {
917
- console.log(`Accepted candidate for ${appliedCount} file(s).`);
918
- }
919
- if (acceptedCount > 0) {
920
- console.log(`Accepted current baseline for ${acceptedCount} file(s).`);
921
- }
922
- if (deletedCount > 0) {
923
- console.log(`Deleted ${deletedCount} current-only file(s).`);
924
- }
925
- if (skippedCount > 0) {
926
- console.log(`Skipped ${skippedCount} current-only file(s).`);
927
- }
928
- if (conflictCount > 0) {
929
- console.log(`Refused due to ${conflictCount} conflict(s).`);
930
- }
931
- }
932
- const diagnostics = Array.isArray(plan.diagnostics) ? plan.diagnostics : [];
933
- for (const diagnostic of diagnostics) {
934
- console.log(`[${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`);
935
- if (diagnostic.path) {
936
- console.log(` path: ${diagnostic.path}`);
937
- }
938
- if (diagnostic.suggestedFix) {
939
- console.log(` fix: ${diagnostic.suggestedFix}`);
940
- }
941
- if (diagnostic.step) {
942
- console.log(` step: ${diagnostic.step}`);
943
- }
944
- }
945
- for (const conflict of plan.conflicts || []) {
946
- console.log(`Conflict: ${conflict.path}`);
947
- console.log(` reason: ${conflict.reason}`);
948
- }
949
- for (const applied of plan.applied || []) {
950
- console.log(`Applied: ${applied.path}`);
951
- }
952
- for (const skipped of plan.skipped || []) {
953
- console.log(`Skipped: ${skipped.path}`);
954
- console.log(` reason: ${skipped.reason}`);
955
- }
956
- for (const accepted of plan.accepted || []) {
957
- console.log(`Accepted current: ${accepted.path}`);
958
- }
959
- for (const deleted of plan.deleted || []) {
960
- console.log(`Deleted: ${deleted.path}`);
961
- }
962
- for (const file of plan.files) {
963
- console.log("");
964
- console.log(`${file.kind.toUpperCase()}: ${file.path}`);
965
- if (file.current) {
966
- console.log(` current sha256: ${file.current.sha256}`);
967
- console.log(` current size: ${file.current.size}`);
968
- }
969
- if (file.candidate) {
970
- console.log(` candidate sha256: ${file.candidate.sha256}`);
971
- console.log(` candidate size: ${file.candidate.size}`);
972
- }
973
- if (file.binary) {
974
- console.log(" diff: binary file");
975
- } else if (file.diffOmitted && !file.unifiedDiff) {
976
- console.log(" diff: hash-only");
977
- }
978
- if (file.unifiedDiff) {
979
- console.log(file.unifiedDiff.trimEnd());
980
- }
981
- }
982
- if (plan.files.length === 0) {
983
- console.log("No template-owned file changes found.");
984
- }
985
- if (!isApply && !isCheck && !isStatus && !isFileAction) {
986
- console.log("");
987
- console.log("This command did not write files. Review the plan before applying template updates.");
988
- } else if (isCheck || isStatus) {
989
- console.log("");
990
- console.log("This command did not write files.");
991
- }
992
- }
993
-
994
- /**
995
- * @param {any} status
996
- * @returns {any}
997
- */
998
- export function buildTemplateUpdateRecommendationPayload(status) {
999
- /** @type {Array<{ action: string, command: string|null, reason: string, path: string|null }>} */
1000
- const recommendations = [];
1001
- /** @type {any[]} */
1002
- const diagnostics = Array.isArray(status.diagnostics)
1003
- ? status.diagnostics.map((/** @type {any} */ diagnostic) => diagnostic.code === "template_update_available"
1004
- ? { ...diagnostic, severity: "warning" }
1005
- : diagnostic)
1006
- : [];
1007
- const errorDiagnostics = diagnostics.filter((/** @type {any} */ diagnostic) => diagnostic.severity === "error");
1008
- const conflicts = Array.isArray(status.conflicts) ? status.conflicts : [];
1009
- const skipped = Array.isArray(status.skipped) ? status.skipped : [];
1010
- const files = Array.isArray(status.files) ? status.files : [];
1011
- const addedChanged = files.filter((/** @type {any} */ file) => file.kind === "added" || file.kind === "changed");
1012
-
1013
- if (errorDiagnostics.length > 0) {
1014
- recommendations.push({
1015
- action: "resolve-errors",
1016
- command: "topogram template update --status",
1017
- reason: "Template policy, compatibility, baseline, or conflict errors must be resolved before applying candidate files.",
1018
- path: null
1019
- });
1020
- }
1021
- for (const conflict of conflicts) {
1022
- recommendations.push({
1023
- action: "review-conflict",
1024
- command: `topogram template update --accept-current ${conflict.path}`,
1025
- reason: "Local edits differ from the last trusted template-owned baseline. Accept current after review, or apply the candidate manually.",
1026
- path: conflict.path
1027
- });
1028
- }
1029
- if (addedChanged.length > 0 && conflicts.length === 0 && errorDiagnostics.length === 0) {
1030
- recommendations.push({
1031
- action: "apply-candidate",
1032
- command: "topogram template update --apply",
1033
- reason: `${addedChanged.length} added or changed candidate file(s) can be applied without local conflicts.`,
1034
- path: null
1035
- });
1036
- }
1037
- for (const item of skipped) {
1038
- recommendations.push({
1039
- action: "review-delete",
1040
- command: `topogram template update --delete-current ${item.path}`,
1041
- reason: "The candidate no longer owns this current file. Delete it only after review.",
1042
- path: item.path
1043
- });
1044
- }
1045
- if (files.length === 0 && errorDiagnostics.length === 0) {
1046
- recommendations.push({
1047
- action: "none",
1048
- command: null,
1049
- reason: "Current project files already match the candidate template.",
1050
- path: null
1051
- });
1052
- }
1053
- if (status.candidate?.id && status.candidate?.version && errorDiagnostics.length === 0) {
1054
- recommendations.push({
1055
- action: "pin-reviewed-version",
1056
- command: `topogram template policy pin ${status.candidate.id}@${status.candidate.version}`,
1057
- reason: "After reviewing or applying this candidate, pin the template version in project policy.",
1058
- path: null
1059
- });
1060
- }
1061
- return {
1062
- ...status,
1063
- ok: errorDiagnostics.length === 0,
1064
- mode: "recommend",
1065
- writes: false,
1066
- issues: errorDiagnostics.map((/** @type {any} */ diagnostic) => diagnostic.message),
1067
- diagnostics,
1068
- recommendations
1069
- };
1070
- }
1071
-
1072
- /**
1073
- * @param {ReturnType<typeof buildTemplateUpdateRecommendationPayload>} payload
1074
- * @returns {void}
1075
- */
1076
- export function printTemplateUpdateRecommendation(payload) {
1077
- console.log(payload.ok ? "Template update recommendation: ready" : "Template update recommendation: blocked");
1078
- console.log(`Current: ${payload.current?.id || "unknown"}@${payload.current?.version || "unknown"}`);
1079
- console.log(`Candidate: ${payload.candidate?.id || "unknown"}@${payload.candidate?.version || "unknown"}`);
1080
- console.log(`Added: ${payload.summary.added}`);
1081
- console.log(`Changed: ${payload.summary.changed}`);
1082
- console.log(`Current-only: ${payload.summary.currentOnly}`);
1083
- console.log(`Conflicts: ${payload.conflicts.length}`);
1084
- if (payload.reportPath) {
1085
- console.log(`Report: ${payload.reportPath}`);
1086
- }
1087
- for (const diagnostic of payload.diagnostics || []) {
1088
- console.log(`[${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`);
1089
- if (diagnostic.path) {
1090
- console.log(` path: ${diagnostic.path}`);
1091
- }
1092
- if (diagnostic.suggestedFix) {
1093
- console.log(` fix: ${diagnostic.suggestedFix}`);
1094
- }
1095
- }
1096
- console.log("");
1097
- console.log("Recommended next steps:");
1098
- for (const recommendation of payload.recommendations) {
1099
- console.log(`- ${recommendation.reason}`);
1100
- if (recommendation.command) {
1101
- console.log(` ${recommendation.command}`);
1102
- }
1103
- }
1104
- }
1105
-
1106
- /**
1107
- * @param {{ args: string[], inputPath: string, templateIndex: number, templateName: string|null|undefined, useLatestTemplate: boolean, outPath?: string|null }} options
1108
- * @returns {any}
1109
- */
1110
- export function buildTemplateUpdateCliPayload(options) {
1111
- const { args, inputPath, templateIndex, templateName, useLatestTemplate, outPath = null } = options;
1112
- const applyUpdate = args.includes("--apply");
1113
- const checkUpdate = args.includes("--check");
1114
- const planUpdate = args.includes("--plan");
1115
- const statusUpdate = args.includes("--status");
1116
- const recommendUpdate = args.includes("--recommend");
1117
- const acceptCurrentIndex = args.indexOf("--accept-current");
1118
- const acceptCandidateIndex = args.indexOf("--accept-candidate");
1119
- const deleteCurrentIndex = args.indexOf("--delete-current");
1120
- const acceptCurrentUpdate = acceptCurrentIndex >= 0;
1121
- const acceptCandidateUpdate = acceptCandidateIndex >= 0;
1122
- const deleteCurrentUpdate = deleteCurrentIndex >= 0;
1123
- const fileAction = acceptCurrentUpdate ? "accept-current" : acceptCandidateUpdate ? "accept-candidate" : deleteCurrentUpdate ? "delete-current" : null;
1124
- const fileActionIndex = acceptCurrentUpdate ? acceptCurrentIndex : acceptCandidateUpdate ? acceptCandidateIndex : deleteCurrentUpdate ? deleteCurrentIndex : -1;
1125
- const fileActionPath = fileActionIndex >= 0 ? args[fileActionIndex + 1] : null;
1126
- const updateModeCount = [applyUpdate, checkUpdate, planUpdate, statusUpdate, recommendUpdate, acceptCurrentUpdate, acceptCandidateUpdate, deleteCurrentUpdate].filter(Boolean).length;
1127
- if (updateModeCount > 1) {
1128
- throw new Error("Choose one template update mode or file adoption action.");
1129
- }
1130
- if (updateModeCount === 0) {
1131
- throw new Error("Template update requires `--status`, `--recommend`, `--plan`, `--check`, `--apply`, `--accept-current <file>`, `--accept-candidate <file>`, or `--delete-current <file>`.");
1132
- }
1133
- if (fileAction && (!fileActionPath || fileActionPath.startsWith("-"))) {
1134
- throw new Error(`Template update ${fileAction} requires a relative file path.`);
1135
- }
1136
- const projectConfigInfo = loadProjectConfig(inputPath);
1137
- if (!projectConfigInfo) {
1138
- throw new Error("Cannot update template without topogram.project.json.");
1139
- }
1140
- if (!projectConfigInfo.config.template?.id && !projectConfigInfo.config.template?.sourceSpec) {
1141
- throw new Error("Cannot update template because this project is detached from template metadata.");
1142
- }
1143
- const requestedTemplateName = templateIndex >= 0
1144
- ? templateName
1145
- : useLatestTemplate
1146
- ? latestTemplateInfo(templateMetadataFromProjectConfig(projectConfigInfo.config)).candidateSpec
1147
- : null;
1148
- if (useLatestTemplate && !requestedTemplateName) {
1149
- throw new Error("Cannot use --latest because the current template is not package-backed.");
1150
- }
1151
- let update;
1152
- try {
1153
- const updateOptions = {
1154
- projectRoot: projectConfigInfo.configDir,
1155
- projectConfig: projectConfigInfo.config,
1156
- templateName: requestedTemplateName,
1157
- templatesRoot: TEMPLATES_ROOT
1158
- };
1159
- update = fileAction
1160
- ? applyTemplateUpdateFileAction({ ...updateOptions, action: fileAction, filePath: fileActionPath || "" })
1161
- : recommendUpdate
1162
- ? buildTemplateUpdateRecommendationPayload(buildTemplateUpdateStatus(updateOptions))
1163
- : (applyUpdate ? applyTemplateUpdate : checkUpdate ? buildTemplateUpdateCheck : statusUpdate ? buildTemplateUpdateStatus : buildTemplateUpdatePlan)(updateOptions);
1164
- } catch (error) {
1165
- const message = messageFromError(error);
1166
- update = {
1167
- ok: false,
1168
- mode: fileAction || (applyUpdate ? "apply" : checkUpdate ? "check" : statusUpdate ? "status" : recommendUpdate ? "recommend" : "plan"),
1169
- writes: false,
1170
- current: {
1171
- id: typeof projectConfigInfo.config.template?.id === "string" ? projectConfigInfo.config.template.id : null,
1172
- version: typeof projectConfigInfo.config.template?.version === "string" ? projectConfigInfo.config.template.version : null
1173
- },
1174
- candidate: null,
1175
- compatible: false,
1176
- issues: [message],
1177
- diagnostics: [templateCheckDiagnostic({
1178
- code: "template_resolve_failed",
1179
- message,
1180
- path: templateIndex >= 0 && typeof templateName === "string" && path.isAbsolute(templateName) ? templateName : null,
1181
- suggestedFix: "Check the template path or package spec, and verify private registry authentication if this is a package template.",
1182
- step: "resolve-candidate"
1183
- })],
1184
- summary: { added: 0, changed: 0, currentOnly: 0, unchanged: 0 },
1185
- files: [],
1186
- applied: [],
1187
- skipped: [],
1188
- conflicts: [],
1189
- recommendations: recommendUpdate ? [{
1190
- action: "resolve-errors",
1191
- command: "topogram template update --status",
1192
- reason: "Resolve the candidate template before choosing an update action.",
1193
- path: null
1194
- }] : undefined
1195
- };
1196
- }
1197
- if (outPath) {
1198
- const reportPath = path.resolve(outPath);
1199
- fs.mkdirSync(path.dirname(reportPath), { recursive: true });
1200
- fs.writeFileSync(reportPath, `${stableStringify(update)}\n`, "utf8");
1201
- update.reportPath = reportPath;
1202
- }
1203
- return update;
1204
- }
1205
-
1206
- /**
1207
- * @typedef {Object} TemplateCheckDiagnostic
1208
- * @property {string} code
1209
- * @property {"error"|"warning"} severity
1210
- * @property {string} message
1211
- * @property {string|null} path
1212
- * @property {string|null} suggestedFix
1213
- * @property {string|null} step
1214
- */
1215
-
1216
- /**
1217
- * @param {Record<string, any>} input
1218
- * @returns {TemplateCheckDiagnostic}
1219
- */
1220
- export function templateCheckDiagnostic(input) {
1221
- return {
1222
- code: String(input.code || "template_check_failed"),
1223
- severity: input.severity === "warning" ? "warning" : "error",
1224
- message: String(input.message || "Template check failed."),
1225
- path: typeof input.path === "string" ? input.path : null,
1226
- suggestedFix: typeof input.suggestedFix === "string" ? input.suggestedFix : null,
1227
- step: typeof input.step === "string" ? input.step : null
1228
- };
1229
- }
1230
-
1231
- /**
1232
- * @param {string} templateSpec
1233
- * @param {string} relativePath
1234
- * @returns {string|null}
1235
- */
1236
- function localTemplatePath(templateSpec, relativePath) {
1237
- if (
1238
- templateSpec === "." ||
1239
- templateSpec.startsWith("./") ||
1240
- templateSpec.startsWith("../") ||
1241
- path.isAbsolute(templateSpec)
1242
- ) {
1243
- return path.join(path.resolve(templateSpec), relativePath);
1244
- }
1245
- return null;
1246
- }
1247
-
1248
- /**
1249
- * @param {string} message
1250
- * @param {string} templateSpec
1251
- * @param {string} step
1252
- * @returns {TemplateCheckDiagnostic}
1253
- */
1254
- function diagnosticForTemplateCreateFailure(message, templateSpec, step) {
1255
- if (message.includes("is missing topogram-template.json")) {
1256
- return templateCheckDiagnostic({
1257
- code: "template_manifest_missing",
1258
- message,
1259
- path: localTemplatePath(templateSpec, "topogram-template.json"),
1260
- suggestedFix: "Add topogram-template.json with id, version, kind, and topogramVersion.",
1261
- step
1262
- });
1263
- }
1264
- if (message.includes("contains implementation/") && message.includes("includesExecutableImplementation: true")) {
1265
- return templateCheckDiagnostic({
1266
- code: "template_implementation_undeclared",
1267
- message,
1268
- path: localTemplatePath(templateSpec, "topogram-template.json"),
1269
- suggestedFix: "Set includesExecutableImplementation to true after reviewing implementation/, or remove implementation/.",
1270
- step
1271
- });
1272
- }
1273
- if (message.includes("is missing required string field") || message.includes("topogram-template.json")) {
1274
- return templateCheckDiagnostic({
1275
- code: "template_manifest_invalid",
1276
- message,
1277
- path: localTemplatePath(templateSpec, "topogram-template.json"),
1278
- suggestedFix: "Fix topogram-template.json so it matches the template manifest schema.",
1279
- step
1280
- });
1281
- }
1282
- if (message.includes("is missing topogram/")) {
1283
- return templateCheckDiagnostic({
1284
- code: "template_topogram_missing",
1285
- message,
1286
- path: localTemplatePath(templateSpec, "topogram"),
1287
- suggestedFix: "Add a topogram/ directory with the reusable Topogram source files.",
1288
- step
1289
- });
1290
- }
1291
- if (message.includes("is missing topogram.project.json")) {
1292
- return templateCheckDiagnostic({
1293
- code: "template_project_config_missing",
1294
- message,
1295
- path: localTemplatePath(templateSpec, "topogram.project.json"),
1296
- suggestedFix: "Add topogram.project.json beside topogram/ with outputs and topology.runtimes.",
1297
- step
1298
- });
1299
- }
1300
- if (message.includes("is missing implementation/")) {
1301
- return templateCheckDiagnostic({
1302
- code: "template_implementation_missing",
1303
- message,
1304
- path: localTemplatePath(templateSpec, "implementation"),
1305
- suggestedFix: "Add implementation/ or set includesExecutableImplementation to false.",
1306
- step
1307
- });
1308
- }
1309
- if (message.includes("unsupported symlink")) {
1310
- return templateCheckDiagnostic({
1311
- code: "template_symlink_unsupported",
1312
- message,
1313
- path: path.isAbsolute(templateSpec) ? templateSpec : null,
1314
- suggestedFix: "Replace template symlinks with real files or directories, then rerun `topogram new` or `topogram template check`.",
1315
- step
1316
- });
1317
- }
1318
- return templateCheckDiagnostic({
1319
- code: "template_create_failed",
1320
- message,
1321
- path: path.isAbsolute(templateSpec) ? templateSpec : null,
1322
- suggestedFix: "Fix the template pack so topogram new can create a starter from it.",
1323
- step
1324
- });
1325
- }
1326
-
1327
- /**
1328
- * @param {{ message: string, loc?: any }} error
1329
- * @param {string} step
1330
- * @param {string|null} configPath
1331
- * @returns {TemplateCheckDiagnostic}
1332
- */
1333
- function diagnosticForStarterCheckFailure(error, step, configPath) {
1334
- const locFile = typeof error?.loc?.file === "string" ? error.loc.file : null;
1335
- const isTrust = error.message.includes(TEMPLATE_TRUST_FILE) ||
1336
- error.message.includes("unsupported symlink") ||
1337
- error.message.includes("must be under implementation/");
1338
- return templateCheckDiagnostic({
1339
- code: isTrust ? "template_trust_invalid" : "starter_check_failed",
1340
- message: error.message,
1341
- path: locFile || configPath,
1342
- suggestedFix: isTrust
1343
- ? templateTrustRecoveryGuidance(error.message)
1344
- : "Fix the generated Topogram source or topogram.project.json so topogram check passes.",
1345
- step
1346
- });
1347
- }
1348
-
1349
- /**
1350
- * @param {string} name
1351
- * @param {boolean} ok
1352
- * @param {Record<string, any>} [details]
1353
- * @param {TemplateCheckDiagnostic[]} [diagnostics]
1354
- * @returns {{ name: string, ok: boolean, details: Record<string, any>, diagnostics: TemplateCheckDiagnostic[] }}
1355
- */
1356
- function templateCheckStep(name, ok, details = {}, diagnostics = []) {
1357
- return { name, ok, details, diagnostics };
1358
- }
1359
-
1360
- /**
1361
- * @param {string} projectRoot
1362
- * @returns {string[]}
1363
- */
1364
- function templateCheckGeneratorDependencies(projectRoot) {
1365
- const packagePath = path.join(projectRoot, "package.json");
1366
- if (!fs.existsSync(packagePath)) {
1367
- return [];
1368
- }
1369
- const pkg = JSON.parse(fs.readFileSync(packagePath, "utf8"));
1370
- const dependencies = {
1371
- ...(pkg.dependencies || {}),
1372
- ...(pkg.devDependencies || {})
1373
- };
1374
- return Object.keys(dependencies).filter((name) =>
1375
- name.includes("topogram-generator") || name.startsWith("@topogram/generator-")
1376
- ).sort();
1377
- }
1378
-
1379
- /**
1380
- * @param {string} projectRoot
1381
- * @param {string[]} dependencies
1382
- * @returns {TemplateCheckDiagnostic|null}
1383
- */
1384
- function installTemplateCheckGeneratorDependencies(projectRoot, dependencies) {
1385
- if (dependencies.length === 0) {
1386
- return null;
1387
- }
1388
- const result = runNpmForPackageUpdate(["install", "--ignore-scripts"], projectRoot);
1389
- if (result.status === 0) {
1390
- return null;
1391
- }
1392
- const output = `${result.stdout || ""}\n${result.stderr || ""}`.trim();
1393
- return templateCheckDiagnostic({
1394
- code: "template_generator_dependencies_install_failed",
1395
- message: `Failed to install package-backed generator dependencies: ${dependencies.join(", ")}.`,
1396
- path: path.join(projectRoot, "package.json"),
1397
- suggestedFix: `Run npm install before checking this package-backed generator template.${output ? ` ${output.split(/\r?\n/).slice(-3).join(" ")}` : ""}`,
1398
- step: "generator-dependencies"
1399
- });
1400
- }
1401
-
1402
- /**
1403
- * @param {string} templateSpec
1404
- * @returns {{ ok: boolean, templateSpec: string, projectRoot: string|null, steps: Array<{ name: string, ok: boolean, details: Record<string, any>, diagnostics: TemplateCheckDiagnostic[] }>, diagnostics: TemplateCheckDiagnostic[], errors: string[] }}
1405
- */
1406
- export function buildTemplateCheckPayload(templateSpec) {
1407
- if (!templateSpec) {
1408
- throw new Error("topogram template check requires <template-spec-or-path>.");
1409
- }
1410
- const runRoot = fs.mkdtempSync(path.join(os.tmpdir(), "topogram-template-check-"));
1411
- const projectRoot = path.join(runRoot, "starter");
1412
- /** @type {Array<{ name: string, ok: boolean, details: Record<string, any>, diagnostics: TemplateCheckDiagnostic[] }>} */
1413
- const steps = [];
1414
- /** @type {TemplateCheckDiagnostic[]} */
1415
- const diagnostics = [];
1416
- try {
1417
- const callerPolicyInfo = loadTemplatePolicy(process.cwd());
1418
- if (callerPolicyInfo.exists) {
1419
- const resolvedTemplate = resolveTemplate(templateSpec, TEMPLATES_ROOT);
1420
- const policyDiagnostics = templatePolicyDiagnosticsForTemplate(callerPolicyInfo, resolvedTemplate, "template-check-policy");
1421
- if (policyDiagnostics.some((diagnostic) => diagnostic.severity === "error")) {
1422
- const stepDiagnostics = policyDiagnostics.map((diagnostic) => templateCheckDiagnostic(diagnostic));
1423
- diagnostics.push(...stepDiagnostics);
1424
- steps.push(templateCheckStep("template-policy", false, {
1425
- path: callerPolicyInfo.path
1426
- }, stepDiagnostics));
1427
- return {
1428
- ok: false,
1429
- templateSpec,
1430
- projectRoot: null,
1431
- steps,
1432
- diagnostics,
1433
- errors: diagnostics.map((diagnostic) => diagnostic.message)
1434
- };
1435
- }
1436
- }
1437
- const created = createNewProject({
1438
- targetPath: projectRoot,
1439
- templateName: templateSpec,
1440
- engineRoot: ENGINE_ROOT,
1441
- templatesRoot: TEMPLATES_ROOT
1442
- });
1443
- steps.push(templateCheckStep("create-starter", true, {
1444
- template: created.templateName,
1445
- warnings: created.warnings.length
1446
- }));
1447
- const generatorDependencies = templateCheckGeneratorDependencies(projectRoot);
1448
- const installDiagnostic = installTemplateCheckGeneratorDependencies(projectRoot, generatorDependencies);
1449
- if (installDiagnostic) {
1450
- diagnostics.push(installDiagnostic);
1451
- steps.push(templateCheckStep("generator-dependencies", false, {
1452
- dependencies: generatorDependencies
1453
- }, [installDiagnostic]));
1454
- return {
1455
- ok: false,
1456
- templateSpec,
1457
- projectRoot,
1458
- steps,
1459
- diagnostics,
1460
- errors: diagnostics.map((diagnostic) => diagnostic.message)
1461
- };
1462
- }
1463
- if (generatorDependencies.length > 0) {
1464
- steps.push(templateCheckStep("generator-dependencies", true, {
1465
- dependencies: generatorDependencies
1466
- }));
1467
- }
1468
- } catch (error) {
1469
- const stepDiagnostics = [
1470
- diagnosticForTemplateCreateFailure(messageFromError(error), templateSpec, "create-starter")
1471
- ];
1472
- diagnostics.push(...stepDiagnostics);
1473
- steps.push(templateCheckStep("create-starter", false, {}, stepDiagnostics));
1474
- return {
1475
- ok: false,
1476
- templateSpec,
1477
- projectRoot: null,
1478
- steps,
1479
- diagnostics,
1480
- errors: diagnostics.map((diagnostic) => diagnostic.message)
1481
- };
1482
- }
1483
-
1484
- const projectConfigInfo = loadProjectConfig(projectRoot);
1485
- if (!projectConfigInfo) {
1486
- const stepDiagnostics = [
1487
- templateCheckDiagnostic({
1488
- code: "starter_project_config_missing",
1489
- message: "Generated starter is missing topogram.project.json.",
1490
- path: path.join(projectRoot, "topogram.project.json"),
1491
- suggestedFix: "Ensure the template includes topogram.project.json at its root.",
1492
- step: "project-config"
1493
- })
1494
- ];
1495
- diagnostics.push(...stepDiagnostics);
1496
- steps.push(templateCheckStep("project-config", false, {}, stepDiagnostics));
1497
- return {
1498
- ok: false,
1499
- templateSpec,
1500
- projectRoot,
1501
- steps,
1502
- diagnostics,
1503
- errors: diagnostics.map((diagnostic) => diagnostic.message)
1504
- };
1505
- }
1506
- steps.push(templateCheckStep("project-config", true, {
1507
- path: projectConfigInfo.configPath,
1508
- template: projectConfigInfo.config.template?.id || null
1509
- }));
1510
-
1511
- const ast = parsePath(path.join(projectRoot, "topogram"));
1512
- const resolved = resolveWorkspace(ast);
1513
- const projectValidation = combineProjectValidationResults(
1514
- validateProjectConfig(projectConfigInfo.config, resolved.ok ? resolved.graph : null, { configDir: projectConfigInfo.configDir }),
1515
- validateProjectOutputOwnership(projectConfigInfo),
1516
- validateProjectImplementationTrust(projectConfigInfo)
1517
- );
1518
- const starterCheckOk = resolved.ok && projectValidation.ok;
1519
- const starterDiagnostics = [
1520
- ...(resolved.ok ? [] : resolved.validation.errors),
1521
- ...projectValidation.errors
1522
- ].map((error) => diagnosticForStarterCheckFailure(error, "starter-check", projectConfigInfo.configPath));
1523
- steps.push(templateCheckStep("starter-check", starterCheckOk, {
1524
- files: ast.files.length,
1525
- statements: ast.files.flatMap((/** @type {{ statements: any[] }} */ file) => file.statements).length
1526
- }, starterDiagnostics));
1527
- if (!starterCheckOk) {
1528
- diagnostics.push(...starterDiagnostics);
1529
- }
1530
-
1531
- const implementationInfo = projectConfigInfo.config.implementation
1532
- ? {
1533
- config: projectConfigInfo.config.implementation,
1534
- configPath: projectConfigInfo.configPath,
1535
- configDir: projectConfigInfo.configDir
1536
- }
1537
- : null;
1538
- if (implementationInfo && implementationRequiresTrust(implementationInfo, projectConfigInfo.config)) {
1539
- const trustStatus = getTemplateTrustStatus(implementationInfo, projectConfigInfo.config);
1540
- const trustDiagnostics = trustStatus.issues.map((issue) => templateCheckDiagnostic({
1541
- code: "template_trust_invalid",
1542
- message: issue,
1543
- path: trustStatus.trustPath,
1544
- suggestedFix: templateTrustRecoveryGuidance(issue),
1545
- step: "executable-implementation-trust"
1546
- }));
1547
- steps.push(templateCheckStep("executable-implementation-trust", trustStatus.ok, {
1548
- requiresTrust: true,
1549
- trustPath: trustStatus.trustPath,
1550
- trustedFiles: trustStatus.trustRecord?.content?.files?.length || 0
1551
- }, trustDiagnostics));
1552
- if (!trustStatus.ok) {
1553
- diagnostics.push(...trustDiagnostics);
1554
- }
1555
- } else {
1556
- steps.push(templateCheckStep("executable-implementation-trust", true, {
1557
- requiresTrust: false
1558
- }));
1559
- }
1560
-
1561
- try {
1562
- const updatePlan = buildTemplateUpdatePlan({
1563
- projectRoot,
1564
- projectConfig: projectConfigInfo.config,
1565
- templateName: null,
1566
- templatesRoot: TEMPLATES_ROOT
1567
- });
1568
- steps.push(templateCheckStep("template-update-plan", updatePlan.ok, {
1569
- writes: updatePlan.writes,
1570
- added: updatePlan.summary.added,
1571
- changed: updatePlan.summary.changed,
1572
- currentOnly: updatePlan.summary.currentOnly
1573
- }));
1574
- if (!updatePlan.ok) {
1575
- const stepDiagnostics = updatePlan.issues.map((issue) => templateCheckDiagnostic({
1576
- code: "template_update_plan_failed",
1577
- message: issue,
1578
- path: projectConfigInfo.configPath,
1579
- suggestedFix: "Fix template metadata so a no-write update plan can be produced.",
1580
- step: "template-update-plan"
1581
- }));
1582
- steps[steps.length - 1].diagnostics.push(...stepDiagnostics);
1583
- diagnostics.push(...stepDiagnostics);
1584
- }
1585
- } catch (error) {
1586
- const stepDiagnostics = [
1587
- templateCheckDiagnostic({
1588
- code: "template_update_plan_failed",
1589
- message: messageFromError(error),
1590
- path: projectConfigInfo.configPath,
1591
- suggestedFix: "Fix template metadata so a no-write update plan can be produced.",
1592
- step: "template-update-plan"
1593
- })
1594
- ];
1595
- diagnostics.push(...stepDiagnostics);
1596
- steps.push(templateCheckStep("template-update-plan", false, {}, stepDiagnostics));
1597
- }
1598
-
1599
- return {
1600
- ok: steps.every((step) => step.ok),
1601
- templateSpec,
1602
- projectRoot,
1603
- steps,
1604
- diagnostics,
1605
- errors: diagnostics.map((diagnostic) => diagnostic.message)
1606
- };
1607
- }
1608
-
1609
- /**
1610
- * @param {ReturnType<typeof loadProjectConfig>} projectConfigInfo
1611
- * @returns {{ requested: string, root: string, manifest: { id: string, version: string, kind: string, topogramVersion: string, includesExecutableImplementation: boolean }, source: "local"|"package", packageSpec: string|null }}
1612
- */
1613
- function currentPolicyTemplate(projectConfigInfo) {
1614
- const template = projectConfigInfo?.config.template || {};
1615
- const source = template.source === "local" || template.source === "package"
1616
- ? template.source
1617
- : "local";
1618
- return {
1619
- requested: typeof template.requested === "string" ? template.requested : String(template.id || "unknown"),
1620
- root: projectConfigInfo?.configDir || process.cwd(),
1621
- manifest: {
1622
- id: typeof template.id === "string" ? template.id : "unknown",
1623
- version: typeof template.version === "string" ? template.version : "unknown",
1624
- kind: "starter",
1625
- topogramVersion: "*",
1626
- includesExecutableImplementation: Boolean(template.includesExecutableImplementation)
1627
- },
1628
- source,
1629
- packageSpec: typeof template.sourceSpec === "string" ? template.sourceSpec : null
1630
- };
1631
- }
1632
-
1633
- /**
1634
- * @param {string} projectPath
1635
- * @returns {{ ok: boolean, path: string, exists: boolean, policy: any, diagnostics: TemplateCheckDiagnostic[], errors: string[] }}
1636
- */
1637
- export function buildTemplatePolicyCheckPayload(projectPath) {
1638
- const projectConfigInfo = loadProjectConfig(projectPath);
1639
- if (!projectConfigInfo) {
1640
- const diagnostic = templateCheckDiagnostic({
1641
- code: "template_policy_project_missing",
1642
- message: "Cannot check template policy without topogram.project.json.",
1643
- path: path.resolve(projectPath),
1644
- suggestedFix: "Run this command in a Topogram project.",
1645
- step: "policy"
1646
- });
1647
- return {
1648
- ok: false,
1649
- path: path.join(path.resolve(projectPath), "topogram.template-policy.json"),
1650
- exists: false,
1651
- policy: null,
1652
- diagnostics: [diagnostic],
1653
- errors: [diagnostic.message]
1654
- };
1655
- }
1656
- const policyInfo = loadTemplatePolicy(projectConfigInfo.configDir);
1657
- /** @type {TemplateCheckDiagnostic[]} */
1658
- const diagnostics = policyInfo.diagnostics.map((diagnostic) => templateCheckDiagnostic(diagnostic));
1659
- if (!policyInfo.exists) {
1660
- diagnostics.push(templateCheckDiagnostic({
1661
- code: "template_policy_missing",
1662
- severity: "warning",
1663
- message: "No topogram.template-policy.json found. Template operations are permissive until a policy is defined.",
1664
- path: policyInfo.path,
1665
- suggestedFix: "Run `topogram template policy init` to create a project template policy.",
1666
- step: "policy"
1667
- }));
1668
- } else if (policyInfo.policy) {
1669
- const currentTemplate = currentPolicyTemplate(projectConfigInfo);
1670
- diagnostics.push(...templatePolicyDiagnosticsForTemplate(policyInfo, currentTemplate, "policy")
1671
- .map((diagnostic) => templateCheckDiagnostic(diagnostic)));
1672
- }
1673
- const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error").map((diagnostic) => diagnostic.message);
1674
- return {
1675
- ok: errors.length === 0,
1676
- path: policyInfo.path,
1677
- exists: policyInfo.exists,
1678
- policy: policyInfo.policy,
1679
- diagnostics,
1680
- errors
1681
- };
1682
- }
1683
-
1684
- /**
1685
- * @param {string} name
1686
- * @param {boolean} ok
1687
- * @param {string} actual
1688
- * @param {string} expected
1689
- * @param {string} message
1690
- * @param {string|null} fix
1691
- * @returns {{ name: string, ok: boolean, actual: string, expected: string, message: string, fix: string|null }}
1692
- */
1693
- function templatePolicyRule(name, ok, actual, expected, message, fix = null) {
1694
- return { name, ok, actual, expected, message, fix };
1695
- }
1696
-
1697
- /**
1698
- * @param {string} name
1699
- * @returns {string}
1700
- */
1701
- function templatePolicyRuleLabel(name) {
1702
- return ({
1703
- "policy-file": "Policy file",
1704
- "allowed-source": "Allowed source",
1705
- "allowed-template-id": "Allowed template id",
1706
- "allowed-package-scope": "Allowed package scope",
1707
- "pinned-version": "Pinned version",
1708
- "executable-implementation": "Executable implementation"
1709
- })[name] || name;
1710
- }
1711
-
1712
- /**
1713
- * @param {string} projectPath
1714
- * @returns {{ ok: boolean, path: string, exists: boolean, policy: any, template: any, catalog: any, package: any, rules: Array<{ name: string, ok: boolean, actual: string, expected: string, message: string, fix: string|null }>, diagnostics: TemplateCheckDiagnostic[], errors: string[] }}
1715
- */
1716
- export function buildTemplatePolicyExplainPayload(projectPath) {
1717
- const check = buildTemplatePolicyCheckPayload(projectPath);
1718
- const projectConfigInfo = loadProjectConfig(projectPath);
1719
- if (!projectConfigInfo) {
1720
- return {
1721
- ...check,
1722
- template: null,
1723
- catalog: null,
1724
- package: null,
1725
- rules: []
1726
- };
1727
- }
1728
- const templateMetadata = projectConfigInfo.config.template || {};
1729
- const currentTemplate = currentPolicyTemplate(projectConfigInfo);
1730
- const policy = check.policy;
1731
- const packageScope = currentTemplate.source === "package"
1732
- ? packageScopeFromSpec(currentTemplate.packageSpec || currentTemplate.requested)
1733
- : null;
1734
- const rules = [];
1735
- rules.push(templatePolicyRule(
1736
- "policy-file",
1737
- check.exists,
1738
- check.exists ? "present" : "missing",
1739
- "present",
1740
- check.exists
1741
- ? "Project has a template policy file."
1742
- : "Project has no template policy file; template operations are permissive until one is defined.",
1743
- check.exists ? null : "Run `topogram template policy init`."
1744
- ));
1745
- if (policy) {
1746
- rules.push(templatePolicyRule(
1747
- "allowed-source",
1748
- policy.allowedSources.length === 0 || policy.allowedSources.includes(currentTemplate.source),
1749
- currentTemplate.source,
1750
- policy.allowedSources.length > 0 ? policy.allowedSources.join(", ") : "(any)",
1751
- "Current template source must be allowed by allowedSources.",
1752
- `Add '${currentTemplate.source}' to allowedSources after review, or run \`topogram template policy init\`.`
1753
- ));
1754
- rules.push(templatePolicyRule(
1755
- "allowed-template-id",
1756
- policy.allowedTemplateIds.length === 0 || policy.allowedTemplateIds.includes(currentTemplate.manifest.id),
1757
- currentTemplate.manifest.id,
1758
- policy.allowedTemplateIds.length > 0 ? policy.allowedTemplateIds.join(", ") : "(any)",
1759
- "Current template id must be allowed by allowedTemplateIds.",
1760
- `Run \`topogram template policy pin ${currentTemplate.manifest.id}@${currentTemplate.manifest.version}\` after review.`
1761
- ));
1762
- if (currentTemplate.source === "package") {
1763
- rules.push(templatePolicyRule(
1764
- "allowed-package-scope",
1765
- !policy.allowedPackageScopes ||
1766
- policy.allowedPackageScopes.length === 0 ||
1767
- Boolean(packageScope && policy.allowedPackageScopes.includes(packageScope)),
1768
- packageScope || "(unscoped)",
1769
- policy.allowedPackageScopes && policy.allowedPackageScopes.length > 0 ? policy.allowedPackageScopes.join(", ") : "(any)",
1770
- "Package-backed template source must be in an allowed package scope.",
1771
- `Add '${packageScope || "(unscoped)"}' to allowedPackageScopes after review.`
1772
- ));
1773
- }
1774
- const pinnedVersion = policy.pinnedVersions?.[currentTemplate.manifest.id] || null;
1775
- rules.push(templatePolicyRule(
1776
- "pinned-version",
1777
- !pinnedVersion || pinnedVersion === currentTemplate.manifest.version,
1778
- currentTemplate.manifest.version,
1779
- pinnedVersion || "(unpinned)",
1780
- "Pinned version must match the current template version when a pin exists.",
1781
- `Run \`topogram template policy pin ${currentTemplate.manifest.id}@${currentTemplate.manifest.version}\` after review.`
1782
- ));
1783
- rules.push(templatePolicyRule(
1784
- "executable-implementation",
1785
- !currentTemplate.manifest.includesExecutableImplementation || policy.executableImplementation !== "deny",
1786
- currentTemplate.manifest.includesExecutableImplementation ? "yes" : "no",
1787
- policy.executableImplementation,
1788
- "Executable template implementation must be allowed when implementation/ is present.",
1789
- "Review implementation/, then set executableImplementation to 'allow' or choose a non-executable template."
1790
- ));
1791
- }
1792
- return {
1793
- ...check,
1794
- template: {
1795
- id: currentTemplate.manifest.id,
1796
- version: currentTemplate.manifest.version,
1797
- source: currentTemplate.source,
1798
- requested: currentTemplate.requested,
1799
- sourceSpec: currentTemplate.packageSpec,
1800
- includesExecutableImplementation: currentTemplate.manifest.includesExecutableImplementation
1801
- },
1802
- catalog: templateMetadata.catalog || null,
1803
- package: currentTemplate.source === "package" ? {
1804
- spec: currentTemplate.packageSpec,
1805
- scope: packageScope
1806
- } : null,
1807
- rules
1808
- };
1809
- }
1810
-
1811
- /**
1812
- * @param {ReturnType<typeof buildTemplatePolicyExplainPayload>} payload
1813
- * @returns {void}
1814
- */
1815
- export function printTemplatePolicyExplainPayload(payload) {
1816
- console.log(payload.ok ? "Template policy: allowed" : "Template policy: denied");
1817
- console.log(payload.ok
1818
- ? "Decision: the current template is allowed by this project's template policy."
1819
- : "Decision: the current template is blocked by this project's template policy.");
1820
- console.log(`Policy file: ${payload.path}`);
1821
- console.log(`Policy file exists: ${payload.exists ? "yes" : "no"}`);
1822
- if (payload.template) {
1823
- console.log(`Template: ${payload.template.id}@${payload.template.version}`);
1824
- console.log(`Source: ${payload.template.source}`);
1825
- console.log(`Requested: ${payload.template.requested}`);
1826
- if (payload.template.sourceSpec) {
1827
- console.log(`Source spec: ${payload.template.sourceSpec}`);
1828
- }
1829
- console.log(`Executable implementation: ${payload.template.includesExecutableImplementation ? "yes" : "no"}`);
1830
- }
1831
- if (payload.catalog?.id) {
1832
- console.log(`Catalog: ${payload.catalog.id} from ${payload.catalog.source || "unknown"}`);
1833
- console.log(`Catalog package: ${payload.catalog.packageSpec || payload.catalog.package || "unknown"}`);
1834
- }
1835
- if (payload.package) {
1836
- console.log(`Package scope: ${payload.package.scope || "(unscoped)"}`);
1837
- }
1838
- if (payload.rules.length > 0) {
1839
- console.log("");
1840
- console.log("Policy checks:");
1841
- }
1842
- for (const rule of payload.rules) {
1843
- console.log(`${rule.ok ? "PASS" : "FAIL"} ${templatePolicyRuleLabel(rule.name)}: ${rule.message}`);
1844
- console.log(` actual: ${rule.actual}`);
1845
- console.log(` expected: ${rule.expected}`);
1846
- if (!rule.ok && rule.fix) {
1847
- console.log(` fix: ${rule.fix}`);
1848
- }
1849
- }
1850
- for (const diagnostic of payload.diagnostics) {
1851
- const label = diagnostic.severity === "warning" ? "Warning" : "Error";
1852
- console.log(`${label}: ${diagnostic.code}: ${diagnostic.message}`);
1853
- if (diagnostic.suggestedFix) {
1854
- console.log(` fix: ${diagnostic.suggestedFix}`);
1855
- }
1856
- }
1857
- }
1858
-
1859
- /**
1860
- * @param {{ ok: boolean, path: string, exists: boolean, policy: any, diagnostics: TemplateCheckDiagnostic[] }} payload
1861
- * @returns {void}
1862
- */
1863
- export function printTemplatePolicyCheckPayload(payload) {
1864
- console.log(payload.ok ? "Template policy check passed" : "Template policy check failed");
1865
- console.log(`Policy: ${payload.path}`);
1866
- console.log(`Exists: ${payload.exists ? "yes" : "no"}`);
1867
- for (const diagnostic of payload.diagnostics) {
1868
- console.log(`[${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`);
1869
- if (diagnostic.path) {
1870
- console.log(` path: ${diagnostic.path}`);
1871
- }
1872
- if (diagnostic.suggestedFix) {
1873
- console.log(` fix: ${diagnostic.suggestedFix}`);
1874
- }
1875
- }
1876
- }
1877
-
1878
- /**
1879
- * @param {string|null|undefined} spec
1880
- * @returns {{ id: string, version: string }|null}
1881
- */
1882
- function parseTemplateVersionPin(spec) {
1883
- if (!spec) {
1884
- return null;
1885
- }
1886
- const separator = spec.lastIndexOf("@");
1887
- if (separator <= 0 || separator === spec.length - 1) {
1888
- throw new Error("Template policy pin requires a template id and version, for example @scope/template@0.2.0.");
1889
- }
1890
- return {
1891
- id: spec.slice(0, separator),
1892
- version: spec.slice(separator + 1)
1893
- };
1894
- }
1895
-
1896
- /**
1897
- * @param {string} projectPath
1898
- * @param {string|null|undefined} spec
1899
- * @returns {{ ok: boolean, path: string, policy: any, pinned: { id: string, version: string }, diagnostics: TemplateCheckDiagnostic[], errors: string[] }}
1900
- */
1901
- export function buildTemplatePolicyPinPayload(projectPath, spec) {
1902
- const projectConfigInfo = loadProjectConfig(projectPath);
1903
- if (!projectConfigInfo) {
1904
- const diagnostic = templateCheckDiagnostic({
1905
- code: "template_policy_project_missing",
1906
- message: "Cannot pin template policy without topogram.project.json.",
1907
- path: path.resolve(projectPath),
1908
- suggestedFix: "Run this command in a Topogram project.",
1909
- step: "policy"
1910
- });
1911
- return {
1912
- ok: false,
1913
- path: path.join(path.resolve(projectPath), "topogram.template-policy.json"),
1914
- policy: null,
1915
- pinned: { id: "", version: "" },
1916
- diagnostics: [diagnostic],
1917
- errors: [diagnostic.message]
1918
- };
1919
- }
1920
- const parsed = parseTemplateVersionPin(spec);
1921
- const currentTemplate = projectConfigInfo.config.template || {};
1922
- const pin = parsed || {
1923
- id: typeof currentTemplate.id === "string" ? currentTemplate.id : "",
1924
- version: typeof currentTemplate.version === "string" ? currentTemplate.version : ""
1925
- };
1926
- if (!pin.id || !pin.version) {
1927
- const diagnostic = templateCheckDiagnostic({
1928
- code: "template_policy_pin_missing_version",
1929
- message: "Cannot pin a template version without a template id and version.",
1930
- path: projectConfigInfo.configPath,
1931
- suggestedFix: "Pass a pin such as @scope/template@0.2.0, or ensure topogram.project.json records template.id and template.version.",
1932
- step: "policy"
1933
- });
1934
- return {
1935
- ok: false,
1936
- path: path.join(projectConfigInfo.configDir, "topogram.template-policy.json"),
1937
- policy: null,
1938
- pinned: pin,
1939
- diagnostics: [diagnostic],
1940
- errors: [diagnostic.message]
1941
- };
1942
- }
1943
-
1944
- const existing = loadTemplatePolicy(projectConfigInfo.configDir);
1945
- const diagnostics = existing.diagnostics.map((diagnostic) => templateCheckDiagnostic(diagnostic));
1946
- if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
1947
- return {
1948
- ok: false,
1949
- path: existing.path,
1950
- policy: existing.policy,
1951
- pinned: pin,
1952
- diagnostics,
1953
- errors: diagnostics.map((diagnostic) => diagnostic.message)
1954
- };
1955
- }
1956
- const policy = existing.policy || writeTemplatePolicyForProject(projectConfigInfo.configDir, projectConfigInfo.config);
1957
- const allowedTemplateIds = policy.allowedTemplateIds.includes(pin.id)
1958
- ? policy.allowedTemplateIds
1959
- : [...policy.allowedTemplateIds, pin.id];
1960
- const allowedPackageScopes = [...(policy.allowedPackageScopes || [])];
1961
- if (pin.id.startsWith("@")) {
1962
- const scope = pin.id.split("/")[0];
1963
- if (scope && !allowedPackageScopes.includes(scope)) {
1964
- allowedPackageScopes.push(scope);
1965
- }
1966
- }
1967
- const nextPolicy = {
1968
- ...policy,
1969
- allowedTemplateIds,
1970
- allowedPackageScopes,
1971
- pinnedVersions: {
1972
- ...(policy.pinnedVersions || {}),
1973
- [pin.id]: pin.version
1974
- }
1975
- };
1976
- writeTemplatePolicy(projectConfigInfo.configDir, nextPolicy);
1977
- return {
1978
- ok: true,
1979
- path: path.join(projectConfigInfo.configDir, "topogram.template-policy.json"),
1980
- policy: nextPolicy,
1981
- pinned: pin,
1982
- diagnostics: [],
1983
- errors: []
1984
- };
1985
- }
1986
-
1987
- /**
1988
- * @param {{ ok: boolean, path: string, pinned: { id: string, version: string }, diagnostics: TemplateCheckDiagnostic[] }} payload
1989
- * @returns {void}
1990
- */
1991
- export function printTemplatePolicyPinPayload(payload) {
1992
- console.log(payload.ok ? "Template policy pin updated" : "Template policy pin failed");
1993
- console.log(`Policy: ${payload.path}`);
1994
- if (payload.pinned.id) {
1995
- console.log(`Pinned: ${payload.pinned.id}@${payload.pinned.version || "unknown"}`);
1996
- }
1997
- for (const diagnostic of payload.diagnostics) {
1998
- console.log(`[${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`);
1999
- if (diagnostic.path) {
2000
- console.log(` path: ${diagnostic.path}`);
2001
- }
2002
- if (diagnostic.suggestedFix) {
2003
- console.log(` fix: ${diagnostic.suggestedFix}`);
2004
- }
2005
- }
2006
- }
2007
-
2008
- /**
2009
- * @param {Record<string, any>} details
2010
- * @returns {string[]}
2011
- */
2012
- function formatTemplateCheckDetails(details) {
2013
- return Object.entries(details)
2014
- .filter(([, value]) => value !== undefined && value !== null)
2015
- .map(([key, value]) => ` ${key}: ${typeof value === "object" ? JSON.stringify(value) : String(value)}`);
2016
- }
2017
-
2018
- /**
2019
- * @param {ReturnType<typeof buildTemplateCheckPayload>} payload
2020
- * @returns {void}
2021
- */
2022
- export function printTemplateCheckPayload(payload) {
2023
- console.log(payload.ok ? "Template check passed" : "Template check failed");
2024
- console.log(`Template spec: ${payload.templateSpec}`);
2025
- if (payload.projectRoot) {
2026
- console.log(`Temp starter: ${payload.projectRoot}`);
2027
- }
2028
- for (const step of payload.steps) {
2029
- console.log(`${step.ok ? "PASS" : "FAIL"} ${step.name}`);
2030
- for (const detail of formatTemplateCheckDetails(step.details)) {
2031
- console.log(detail);
2032
- }
2033
- for (const diagnostic of step.diagnostics) {
2034
- console.log(` [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`);
2035
- if (diagnostic.path) {
2036
- console.log(` path: ${diagnostic.path}`);
2037
- }
2038
- if (diagnostic.suggestedFix) {
2039
- console.log(` fix: ${diagnostic.suggestedFix}`);
2040
- }
2041
- }
2042
- }
2043
- const stepDiagnostics = new Set(payload.steps.flatMap((step) => step.diagnostics));
2044
- for (const diagnostic of payload.diagnostics.filter((item) => !stepDiagnostics.has(item))) {
2045
- console.log(`[${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`);
2046
- if (diagnostic.path) {
2047
- console.log(` path: ${diagnostic.path}`);
2048
- }
2049
- if (diagnostic.suggestedFix) {
2050
- console.log(` fix: ${diagnostic.suggestedFix}`);
2051
- }
2052
- }
2053
- }
2054
-
2055
- /**
2056
- * @param {string} filePath
2057
- * @returns {{ sha256: string, size: number }}
2058
- */
2059
- function projectFileHash(filePath) {
2060
- const bytes = fs.readFileSync(filePath);
2061
- return {
2062
- sha256: crypto.createHash("sha256").update(bytes).digest("hex"),
2063
- size: bytes.length
2064
- };
2065
- }
2066
-
2067
- /**
2068
- * @param {string} projectRoot
2069
- * @param {string} relativePath
2070
- * @returns {{ sha256: string, size: number }}
2071
- */
2072
- function templateBaselineFileHash(projectRoot, relativePath) {
2073
- const filePath = path.join(projectRoot, relativePath);
2074
- if (relativePath === "topogram.project.json") {
2075
- const content = `${stableStringify(JSON.parse(fs.readFileSync(filePath, "utf8")))}\n`;
2076
- return {
2077
- sha256: crypto.createHash("sha256").update(content).digest("hex"),
2078
- size: Buffer.byteLength(content)
2079
- };
2080
- }
2081
- return projectFileHash(filePath);
2082
- }
2083
-
2084
- /**
2085
- * @param {string} projectRoot
2086
- * @returns {{ exists: boolean, path: string, status: "missing"|"clean"|"changed", state: "missing"|"matches-template"|"diverged", meaning: "no-template-baseline"|"matches-template-baseline"|"local-project-owns-changes", changedAllowed: boolean, localOwnership: boolean, blocksCheck: boolean, blocksGenerate: boolean, nextCommand: string|null, content: { changed: string[], added: string[], removed: string[] }, trustedFiles: number }}
2087
- */
2088
- export function buildTemplateOwnedBaselineStatus(projectRoot) {
2089
- const manifestPath = path.join(projectRoot, TEMPLATE_FILES_MANIFEST);
2090
- if (!fs.existsSync(manifestPath)) {
2091
- return {
2092
- exists: false,
2093
- path: manifestPath,
2094
- status: "missing",
2095
- state: "missing",
2096
- meaning: "no-template-baseline",
2097
- changedAllowed: true,
2098
- localOwnership: false,
2099
- blocksCheck: false,
2100
- blocksGenerate: false,
2101
- nextCommand: null,
2102
- content: { changed: [], added: [], removed: [] },
2103
- trustedFiles: 0
2104
- };
2105
- }
2106
- const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
2107
- const trustedFiles = Array.isArray(manifest.files) ? manifest.files : [];
2108
- const changed = [];
2109
- const removed = [];
2110
- for (const file of trustedFiles) {
2111
- const relativePath = String(file.path || "");
2112
- if (!relativePath) {
2113
- continue;
2114
- }
2115
- const absolutePath = path.join(projectRoot, relativePath);
2116
- if (!fs.existsSync(absolutePath)) {
2117
- removed.push(relativePath);
2118
- continue;
2119
- }
2120
- const current = templateBaselineFileHash(projectRoot, relativePath);
2121
- if (current.sha256 !== file.sha256 || current.size !== file.size) {
2122
- changed.push(relativePath);
2123
- }
2124
- }
2125
- const status = changed.length || removed.length ? "changed" : "clean";
2126
- const diverged = status === "changed";
2127
- return {
2128
- exists: true,
2129
- path: manifestPath,
2130
- status,
2131
- state: diverged ? "diverged" : "matches-template",
2132
- meaning: diverged ? "local-project-owns-changes" : "matches-template-baseline",
2133
- changedAllowed: true,
2134
- localOwnership: diverged,
2135
- blocksCheck: false,
2136
- blocksGenerate: false,
2137
- nextCommand: diverged ? "topogram template update --check" : null,
2138
- content: {
2139
- changed: changed.sort((a, b) => a.localeCompare(b)),
2140
- added: [],
2141
- removed: removed.sort((a, b) => a.localeCompare(b))
2142
- },
2143
- trustedFiles: trustedFiles.length
2144
- };
2145
- }
3
+ export { printTemplateHelp } from "./template/help.js";
4
+ export {
5
+ buildTemplateListPayload,
6
+ buildTemplateShowPayload,
7
+ printTemplateList,
8
+ printTemplateShow
9
+ } from "./template/list-show.js";
10
+ export {
11
+ latestTemplateInfo,
12
+ templateMetadataFromProjectConfig
13
+ } from "./template/shared.js";
14
+ export {
15
+ buildTemplateStatusPayload,
16
+ printTemplateStatus,
17
+ buildTemplateExplainPayload,
18
+ printTemplateExplain,
19
+ buildTemplateDetachPayload,
20
+ printTemplateDetachPayload
21
+ } from "./template/lifecycle.js";
22
+ export {
23
+ printTemplateUpdatePlan,
24
+ buildTemplateUpdateRecommendationPayload,
25
+ printTemplateUpdateRecommendation,
26
+ buildTemplateUpdateCliPayload
27
+ } from "./template/updates.js";
28
+ export {
29
+ templateCheckDiagnostic
30
+ } from "./template/diagnostics.js";
31
+ export {
32
+ buildTemplateCheckPayload,
33
+ printTemplateCheckPayload
34
+ } from "./template/check.js";
35
+ export {
36
+ buildTemplatePolicyCheckPayload,
37
+ printTemplatePolicyCheckPayload,
38
+ buildTemplatePolicyExplainPayload,
39
+ printTemplatePolicyExplainPayload,
40
+ buildTemplatePolicyPinPayload,
41
+ printTemplatePolicyPinPayload
42
+ } from "./template/policy.js";
43
+ export { buildTemplateOwnedBaselineStatus } from "./template/baseline.js";