@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
|
@@ -1,2145 +1,43 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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";
|