@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.
- package/package.json +1 -1
- package/src/adoption/plan/index.js +703 -0
- package/src/adoption/plan.js +12 -703
- package/src/agent-ops/query-builders/auth.js +375 -0
- package/src/agent-ops/query-builders/change-risk/change-plan.js +123 -0
- package/src/agent-ops/query-builders/change-risk/import-plan.js +49 -0
- package/src/agent-ops/query-builders/change-risk/maintained.js +286 -0
- package/src/agent-ops/query-builders/change-risk/review-packets.js +123 -0
- package/src/agent-ops/query-builders/change-risk/risk.js +189 -0
- package/src/agent-ops/query-builders/change-risk.js +25 -0
- package/src/agent-ops/query-builders/common.js +149 -0
- package/src/agent-ops/query-builders/maintained-risk.js +539 -0
- package/src/agent-ops/query-builders/maintained-shared.js +120 -0
- package/src/agent-ops/query-builders/multi-agent.js +547 -0
- package/src/agent-ops/query-builders/projection-impacts.js +514 -0
- package/src/agent-ops/query-builders/work-packets.js +417 -0
- package/src/agent-ops/query-builders/workflow-context-shared.js +300 -0
- package/src/agent-ops/query-builders/workflow-context.js +398 -0
- package/src/agent-ops/query-builders/workflow-presets-core.js +676 -0
- package/src/agent-ops/query-builders/workflow-presets.js +341 -0
- package/src/agent-ops/query-builders.d.ts +26 -26
- package/src/agent-ops/query-builders.js +42 -5021
- package/src/catalog/constants.js +10 -0
- package/src/catalog/copy.js +60 -0
- package/src/catalog/diagnostics.js +15 -0
- package/src/catalog/entries.js +42 -0
- package/src/catalog/files.js +67 -0
- package/src/catalog/provenance.js +122 -0
- package/src/catalog/source.js +150 -0
- package/src/catalog/validation.js +252 -0
- package/src/catalog.d.ts +2 -0
- package/src/catalog.js +18 -746
- package/src/cli/commands/catalog/check.js +31 -0
- package/src/cli/commands/catalog/copy.js +59 -0
- package/src/cli/commands/catalog/doctor.js +248 -0
- package/src/cli/commands/catalog/help.js +21 -0
- package/src/cli/commands/catalog/list.js +52 -0
- package/src/cli/commands/catalog/runner.js +92 -0
- package/src/cli/commands/catalog/shared.js +17 -0
- package/src/cli/commands/catalog/show.js +134 -0
- package/src/cli/commands/catalog.js +30 -615
- package/src/cli/commands/generator-policy/package-info.js +162 -0
- package/src/cli/commands/generator-policy/payloads.js +372 -0
- package/src/cli/commands/generator-policy/printers.js +159 -0
- package/src/cli/commands/generator-policy/runner.js +81 -0
- package/src/cli/commands/generator-policy/shared.js +39 -0
- package/src/cli/commands/generator-policy.js +15 -783
- package/src/cli/commands/import/adopt.js +170 -0
- package/src/cli/commands/import/check.js +91 -0
- package/src/cli/commands/import/diff.js +84 -0
- package/src/cli/commands/import/help.js +47 -0
- package/src/cli/commands/import/paths.js +277 -0
- package/src/cli/commands/import/plan.js +284 -0
- package/src/cli/commands/import/refresh.js +470 -0
- package/src/cli/commands/import/status-history.js +196 -0
- package/src/cli/commands/import/workspace.js +230 -0
- package/src/cli/commands/import.js +33 -1732
- package/src/cli/commands/package/constants.js +17 -0
- package/src/cli/commands/package/doctor.js +240 -0
- package/src/cli/commands/package/help.js +27 -0
- package/src/cli/commands/package/lockfile.js +135 -0
- package/src/cli/commands/package/npm.js +97 -0
- package/src/cli/commands/package/reporting.js +35 -0
- package/src/cli/commands/package/runner.js +33 -0
- package/src/cli/commands/package/shared.js +9 -0
- package/src/cli/commands/package/update-cli.js +252 -0
- package/src/cli/commands/package/versions.js +35 -0
- package/src/cli/commands/package.js +29 -813
- package/src/cli/commands/query/change-plan.js +68 -0
- package/src/cli/commands/query/definitions.js +202 -0
- package/src/cli/commands/query/import-adopt.js +121 -0
- package/src/cli/commands/query/runner/artifacts.js +102 -0
- package/src/cli/commands/query/runner/boundaries.js +211 -0
- package/src/cli/commands/query/runner/change.js +182 -0
- package/src/cli/commands/query/runner/import-adopt.js +111 -0
- package/src/cli/commands/query/runner/index.js +31 -0
- package/src/cli/commands/query/runner/output.js +12 -0
- package/src/cli/commands/query/runner/workflow.js +241 -0
- package/src/cli/commands/query/runner.js +3 -0
- package/src/cli/commands/query/workflow-context.js +5 -0
- package/src/cli/commands/query/workspace.js +274 -0
- package/src/cli/commands/query.js +9 -1300
- package/src/cli/commands/template/baseline.js +100 -0
- package/src/cli/commands/template/check.js +466 -0
- package/src/cli/commands/template/constants.js +8 -0
- package/src/cli/commands/template/diagnostics.js +26 -0
- package/src/cli/commands/template/help.js +28 -0
- package/src/cli/commands/template/lifecycle.js +404 -0
- package/src/cli/commands/template/list-show.js +287 -0
- package/src/cli/commands/template/policy.js +422 -0
- package/src/cli/commands/template/shared.js +127 -0
- package/src/cli/commands/template/updates.js +352 -0
- package/src/cli/commands/template.js +41 -2143
- package/src/generator/api/contracts.js +497 -0
- package/src/generator/api/metadata.js +221 -0
- package/src/generator/api/openapi.js +559 -0
- package/src/generator/api/schema.js +124 -0
- package/src/generator/api/types.d.ts +98 -0
- package/src/generator/api.js +3 -1195
- package/src/generator/context/shared/domain-sdlc.js +282 -0
- package/src/generator/context/shared/maintained-boundary.js +665 -0
- package/src/generator/context/shared/metrics.js +85 -0
- package/src/generator/context/shared/primitives.js +64 -0
- package/src/generator/context/shared/relationships.js +453 -0
- package/src/generator/context/shared/summaries.js +263 -0
- package/src/generator/context/shared/types.d.ts +207 -0
- package/src/generator/context/shared.d.ts +42 -0
- package/src/generator/context/shared.js +80 -1390
- package/src/generator/context/slice/core.js +397 -0
- package/src/generator/context/slice/sdlc.js +417 -0
- package/src/generator/context/slice/ui-packets.js +183 -0
- package/src/generator/context/slice.js +2 -859
- package/src/generator/registry/index.js +507 -0
- package/src/generator/registry.js +18 -504
- package/src/generator/runtime/environment/index.js +666 -0
- package/src/generator/runtime/environment.js +4 -666
- package/src/generator/runtime/runtime-check/index.js +554 -0
- package/src/generator/runtime/runtime-check.js +4 -554
- package/src/generator/runtime/shared/index.js +572 -0
- package/src/generator/runtime/shared.js +19 -570
- package/src/generator/shared.d.ts +2 -0
- package/src/generator/surfaces/shared.d.ts +3 -0
- package/src/generator/widget-conformance/behavior-report.js +258 -0
- package/src/generator/widget-conformance/checks.js +371 -0
- package/src/generator/widget-conformance/projection-context.js +200 -0
- package/src/generator/widget-conformance/report.js +166 -0
- package/src/generator/widget-conformance/types.d.ts +121 -0
- package/src/generator/widget-conformance.js +3 -824
- package/src/import/core/context.d.ts +3 -0
- package/src/import/core/contracts.d.ts +1 -0
- package/src/import/core/registry.d.ts +4 -0
- package/src/import/core/runner/candidates.js +217 -0
- package/src/import/core/runner/options.js +22 -0
- package/src/import/core/runner/reports.js +50 -0
- package/src/import/core/runner/run.js +79 -0
- package/src/import/core/runner/tracks.js +150 -0
- package/src/import/core/runner/ui-drafts.js +337 -0
- package/src/import/core/runner.js +3 -698
- package/src/import/core/shared/api-routes.js +221 -0
- package/src/import/core/shared/candidates.js +97 -0
- package/src/import/core/shared/files.js +177 -0
- package/src/import/core/shared/next-app.js +389 -0
- package/src/import/core/shared/types.d.ts +51 -0
- package/src/import/core/shared/ui-routes.js +230 -0
- package/src/import/core/shared.js +60 -861
- package/src/new-project/constants.js +128 -0
- package/src/new-project/create.js +83 -0
- package/src/new-project/json.js +28 -0
- package/src/new-project/metadata.js +96 -0
- package/src/new-project/package-spec.js +161 -0
- package/src/new-project/project-files.js +348 -0
- package/src/new-project/template-policy.js +269 -0
- package/src/new-project/template-resolution.js +368 -0
- package/src/new-project/template-snapshots.js +430 -0
- package/src/new-project/template-updates.js +512 -0
- package/src/new-project/types.d.ts +83 -0
- package/src/new-project.js +6 -2277
- package/src/parser.d.ts +87 -1
- package/src/parser.js +118 -0
- package/src/policy/review-boundaries.d.ts +15 -0
- package/src/project-config/index.js +564 -0
- package/src/project-config.js +19 -561
- package/src/resolver/enrich/acceptance-criterion.js +2 -0
- package/src/resolver/enrich/bug.js +2 -0
- package/src/resolver/enrich/pitch.js +2 -0
- package/src/resolver/enrich/requirement.js +2 -0
- package/src/resolver/enrich/task.js +2 -0
- package/src/resolver/index.js +19 -2089
- package/src/resolver/normalize.js +384 -1
- package/src/resolver/plans.js +168 -0
- package/src/resolver/projections-api.js +494 -0
- package/src/resolver/projections-db.js +133 -0
- package/src/resolver/projections-ui.js +317 -0
- package/src/resolver/shapes.js +251 -0
- package/src/resolver/shared.js +278 -0
- package/src/resolver/widgets.js +132 -0
- package/src/template-trust/constants.js +62 -0
- package/src/template-trust/content.js +258 -0
- package/src/template-trust/diff.js +92 -0
- package/src/template-trust/policy.js +61 -0
- package/src/template-trust/record.js +90 -0
- package/src/template-trust/status.js +182 -0
- package/src/template-trust.js +24 -687
- package/src/text-helpers.d.ts +1 -0
- package/src/topogram-types.d.ts +69 -0
- package/src/validator/common.js +488 -0
- package/src/validator/data-model.js +237 -0
- package/src/validator/docs.js +167 -0
- package/src/validator/expressions.js +146 -1
- package/src/validator/index.d.ts +23 -0
- package/src/validator/index.js +32 -3585
- package/src/validator/kinds.d.ts +41 -0
- package/src/validator/kinds.js +2 -0
- package/src/validator/model-helpers.js +46 -0
- package/src/validator/per-kind/acceptance-criterion.js +5 -0
- package/src/validator/per-kind/bug.js +6 -0
- package/src/validator/per-kind/domain.js +15 -2
- package/src/validator/per-kind/pitch.js +7 -0
- package/src/validator/per-kind/requirement.js +5 -0
- package/src/validator/per-kind/task.js +7 -0
- package/src/validator/per-kind/widget.js +14 -0
- package/src/validator/projections/api-http-async.js +410 -0
- package/src/validator/projections/api-http-authz.js +88 -0
- package/src/validator/projections/api-http-core.js +205 -0
- package/src/validator/projections/api-http-policies.js +339 -0
- package/src/validator/projections/api-http-responses.js +233 -0
- package/src/validator/projections/api-http.js +44 -0
- package/src/validator/projections/db.js +353 -0
- package/src/validator/projections/generator-defaults.js +45 -0
- package/src/validator/projections/helpers.js +87 -0
- package/src/validator/projections/ui-helpers.js +214 -0
- package/src/validator/projections/ui-navigation.js +344 -0
- package/src/validator/projections/ui-structure.js +364 -0
- package/src/validator/projections/ui-widgets.js +493 -0
- package/src/validator/projections/ui.js +46 -0
- package/src/validator/registry.js +48 -1
- package/src/validator/utils.d.ts +20 -0
- package/src/validator/utils.js +115 -12
- package/src/widget-behavior.d.ts +1 -0
- package/src/workflows/import-app/api/collect.js +221 -0
- package/src/workflows/import-app/api/openapi.js +257 -0
- package/src/workflows/import-app/api/routes.js +327 -0
- package/src/workflows/import-app/api/sources.js +22 -0
- package/src/workflows/import-app/api.js +2 -797
- package/src/workflows/reconcile/adoption-plan/build.js +208 -0
- package/src/workflows/reconcile/adoption-plan/dependencies.js +75 -0
- package/src/workflows/reconcile/adoption-plan/outputs.js +143 -0
- package/src/workflows/reconcile/adoption-plan/paths.js +58 -0
- package/src/workflows/reconcile/adoption-plan/projection-patches.js +177 -0
- package/src/workflows/reconcile/adoption-plan/reasons.js +107 -0
- package/src/workflows/reconcile/adoption-plan.js +30 -740
- package/src/workflows/reconcile/auth/closures.js +115 -0
- package/src/workflows/reconcile/auth/formatters.js +142 -0
- package/src/workflows/reconcile/auth/inference.js +330 -0
- package/src/workflows/reconcile/auth/roles.js +122 -0
- package/src/workflows/reconcile/auth.js +35 -690
- package/src/workflows/reconcile/bundle-core/index.js +600 -0
- package/src/workflows/reconcile/bundle-core.js +12 -598
- package/src/workflows/reconcile/canonical-surface.js +1 -1
- package/src/workflows/reconcile/impacts/adoption-plan.js +192 -0
- package/src/workflows/reconcile/impacts/indexes.js +101 -0
- package/src/workflows/reconcile/impacts/patches.js +252 -0
- package/src/workflows/reconcile/impacts/reports.js +80 -0
- package/src/workflows/reconcile/impacts.js +14 -623
- 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
|
+
}
|