@topogram/cli 0.3.64 → 0.3.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/package.json +1 -1
  2. package/src/adoption/plan/index.js +703 -0
  3. package/src/adoption/plan.js +12 -703
  4. package/src/agent-ops/query-builders/auth.js +375 -0
  5. package/src/agent-ops/query-builders/change-risk/change-plan.js +123 -0
  6. package/src/agent-ops/query-builders/change-risk/import-plan.js +49 -0
  7. package/src/agent-ops/query-builders/change-risk/maintained.js +286 -0
  8. package/src/agent-ops/query-builders/change-risk/review-packets.js +123 -0
  9. package/src/agent-ops/query-builders/change-risk/risk.js +189 -0
  10. package/src/agent-ops/query-builders/change-risk.js +25 -0
  11. package/src/agent-ops/query-builders/common.js +149 -0
  12. package/src/agent-ops/query-builders/maintained-risk.js +539 -0
  13. package/src/agent-ops/query-builders/maintained-shared.js +120 -0
  14. package/src/agent-ops/query-builders/multi-agent.js +547 -0
  15. package/src/agent-ops/query-builders/projection-impacts.js +514 -0
  16. package/src/agent-ops/query-builders/work-packets.js +417 -0
  17. package/src/agent-ops/query-builders/workflow-context-shared.js +300 -0
  18. package/src/agent-ops/query-builders/workflow-context.js +398 -0
  19. package/src/agent-ops/query-builders/workflow-presets-core.js +676 -0
  20. package/src/agent-ops/query-builders/workflow-presets.js +341 -0
  21. package/src/agent-ops/query-builders.d.ts +26 -26
  22. package/src/agent-ops/query-builders.js +42 -5021
  23. package/src/catalog/constants.js +10 -0
  24. package/src/catalog/copy.js +60 -0
  25. package/src/catalog/diagnostics.js +15 -0
  26. package/src/catalog/entries.js +42 -0
  27. package/src/catalog/files.js +67 -0
  28. package/src/catalog/provenance.js +122 -0
  29. package/src/catalog/source.js +150 -0
  30. package/src/catalog/validation.js +252 -0
  31. package/src/catalog.d.ts +2 -0
  32. package/src/catalog.js +18 -746
  33. package/src/cli/commands/catalog/check.js +31 -0
  34. package/src/cli/commands/catalog/copy.js +59 -0
  35. package/src/cli/commands/catalog/doctor.js +248 -0
  36. package/src/cli/commands/catalog/help.js +21 -0
  37. package/src/cli/commands/catalog/list.js +52 -0
  38. package/src/cli/commands/catalog/runner.js +92 -0
  39. package/src/cli/commands/catalog/shared.js +17 -0
  40. package/src/cli/commands/catalog/show.js +134 -0
  41. package/src/cli/commands/catalog.js +30 -615
  42. package/src/cli/commands/generator-policy/package-info.js +162 -0
  43. package/src/cli/commands/generator-policy/payloads.js +372 -0
  44. package/src/cli/commands/generator-policy/printers.js +159 -0
  45. package/src/cli/commands/generator-policy/runner.js +81 -0
  46. package/src/cli/commands/generator-policy/shared.js +39 -0
  47. package/src/cli/commands/generator-policy.js +15 -783
  48. package/src/cli/commands/import/adopt.js +170 -0
  49. package/src/cli/commands/import/check.js +91 -0
  50. package/src/cli/commands/import/diff.js +84 -0
  51. package/src/cli/commands/import/help.js +47 -0
  52. package/src/cli/commands/import/paths.js +277 -0
  53. package/src/cli/commands/import/plan.js +284 -0
  54. package/src/cli/commands/import/refresh.js +470 -0
  55. package/src/cli/commands/import/status-history.js +196 -0
  56. package/src/cli/commands/import/workspace.js +230 -0
  57. package/src/cli/commands/import.js +33 -1732
  58. package/src/cli/commands/package/constants.js +17 -0
  59. package/src/cli/commands/package/doctor.js +240 -0
  60. package/src/cli/commands/package/help.js +27 -0
  61. package/src/cli/commands/package/lockfile.js +135 -0
  62. package/src/cli/commands/package/npm.js +97 -0
  63. package/src/cli/commands/package/reporting.js +35 -0
  64. package/src/cli/commands/package/runner.js +33 -0
  65. package/src/cli/commands/package/shared.js +9 -0
  66. package/src/cli/commands/package/update-cli.js +252 -0
  67. package/src/cli/commands/package/versions.js +35 -0
  68. package/src/cli/commands/package.js +29 -813
  69. package/src/cli/commands/query/change-plan.js +68 -0
  70. package/src/cli/commands/query/definitions.js +202 -0
  71. package/src/cli/commands/query/import-adopt.js +121 -0
  72. package/src/cli/commands/query/runner/artifacts.js +102 -0
  73. package/src/cli/commands/query/runner/boundaries.js +211 -0
  74. package/src/cli/commands/query/runner/change.js +182 -0
  75. package/src/cli/commands/query/runner/import-adopt.js +111 -0
  76. package/src/cli/commands/query/runner/index.js +31 -0
  77. package/src/cli/commands/query/runner/output.js +12 -0
  78. package/src/cli/commands/query/runner/workflow.js +241 -0
  79. package/src/cli/commands/query/runner.js +3 -0
  80. package/src/cli/commands/query/workflow-context.js +5 -0
  81. package/src/cli/commands/query/workspace.js +274 -0
  82. package/src/cli/commands/query.js +9 -1300
  83. package/src/cli/commands/template/baseline.js +100 -0
  84. package/src/cli/commands/template/check.js +466 -0
  85. package/src/cli/commands/template/constants.js +8 -0
  86. package/src/cli/commands/template/diagnostics.js +26 -0
  87. package/src/cli/commands/template/help.js +28 -0
  88. package/src/cli/commands/template/lifecycle.js +404 -0
  89. package/src/cli/commands/template/list-show.js +287 -0
  90. package/src/cli/commands/template/policy.js +422 -0
  91. package/src/cli/commands/template/shared.js +127 -0
  92. package/src/cli/commands/template/updates.js +352 -0
  93. package/src/cli/commands/template.js +41 -2143
  94. package/src/generator/api/contracts.js +497 -0
  95. package/src/generator/api/metadata.js +221 -0
  96. package/src/generator/api/openapi.js +559 -0
  97. package/src/generator/api/schema.js +124 -0
  98. package/src/generator/api/types.d.ts +98 -0
  99. package/src/generator/api.js +3 -1195
  100. package/src/generator/context/shared/domain-sdlc.js +282 -0
  101. package/src/generator/context/shared/maintained-boundary.js +665 -0
  102. package/src/generator/context/shared/metrics.js +85 -0
  103. package/src/generator/context/shared/primitives.js +64 -0
  104. package/src/generator/context/shared/relationships.js +453 -0
  105. package/src/generator/context/shared/summaries.js +263 -0
  106. package/src/generator/context/shared/types.d.ts +207 -0
  107. package/src/generator/context/shared.d.ts +42 -0
  108. package/src/generator/context/shared.js +80 -1390
  109. package/src/generator/context/slice/core.js +397 -0
  110. package/src/generator/context/slice/sdlc.js +417 -0
  111. package/src/generator/context/slice/ui-packets.js +183 -0
  112. package/src/generator/context/slice.js +2 -859
  113. package/src/generator/registry/index.js +507 -0
  114. package/src/generator/registry.js +18 -504
  115. package/src/generator/runtime/environment/index.js +666 -0
  116. package/src/generator/runtime/environment.js +4 -666
  117. package/src/generator/runtime/runtime-check/index.js +554 -0
  118. package/src/generator/runtime/runtime-check.js +4 -554
  119. package/src/generator/runtime/shared/index.js +572 -0
  120. package/src/generator/runtime/shared.js +19 -570
  121. package/src/generator/shared.d.ts +2 -0
  122. package/src/generator/surfaces/shared.d.ts +3 -0
  123. package/src/generator/widget-conformance/behavior-report.js +258 -0
  124. package/src/generator/widget-conformance/checks.js +371 -0
  125. package/src/generator/widget-conformance/projection-context.js +200 -0
  126. package/src/generator/widget-conformance/report.js +166 -0
  127. package/src/generator/widget-conformance/types.d.ts +121 -0
  128. package/src/generator/widget-conformance.js +3 -824
  129. package/src/import/core/context.d.ts +3 -0
  130. package/src/import/core/contracts.d.ts +1 -0
  131. package/src/import/core/registry.d.ts +4 -0
  132. package/src/import/core/runner/candidates.js +217 -0
  133. package/src/import/core/runner/options.js +22 -0
  134. package/src/import/core/runner/reports.js +50 -0
  135. package/src/import/core/runner/run.js +79 -0
  136. package/src/import/core/runner/tracks.js +150 -0
  137. package/src/import/core/runner/ui-drafts.js +337 -0
  138. package/src/import/core/runner.js +3 -698
  139. package/src/import/core/shared/api-routes.js +221 -0
  140. package/src/import/core/shared/candidates.js +97 -0
  141. package/src/import/core/shared/files.js +177 -0
  142. package/src/import/core/shared/next-app.js +389 -0
  143. package/src/import/core/shared/types.d.ts +51 -0
  144. package/src/import/core/shared/ui-routes.js +230 -0
  145. package/src/import/core/shared.js +60 -861
  146. package/src/new-project/constants.js +128 -0
  147. package/src/new-project/create.js +83 -0
  148. package/src/new-project/json.js +28 -0
  149. package/src/new-project/metadata.js +96 -0
  150. package/src/new-project/package-spec.js +161 -0
  151. package/src/new-project/project-files.js +348 -0
  152. package/src/new-project/template-policy.js +269 -0
  153. package/src/new-project/template-resolution.js +368 -0
  154. package/src/new-project/template-snapshots.js +430 -0
  155. package/src/new-project/template-updates.js +512 -0
  156. package/src/new-project/types.d.ts +83 -0
  157. package/src/new-project.js +6 -2277
  158. package/src/parser.d.ts +87 -1
  159. package/src/parser.js +118 -0
  160. package/src/policy/review-boundaries.d.ts +15 -0
  161. package/src/project-config/index.js +564 -0
  162. package/src/project-config.js +19 -561
  163. package/src/resolver/enrich/acceptance-criterion.js +2 -0
  164. package/src/resolver/enrich/bug.js +2 -0
  165. package/src/resolver/enrich/pitch.js +2 -0
  166. package/src/resolver/enrich/requirement.js +2 -0
  167. package/src/resolver/enrich/task.js +2 -0
  168. package/src/resolver/index.js +19 -2089
  169. package/src/resolver/normalize.js +384 -1
  170. package/src/resolver/plans.js +168 -0
  171. package/src/resolver/projections-api.js +494 -0
  172. package/src/resolver/projections-db.js +133 -0
  173. package/src/resolver/projections-ui.js +317 -0
  174. package/src/resolver/shapes.js +251 -0
  175. package/src/resolver/shared.js +278 -0
  176. package/src/resolver/widgets.js +132 -0
  177. package/src/template-trust/constants.js +62 -0
  178. package/src/template-trust/content.js +258 -0
  179. package/src/template-trust/diff.js +92 -0
  180. package/src/template-trust/policy.js +61 -0
  181. package/src/template-trust/record.js +90 -0
  182. package/src/template-trust/status.js +182 -0
  183. package/src/template-trust.js +24 -687
  184. package/src/text-helpers.d.ts +1 -0
  185. package/src/topogram-types.d.ts +69 -0
  186. package/src/validator/common.js +488 -0
  187. package/src/validator/data-model.js +237 -0
  188. package/src/validator/docs.js +167 -0
  189. package/src/validator/expressions.js +146 -1
  190. package/src/validator/index.d.ts +23 -0
  191. package/src/validator/index.js +32 -3585
  192. package/src/validator/kinds.d.ts +41 -0
  193. package/src/validator/kinds.js +2 -0
  194. package/src/validator/model-helpers.js +46 -0
  195. package/src/validator/per-kind/acceptance-criterion.js +5 -0
  196. package/src/validator/per-kind/bug.js +6 -0
  197. package/src/validator/per-kind/domain.js +15 -2
  198. package/src/validator/per-kind/pitch.js +7 -0
  199. package/src/validator/per-kind/requirement.js +5 -0
  200. package/src/validator/per-kind/task.js +7 -0
  201. package/src/validator/per-kind/widget.js +14 -0
  202. package/src/validator/projections/api-http-async.js +410 -0
  203. package/src/validator/projections/api-http-authz.js +88 -0
  204. package/src/validator/projections/api-http-core.js +205 -0
  205. package/src/validator/projections/api-http-policies.js +339 -0
  206. package/src/validator/projections/api-http-responses.js +233 -0
  207. package/src/validator/projections/api-http.js +44 -0
  208. package/src/validator/projections/db.js +353 -0
  209. package/src/validator/projections/generator-defaults.js +45 -0
  210. package/src/validator/projections/helpers.js +87 -0
  211. package/src/validator/projections/ui-helpers.js +214 -0
  212. package/src/validator/projections/ui-navigation.js +344 -0
  213. package/src/validator/projections/ui-structure.js +364 -0
  214. package/src/validator/projections/ui-widgets.js +493 -0
  215. package/src/validator/projections/ui.js +46 -0
  216. package/src/validator/registry.js +48 -1
  217. package/src/validator/utils.d.ts +20 -0
  218. package/src/validator/utils.js +115 -12
  219. package/src/widget-behavior.d.ts +1 -0
  220. package/src/workflows/import-app/api/collect.js +221 -0
  221. package/src/workflows/import-app/api/openapi.js +257 -0
  222. package/src/workflows/import-app/api/routes.js +327 -0
  223. package/src/workflows/import-app/api/sources.js +22 -0
  224. package/src/workflows/import-app/api.js +2 -797
  225. package/src/workflows/reconcile/adoption-plan/build.js +208 -0
  226. package/src/workflows/reconcile/adoption-plan/dependencies.js +75 -0
  227. package/src/workflows/reconcile/adoption-plan/outputs.js +143 -0
  228. package/src/workflows/reconcile/adoption-plan/paths.js +58 -0
  229. package/src/workflows/reconcile/adoption-plan/projection-patches.js +177 -0
  230. package/src/workflows/reconcile/adoption-plan/reasons.js +107 -0
  231. package/src/workflows/reconcile/adoption-plan.js +30 -740
  232. package/src/workflows/reconcile/auth/closures.js +115 -0
  233. package/src/workflows/reconcile/auth/formatters.js +142 -0
  234. package/src/workflows/reconcile/auth/inference.js +330 -0
  235. package/src/workflows/reconcile/auth/roles.js +122 -0
  236. package/src/workflows/reconcile/auth.js +35 -690
  237. package/src/workflows/reconcile/bundle-core/index.js +600 -0
  238. package/src/workflows/reconcile/bundle-core.js +12 -598
  239. package/src/workflows/reconcile/canonical-surface.js +1 -1
  240. package/src/workflows/reconcile/impacts/adoption-plan.js +192 -0
  241. package/src/workflows/reconcile/impacts/indexes.js +101 -0
  242. package/src/workflows/reconcile/impacts/patches.js +252 -0
  243. package/src/workflows/reconcile/impacts/reports.js +80 -0
  244. package/src/workflows/reconcile/impacts.js +14 -623
  245. package/src/workspace-docs.d.ts +29 -0
@@ -0,0 +1,422 @@
1
+ // @ts-check
2
+
3
+ import path from "node:path";
4
+
5
+ import { loadProjectConfig } from "../../../project-config.js";
6
+ import {
7
+ loadTemplatePolicy,
8
+ packageScopeFromSpec,
9
+ templatePolicyDiagnosticsForTemplate,
10
+ writeTemplatePolicy,
11
+ writeTemplatePolicyForProject
12
+ } from "../../../new-project.js";
13
+ import { templateCheckDiagnostic } from "./diagnostics.js";
14
+
15
+ /**
16
+ * @typedef {Object} TemplateCheckDiagnostic
17
+ * @property {string} code
18
+ * @property {"error"|"warning"} severity
19
+ * @property {string} message
20
+ * @property {string|null} path
21
+ * @property {string|null} suggestedFix
22
+ * @property {string|null} step
23
+ */
24
+
25
+ /**
26
+ * @param {ReturnType<typeof loadProjectConfig>} projectConfigInfo
27
+ * @returns {{ requested: string, root: string, manifest: { id: string, version: string, kind: string, topogramVersion: string, includesExecutableImplementation: boolean }, source: "local"|"package", packageSpec: string|null }}
28
+ */
29
+ function currentPolicyTemplate(projectConfigInfo) {
30
+ const template = projectConfigInfo?.config.template || {};
31
+ const source = template.source === "local" || template.source === "package"
32
+ ? template.source
33
+ : "local";
34
+ return {
35
+ requested: typeof template.requested === "string" ? template.requested : String(template.id || "unknown"),
36
+ root: projectConfigInfo?.configDir || process.cwd(),
37
+ manifest: {
38
+ id: typeof template.id === "string" ? template.id : "unknown",
39
+ version: typeof template.version === "string" ? template.version : "unknown",
40
+ kind: "starter",
41
+ topogramVersion: "*",
42
+ includesExecutableImplementation: Boolean(template.includesExecutableImplementation)
43
+ },
44
+ source,
45
+ packageSpec: typeof template.sourceSpec === "string" ? template.sourceSpec : null
46
+ };
47
+ }
48
+
49
+ /**
50
+ * @param {string} projectPath
51
+ * @returns {{ ok: boolean, path: string, exists: boolean, policy: any, diagnostics: TemplateCheckDiagnostic[], errors: string[] }}
52
+ */
53
+ export function buildTemplatePolicyCheckPayload(projectPath) {
54
+ const projectConfigInfo = loadProjectConfig(projectPath);
55
+ if (!projectConfigInfo) {
56
+ const diagnostic = templateCheckDiagnostic({
57
+ code: "template_policy_project_missing",
58
+ message: "Cannot check template policy without topogram.project.json.",
59
+ path: path.resolve(projectPath),
60
+ suggestedFix: "Run this command in a Topogram project.",
61
+ step: "policy"
62
+ });
63
+ return {
64
+ ok: false,
65
+ path: path.join(path.resolve(projectPath), "topogram.template-policy.json"),
66
+ exists: false,
67
+ policy: null,
68
+ diagnostics: [diagnostic],
69
+ errors: [diagnostic.message]
70
+ };
71
+ }
72
+ const policyInfo = loadTemplatePolicy(projectConfigInfo.configDir);
73
+ /** @type {TemplateCheckDiagnostic[]} */
74
+ const diagnostics = policyInfo.diagnostics.map((diagnostic) => templateCheckDiagnostic(diagnostic));
75
+ if (!policyInfo.exists) {
76
+ diagnostics.push(templateCheckDiagnostic({
77
+ code: "template_policy_missing",
78
+ severity: "warning",
79
+ message: "No topogram.template-policy.json found. Template operations are permissive until a policy is defined.",
80
+ path: policyInfo.path,
81
+ suggestedFix: "Run `topogram template policy init` to create a project template policy.",
82
+ step: "policy"
83
+ }));
84
+ } else if (policyInfo.policy) {
85
+ const currentTemplate = currentPolicyTemplate(projectConfigInfo);
86
+ diagnostics.push(...templatePolicyDiagnosticsForTemplate(policyInfo, currentTemplate, "policy")
87
+ .map((diagnostic) => templateCheckDiagnostic(diagnostic)));
88
+ }
89
+ const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error").map((diagnostic) => diagnostic.message);
90
+ return {
91
+ ok: errors.length === 0,
92
+ path: policyInfo.path,
93
+ exists: policyInfo.exists,
94
+ policy: policyInfo.policy,
95
+ diagnostics,
96
+ errors
97
+ };
98
+ }
99
+
100
+ /**
101
+ * @param {string} name
102
+ * @param {boolean} ok
103
+ * @param {string} actual
104
+ * @param {string} expected
105
+ * @param {string} message
106
+ * @param {string|null} fix
107
+ * @returns {{ name: string, ok: boolean, actual: string, expected: string, message: string, fix: string|null }}
108
+ */
109
+ function templatePolicyRule(name, ok, actual, expected, message, fix = null) {
110
+ return { name, ok, actual, expected, message, fix };
111
+ }
112
+
113
+ /**
114
+ * @param {string} name
115
+ * @returns {string}
116
+ */
117
+ function templatePolicyRuleLabel(name) {
118
+ return ({
119
+ "policy-file": "Policy file",
120
+ "allowed-source": "Allowed source",
121
+ "allowed-template-id": "Allowed template id",
122
+ "allowed-package-scope": "Allowed package scope",
123
+ "pinned-version": "Pinned version",
124
+ "executable-implementation": "Executable implementation"
125
+ })[name] || name;
126
+ }
127
+
128
+ /**
129
+ * @param {string} projectPath
130
+ * @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[] }}
131
+ */
132
+ export function buildTemplatePolicyExplainPayload(projectPath) {
133
+ const check = buildTemplatePolicyCheckPayload(projectPath);
134
+ const projectConfigInfo = loadProjectConfig(projectPath);
135
+ if (!projectConfigInfo) {
136
+ return {
137
+ ...check,
138
+ template: null,
139
+ catalog: null,
140
+ package: null,
141
+ rules: []
142
+ };
143
+ }
144
+ const templateMetadata = projectConfigInfo.config.template || {};
145
+ const currentTemplate = currentPolicyTemplate(projectConfigInfo);
146
+ const policy = check.policy;
147
+ const packageScope = currentTemplate.source === "package"
148
+ ? packageScopeFromSpec(currentTemplate.packageSpec || currentTemplate.requested)
149
+ : null;
150
+ const rules = [];
151
+ rules.push(templatePolicyRule(
152
+ "policy-file",
153
+ check.exists,
154
+ check.exists ? "present" : "missing",
155
+ "present",
156
+ check.exists
157
+ ? "Project has a template policy file."
158
+ : "Project has no template policy file; template operations are permissive until one is defined.",
159
+ check.exists ? null : "Run `topogram template policy init`."
160
+ ));
161
+ if (policy) {
162
+ rules.push(templatePolicyRule(
163
+ "allowed-source",
164
+ policy.allowedSources.length === 0 || policy.allowedSources.includes(currentTemplate.source),
165
+ currentTemplate.source,
166
+ policy.allowedSources.length > 0 ? policy.allowedSources.join(", ") : "(any)",
167
+ "Current template source must be allowed by allowedSources.",
168
+ `Add '${currentTemplate.source}' to allowedSources after review, or run \`topogram template policy init\`.`
169
+ ));
170
+ rules.push(templatePolicyRule(
171
+ "allowed-template-id",
172
+ policy.allowedTemplateIds.length === 0 || policy.allowedTemplateIds.includes(currentTemplate.manifest.id),
173
+ currentTemplate.manifest.id,
174
+ policy.allowedTemplateIds.length > 0 ? policy.allowedTemplateIds.join(", ") : "(any)",
175
+ "Current template id must be allowed by allowedTemplateIds.",
176
+ `Run \`topogram template policy pin ${currentTemplate.manifest.id}@${currentTemplate.manifest.version}\` after review.`
177
+ ));
178
+ if (currentTemplate.source === "package") {
179
+ rules.push(templatePolicyRule(
180
+ "allowed-package-scope",
181
+ !policy.allowedPackageScopes ||
182
+ policy.allowedPackageScopes.length === 0 ||
183
+ Boolean(packageScope && policy.allowedPackageScopes.includes(packageScope)),
184
+ packageScope || "(unscoped)",
185
+ policy.allowedPackageScopes && policy.allowedPackageScopes.length > 0 ? policy.allowedPackageScopes.join(", ") : "(any)",
186
+ "Package-backed template source must be in an allowed package scope.",
187
+ `Add '${packageScope || "(unscoped)"}' to allowedPackageScopes after review.`
188
+ ));
189
+ }
190
+ const pinnedVersion = policy.pinnedVersions?.[currentTemplate.manifest.id] || null;
191
+ rules.push(templatePolicyRule(
192
+ "pinned-version",
193
+ !pinnedVersion || pinnedVersion === currentTemplate.manifest.version,
194
+ currentTemplate.manifest.version,
195
+ pinnedVersion || "(unpinned)",
196
+ "Pinned version must match the current template version when a pin exists.",
197
+ `Run \`topogram template policy pin ${currentTemplate.manifest.id}@${currentTemplate.manifest.version}\` after review.`
198
+ ));
199
+ rules.push(templatePolicyRule(
200
+ "executable-implementation",
201
+ !currentTemplate.manifest.includesExecutableImplementation || policy.executableImplementation !== "deny",
202
+ currentTemplate.manifest.includesExecutableImplementation ? "yes" : "no",
203
+ policy.executableImplementation,
204
+ "Executable template implementation must be allowed when implementation/ is present.",
205
+ "Review implementation/, then set executableImplementation to 'allow' or choose a non-executable template."
206
+ ));
207
+ }
208
+ return {
209
+ ...check,
210
+ template: {
211
+ id: currentTemplate.manifest.id,
212
+ version: currentTemplate.manifest.version,
213
+ source: currentTemplate.source,
214
+ requested: currentTemplate.requested,
215
+ sourceSpec: currentTemplate.packageSpec,
216
+ includesExecutableImplementation: currentTemplate.manifest.includesExecutableImplementation
217
+ },
218
+ catalog: templateMetadata.catalog || null,
219
+ package: currentTemplate.source === "package" ? {
220
+ spec: currentTemplate.packageSpec,
221
+ scope: packageScope
222
+ } : null,
223
+ rules
224
+ };
225
+ }
226
+
227
+ /**
228
+ * @param {ReturnType<typeof buildTemplatePolicyExplainPayload>} payload
229
+ * @returns {void}
230
+ */
231
+ export function printTemplatePolicyExplainPayload(payload) {
232
+ console.log(payload.ok ? "Template policy: allowed" : "Template policy: denied");
233
+ console.log(payload.ok
234
+ ? "Decision: the current template is allowed by this project's template policy."
235
+ : "Decision: the current template is blocked by this project's template policy.");
236
+ console.log(`Policy file: ${payload.path}`);
237
+ console.log(`Policy file exists: ${payload.exists ? "yes" : "no"}`);
238
+ if (payload.template) {
239
+ console.log(`Template: ${payload.template.id}@${payload.template.version}`);
240
+ console.log(`Source: ${payload.template.source}`);
241
+ console.log(`Requested: ${payload.template.requested}`);
242
+ if (payload.template.sourceSpec) {
243
+ console.log(`Source spec: ${payload.template.sourceSpec}`);
244
+ }
245
+ console.log(`Executable implementation: ${payload.template.includesExecutableImplementation ? "yes" : "no"}`);
246
+ }
247
+ if (payload.catalog?.id) {
248
+ console.log(`Catalog: ${payload.catalog.id} from ${payload.catalog.source || "unknown"}`);
249
+ console.log(`Catalog package: ${payload.catalog.packageSpec || payload.catalog.package || "unknown"}`);
250
+ }
251
+ if (payload.package) {
252
+ console.log(`Package scope: ${payload.package.scope || "(unscoped)"}`);
253
+ }
254
+ if (payload.rules.length > 0) {
255
+ console.log("");
256
+ console.log("Policy checks:");
257
+ }
258
+ for (const rule of payload.rules) {
259
+ console.log(`${rule.ok ? "PASS" : "FAIL"} ${templatePolicyRuleLabel(rule.name)}: ${rule.message}`);
260
+ console.log(` actual: ${rule.actual}`);
261
+ console.log(` expected: ${rule.expected}`);
262
+ if (!rule.ok && rule.fix) {
263
+ console.log(` fix: ${rule.fix}`);
264
+ }
265
+ }
266
+ for (const diagnostic of payload.diagnostics) {
267
+ const label = diagnostic.severity === "warning" ? "Warning" : "Error";
268
+ console.log(`${label}: ${diagnostic.code}: ${diagnostic.message}`);
269
+ if (diagnostic.suggestedFix) {
270
+ console.log(` fix: ${diagnostic.suggestedFix}`);
271
+ }
272
+ }
273
+ }
274
+
275
+ /**
276
+ * @param {{ ok: boolean, path: string, exists: boolean, policy: any, diagnostics: TemplateCheckDiagnostic[] }} payload
277
+ * @returns {void}
278
+ */
279
+ export function printTemplatePolicyCheckPayload(payload) {
280
+ console.log(payload.ok ? "Template policy check passed" : "Template policy check failed");
281
+ console.log(`Policy: ${payload.path}`);
282
+ console.log(`Exists: ${payload.exists ? "yes" : "no"}`);
283
+ for (const diagnostic of payload.diagnostics) {
284
+ console.log(`[${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`);
285
+ if (diagnostic.path) {
286
+ console.log(` path: ${diagnostic.path}`);
287
+ }
288
+ if (diagnostic.suggestedFix) {
289
+ console.log(` fix: ${diagnostic.suggestedFix}`);
290
+ }
291
+ }
292
+ }
293
+
294
+ /**
295
+ * @param {string|null|undefined} spec
296
+ * @returns {{ id: string, version: string }|null}
297
+ */
298
+ function parseTemplateVersionPin(spec) {
299
+ if (!spec) {
300
+ return null;
301
+ }
302
+ const separator = spec.lastIndexOf("@");
303
+ if (separator <= 0 || separator === spec.length - 1) {
304
+ throw new Error("Template policy pin requires a template id and version, for example @scope/template@0.2.0.");
305
+ }
306
+ return {
307
+ id: spec.slice(0, separator),
308
+ version: spec.slice(separator + 1)
309
+ };
310
+ }
311
+
312
+ /**
313
+ * @param {string} projectPath
314
+ * @param {string|null|undefined} spec
315
+ * @returns {{ ok: boolean, path: string, policy: any, pinned: { id: string, version: string }, diagnostics: TemplateCheckDiagnostic[], errors: string[] }}
316
+ */
317
+ export function buildTemplatePolicyPinPayload(projectPath, spec) {
318
+ const projectConfigInfo = loadProjectConfig(projectPath);
319
+ if (!projectConfigInfo) {
320
+ const diagnostic = templateCheckDiagnostic({
321
+ code: "template_policy_project_missing",
322
+ message: "Cannot pin template policy without topogram.project.json.",
323
+ path: path.resolve(projectPath),
324
+ suggestedFix: "Run this command in a Topogram project.",
325
+ step: "policy"
326
+ });
327
+ return {
328
+ ok: false,
329
+ path: path.join(path.resolve(projectPath), "topogram.template-policy.json"),
330
+ policy: null,
331
+ pinned: { id: "", version: "" },
332
+ diagnostics: [diagnostic],
333
+ errors: [diagnostic.message]
334
+ };
335
+ }
336
+ const parsed = parseTemplateVersionPin(spec);
337
+ const currentTemplate = projectConfigInfo.config.template || {};
338
+ const pin = parsed || {
339
+ id: typeof currentTemplate.id === "string" ? currentTemplate.id : "",
340
+ version: typeof currentTemplate.version === "string" ? currentTemplate.version : ""
341
+ };
342
+ if (!pin.id || !pin.version) {
343
+ const diagnostic = templateCheckDiagnostic({
344
+ code: "template_policy_pin_missing_version",
345
+ message: "Cannot pin a template version without a template id and version.",
346
+ path: projectConfigInfo.configPath,
347
+ suggestedFix: "Pass a pin such as @scope/template@0.2.0, or ensure topogram.project.json records template.id and template.version.",
348
+ step: "policy"
349
+ });
350
+ return {
351
+ ok: false,
352
+ path: path.join(projectConfigInfo.configDir, "topogram.template-policy.json"),
353
+ policy: null,
354
+ pinned: pin,
355
+ diagnostics: [diagnostic],
356
+ errors: [diagnostic.message]
357
+ };
358
+ }
359
+
360
+ const existing = loadTemplatePolicy(projectConfigInfo.configDir);
361
+ const diagnostics = existing.diagnostics.map((diagnostic) => templateCheckDiagnostic(diagnostic));
362
+ if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
363
+ return {
364
+ ok: false,
365
+ path: existing.path,
366
+ policy: existing.policy,
367
+ pinned: pin,
368
+ diagnostics,
369
+ errors: diagnostics.map((diagnostic) => diagnostic.message)
370
+ };
371
+ }
372
+ const policy = existing.policy || writeTemplatePolicyForProject(projectConfigInfo.configDir, projectConfigInfo.config);
373
+ const allowedTemplateIds = policy.allowedTemplateIds.includes(pin.id)
374
+ ? policy.allowedTemplateIds
375
+ : [...policy.allowedTemplateIds, pin.id];
376
+ const allowedPackageScopes = [...(policy.allowedPackageScopes || [])];
377
+ if (pin.id.startsWith("@")) {
378
+ const scope = pin.id.split("/")[0];
379
+ if (scope && !allowedPackageScopes.includes(scope)) {
380
+ allowedPackageScopes.push(scope);
381
+ }
382
+ }
383
+ const nextPolicy = {
384
+ ...policy,
385
+ allowedTemplateIds,
386
+ allowedPackageScopes,
387
+ pinnedVersions: {
388
+ ...(policy.pinnedVersions || {}),
389
+ [pin.id]: pin.version
390
+ }
391
+ };
392
+ writeTemplatePolicy(projectConfigInfo.configDir, nextPolicy);
393
+ return {
394
+ ok: true,
395
+ path: path.join(projectConfigInfo.configDir, "topogram.template-policy.json"),
396
+ policy: nextPolicy,
397
+ pinned: pin,
398
+ diagnostics: [],
399
+ errors: []
400
+ };
401
+ }
402
+
403
+ /**
404
+ * @param {{ ok: boolean, path: string, pinned: { id: string, version: string }, diagnostics: TemplateCheckDiagnostic[] }} payload
405
+ * @returns {void}
406
+ */
407
+ export function printTemplatePolicyPinPayload(payload) {
408
+ console.log(payload.ok ? "Template policy pin updated" : "Template policy pin failed");
409
+ console.log(`Policy: ${payload.path}`);
410
+ if (payload.pinned.id) {
411
+ console.log(`Pinned: ${payload.pinned.id}@${payload.pinned.version || "unknown"}`);
412
+ }
413
+ for (const diagnostic of payload.diagnostics) {
414
+ console.log(`[${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`);
415
+ if (diagnostic.path) {
416
+ console.log(` path: ${diagnostic.path}`);
417
+ }
418
+ if (diagnostic.suggestedFix) {
419
+ console.log(` fix: ${diagnostic.suggestedFix}`);
420
+ }
421
+ }
422
+ }
@@ -0,0 +1,127 @@
1
+ // @ts-check
2
+
3
+ import childProcess from "node:child_process";
4
+
5
+ import { assertSafeNpmSpec, localNpmrcEnv } from "../../../npm-safety.js";
6
+
7
+ /**
8
+ * @param {unknown} error
9
+ * @returns {string}
10
+ */
11
+ export function messageFromError(error) {
12
+ return error instanceof Error ? error.message : String(error);
13
+ }
14
+
15
+ /**
16
+ * @param {...{ ok: boolean, errors?: any[] }|null|undefined} results
17
+ * @returns {{ ok: boolean, errors: any[] }}
18
+ */
19
+ export function combineProjectValidationResults(...results) {
20
+ const errors = [];
21
+ for (const result of results) {
22
+ errors.push(...(result?.errors || []));
23
+ }
24
+ return {
25
+ ok: errors.length === 0,
26
+ errors
27
+ };
28
+ }
29
+
30
+ /**
31
+ * @param {string} spec
32
+ * @returns {string}
33
+ */
34
+ export function packageNameFromPackageSpec(spec) {
35
+ if (spec.startsWith("@")) {
36
+ const segments = spec.split("/");
37
+ if (segments.length < 2) {
38
+ throw new Error(`Invalid scoped package spec '${spec}'.`);
39
+ }
40
+ const scope = segments[0];
41
+ const nameAndVersion = segments.slice(1).join("/");
42
+ const versionIndex = nameAndVersion.indexOf("@");
43
+ return `${scope}/${versionIndex >= 0 ? nameAndVersion.slice(0, versionIndex) : nameAndVersion}`;
44
+ }
45
+ const versionIndex = spec.indexOf("@");
46
+ return versionIndex >= 0 ? spec.slice(0, versionIndex) : spec;
47
+ }
48
+
49
+ /**
50
+ * @param {Record<string, any>|null|undefined} projectConfig
51
+ * @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 }}
52
+ */
53
+ export function templateMetadataFromProjectConfig(projectConfig) {
54
+ const template = projectConfig?.template || {};
55
+ return {
56
+ id: typeof template.id === "string" ? template.id : null,
57
+ version: typeof template.version === "string" ? template.version : null,
58
+ source: typeof template.source === "string" ? template.source : null,
59
+ sourceSpec: typeof template.sourceSpec === "string" ? template.sourceSpec : null,
60
+ requested: typeof template.requested === "string" ? template.requested : null,
61
+ sourceRoot: typeof template.sourceRoot === "string" ? template.sourceRoot : null,
62
+ catalog: template.catalog && typeof template.catalog === "object" && !Array.isArray(template.catalog)
63
+ ? template.catalog
64
+ : null,
65
+ includesExecutableImplementation: typeof template.includesExecutableImplementation === "boolean"
66
+ ? template.includesExecutableImplementation
67
+ : null
68
+ };
69
+ }
70
+
71
+ /**
72
+ * @param {string} packageName
73
+ * @returns {string}
74
+ */
75
+ function latestVersionForPackage(packageName) {
76
+ assertSafeNpmSpec(packageName);
77
+ const npmBin = process.platform === "win32" ? "npm.cmd" : "npm";
78
+ const result = childProcess.spawnSync(npmBin, ["view", "--json", "--", packageName, "version"], {
79
+ encoding: "utf8",
80
+ env: {
81
+ ...process.env,
82
+ ...localNpmrcEnv(process.cwd()),
83
+ PATH: process.env.PATH || ""
84
+ }
85
+ });
86
+ if (result.status !== 0) {
87
+ throw new Error(`Failed to inspect latest version for '${packageName}'.\n${result.stderr || result.stdout}`.trim());
88
+ }
89
+ const raw = (result.stdout || "").trim();
90
+ if (!raw) {
91
+ throw new Error(`npm view returned no version for '${packageName}'.`);
92
+ }
93
+ const parsed = JSON.parse(raw);
94
+ if (typeof parsed !== "string" || !parsed) {
95
+ throw new Error(`npm view returned an invalid version for '${packageName}'.`);
96
+ }
97
+ return parsed;
98
+ }
99
+
100
+ /**
101
+ * @param {ReturnType<typeof templateMetadataFromProjectConfig>} template
102
+ * @returns {{ checked: boolean, supported: boolean, packageName: string|null, version: string|null, isCurrent: boolean|null, candidateSpec: string|null, reason: string|null }}
103
+ */
104
+ export function latestTemplateInfo(template) {
105
+ if (template.source !== "package") {
106
+ return {
107
+ checked: true,
108
+ supported: false,
109
+ packageName: null,
110
+ version: null,
111
+ isCurrent: null,
112
+ candidateSpec: null,
113
+ reason: "Latest-version lookup is only supported for package-backed templates."
114
+ };
115
+ }
116
+ const packageName = packageNameFromPackageSpec(template.sourceSpec || template.requested || template.id || "");
117
+ const version = latestVersionForPackage(packageName);
118
+ return {
119
+ checked: true,
120
+ supported: true,
121
+ packageName,
122
+ version,
123
+ isCurrent: template.version === version,
124
+ candidateSpec: `${packageName}@${version}`,
125
+ reason: null
126
+ };
127
+ }