@topogram/cli 0.3.64 → 0.3.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/adoption/plan/index.js +703 -0
- package/src/adoption/plan.js +12 -703
- package/src/agent-ops/query-builders/auth.js +375 -0
- package/src/agent-ops/query-builders/change-risk/change-plan.js +123 -0
- package/src/agent-ops/query-builders/change-risk/import-plan.js +49 -0
- package/src/agent-ops/query-builders/change-risk/maintained.js +286 -0
- package/src/agent-ops/query-builders/change-risk/review-packets.js +123 -0
- package/src/agent-ops/query-builders/change-risk/risk.js +189 -0
- package/src/agent-ops/query-builders/change-risk.js +25 -0
- package/src/agent-ops/query-builders/common.js +149 -0
- package/src/agent-ops/query-builders/maintained-risk.js +539 -0
- package/src/agent-ops/query-builders/maintained-shared.js +120 -0
- package/src/agent-ops/query-builders/multi-agent.js +547 -0
- package/src/agent-ops/query-builders/projection-impacts.js +514 -0
- package/src/agent-ops/query-builders/work-packets.js +417 -0
- package/src/agent-ops/query-builders/workflow-context-shared.js +300 -0
- package/src/agent-ops/query-builders/workflow-context.js +398 -0
- package/src/agent-ops/query-builders/workflow-presets-core.js +676 -0
- package/src/agent-ops/query-builders/workflow-presets.js +341 -0
- package/src/agent-ops/query-builders.d.ts +26 -26
- package/src/agent-ops/query-builders.js +42 -5021
- package/src/catalog/constants.js +10 -0
- package/src/catalog/copy.js +60 -0
- package/src/catalog/diagnostics.js +15 -0
- package/src/catalog/entries.js +42 -0
- package/src/catalog/files.js +67 -0
- package/src/catalog/provenance.js +122 -0
- package/src/catalog/source.js +150 -0
- package/src/catalog/validation.js +252 -0
- package/src/catalog.d.ts +2 -0
- package/src/catalog.js +18 -746
- package/src/cli/commands/catalog/check.js +31 -0
- package/src/cli/commands/catalog/copy.js +59 -0
- package/src/cli/commands/catalog/doctor.js +248 -0
- package/src/cli/commands/catalog/help.js +21 -0
- package/src/cli/commands/catalog/list.js +52 -0
- package/src/cli/commands/catalog/runner.js +92 -0
- package/src/cli/commands/catalog/shared.js +17 -0
- package/src/cli/commands/catalog/show.js +134 -0
- package/src/cli/commands/catalog.js +30 -615
- package/src/cli/commands/generator-policy/package-info.js +162 -0
- package/src/cli/commands/generator-policy/payloads.js +372 -0
- package/src/cli/commands/generator-policy/printers.js +159 -0
- package/src/cli/commands/generator-policy/runner.js +81 -0
- package/src/cli/commands/generator-policy/shared.js +39 -0
- package/src/cli/commands/generator-policy.js +15 -783
- package/src/cli/commands/import/adopt.js +170 -0
- package/src/cli/commands/import/check.js +91 -0
- package/src/cli/commands/import/diff.js +84 -0
- package/src/cli/commands/import/help.js +47 -0
- package/src/cli/commands/import/paths.js +277 -0
- package/src/cli/commands/import/plan.js +284 -0
- package/src/cli/commands/import/refresh.js +470 -0
- package/src/cli/commands/import/status-history.js +196 -0
- package/src/cli/commands/import/workspace.js +230 -0
- package/src/cli/commands/import.js +33 -1732
- package/src/cli/commands/package/constants.js +17 -0
- package/src/cli/commands/package/doctor.js +240 -0
- package/src/cli/commands/package/help.js +27 -0
- package/src/cli/commands/package/lockfile.js +135 -0
- package/src/cli/commands/package/npm.js +97 -0
- package/src/cli/commands/package/reporting.js +35 -0
- package/src/cli/commands/package/runner.js +33 -0
- package/src/cli/commands/package/shared.js +9 -0
- package/src/cli/commands/package/update-cli.js +252 -0
- package/src/cli/commands/package/versions.js +35 -0
- package/src/cli/commands/package.js +29 -813
- package/src/cli/commands/query/change-plan.js +68 -0
- package/src/cli/commands/query/definitions.js +202 -0
- package/src/cli/commands/query/import-adopt.js +121 -0
- package/src/cli/commands/query/runner/artifacts.js +102 -0
- package/src/cli/commands/query/runner/boundaries.js +211 -0
- package/src/cli/commands/query/runner/change.js +182 -0
- package/src/cli/commands/query/runner/import-adopt.js +111 -0
- package/src/cli/commands/query/runner/index.js +31 -0
- package/src/cli/commands/query/runner/output.js +12 -0
- package/src/cli/commands/query/runner/workflow.js +241 -0
- package/src/cli/commands/query/runner.js +3 -0
- package/src/cli/commands/query/workflow-context.js +5 -0
- package/src/cli/commands/query/workspace.js +274 -0
- package/src/cli/commands/query.js +9 -1300
- package/src/cli/commands/template/baseline.js +100 -0
- package/src/cli/commands/template/check.js +466 -0
- package/src/cli/commands/template/constants.js +8 -0
- package/src/cli/commands/template/diagnostics.js +26 -0
- package/src/cli/commands/template/help.js +28 -0
- package/src/cli/commands/template/lifecycle.js +404 -0
- package/src/cli/commands/template/list-show.js +287 -0
- package/src/cli/commands/template/policy.js +422 -0
- package/src/cli/commands/template/shared.js +127 -0
- package/src/cli/commands/template/updates.js +352 -0
- package/src/cli/commands/template.js +41 -2143
- package/src/generator/api/contracts.js +497 -0
- package/src/generator/api/metadata.js +221 -0
- package/src/generator/api/openapi.js +559 -0
- package/src/generator/api/schema.js +124 -0
- package/src/generator/api/types.d.ts +98 -0
- package/src/generator/api.js +3 -1195
- package/src/generator/context/shared/domain-sdlc.js +282 -0
- package/src/generator/context/shared/maintained-boundary.js +665 -0
- package/src/generator/context/shared/metrics.js +85 -0
- package/src/generator/context/shared/primitives.js +64 -0
- package/src/generator/context/shared/relationships.js +453 -0
- package/src/generator/context/shared/summaries.js +263 -0
- package/src/generator/context/shared/types.d.ts +207 -0
- package/src/generator/context/shared.d.ts +42 -0
- package/src/generator/context/shared.js +80 -1390
- package/src/generator/context/slice/core.js +397 -0
- package/src/generator/context/slice/sdlc.js +417 -0
- package/src/generator/context/slice/ui-packets.js +183 -0
- package/src/generator/context/slice.js +2 -859
- package/src/generator/registry/index.js +507 -0
- package/src/generator/registry.js +18 -504
- package/src/generator/runtime/environment/index.js +666 -0
- package/src/generator/runtime/environment.js +4 -666
- package/src/generator/runtime/runtime-check/index.js +554 -0
- package/src/generator/runtime/runtime-check.js +4 -554
- package/src/generator/runtime/shared/index.js +572 -0
- package/src/generator/runtime/shared.js +19 -570
- package/src/generator/shared.d.ts +2 -0
- package/src/generator/surfaces/shared.d.ts +3 -0
- package/src/generator/widget-conformance/behavior-report.js +258 -0
- package/src/generator/widget-conformance/checks.js +371 -0
- package/src/generator/widget-conformance/projection-context.js +200 -0
- package/src/generator/widget-conformance/report.js +166 -0
- package/src/generator/widget-conformance/types.d.ts +121 -0
- package/src/generator/widget-conformance.js +3 -824
- package/src/import/core/context.d.ts +3 -0
- package/src/import/core/contracts.d.ts +1 -0
- package/src/import/core/registry.d.ts +4 -0
- package/src/import/core/runner/candidates.js +217 -0
- package/src/import/core/runner/options.js +22 -0
- package/src/import/core/runner/reports.js +50 -0
- package/src/import/core/runner/run.js +79 -0
- package/src/import/core/runner/tracks.js +150 -0
- package/src/import/core/runner/ui-drafts.js +337 -0
- package/src/import/core/runner.js +3 -698
- package/src/import/core/shared/api-routes.js +221 -0
- package/src/import/core/shared/candidates.js +97 -0
- package/src/import/core/shared/files.js +177 -0
- package/src/import/core/shared/next-app.js +389 -0
- package/src/import/core/shared/types.d.ts +51 -0
- package/src/import/core/shared/ui-routes.js +230 -0
- package/src/import/core/shared.js +60 -861
- package/src/new-project/constants.js +128 -0
- package/src/new-project/create.js +83 -0
- package/src/new-project/json.js +28 -0
- package/src/new-project/metadata.js +96 -0
- package/src/new-project/package-spec.js +161 -0
- package/src/new-project/project-files.js +348 -0
- package/src/new-project/template-policy.js +269 -0
- package/src/new-project/template-resolution.js +368 -0
- package/src/new-project/template-snapshots.js +430 -0
- package/src/new-project/template-updates.js +512 -0
- package/src/new-project/types.d.ts +83 -0
- package/src/new-project.js +6 -2277
- package/src/parser.d.ts +87 -1
- package/src/parser.js +118 -0
- package/src/policy/review-boundaries.d.ts +15 -0
- package/src/project-config/index.js +564 -0
- package/src/project-config.js +19 -561
- package/src/resolver/enrich/acceptance-criterion.js +2 -0
- package/src/resolver/enrich/bug.js +2 -0
- package/src/resolver/enrich/pitch.js +2 -0
- package/src/resolver/enrich/requirement.js +2 -0
- package/src/resolver/enrich/task.js +2 -0
- package/src/resolver/index.js +19 -2089
- package/src/resolver/normalize.js +384 -1
- package/src/resolver/plans.js +168 -0
- package/src/resolver/projections-api.js +494 -0
- package/src/resolver/projections-db.js +133 -0
- package/src/resolver/projections-ui.js +317 -0
- package/src/resolver/shapes.js +251 -0
- package/src/resolver/shared.js +278 -0
- package/src/resolver/widgets.js +132 -0
- package/src/template-trust/constants.js +62 -0
- package/src/template-trust/content.js +258 -0
- package/src/template-trust/diff.js +92 -0
- package/src/template-trust/policy.js +61 -0
- package/src/template-trust/record.js +90 -0
- package/src/template-trust/status.js +182 -0
- package/src/template-trust.js +24 -687
- package/src/text-helpers.d.ts +1 -0
- package/src/topogram-types.d.ts +69 -0
- package/src/validator/common.js +488 -0
- package/src/validator/data-model.js +237 -0
- package/src/validator/docs.js +167 -0
- package/src/validator/expressions.js +146 -1
- package/src/validator/index.d.ts +23 -0
- package/src/validator/index.js +32 -3585
- package/src/validator/kinds.d.ts +41 -0
- package/src/validator/kinds.js +2 -0
- package/src/validator/model-helpers.js +46 -0
- package/src/validator/per-kind/acceptance-criterion.js +5 -0
- package/src/validator/per-kind/bug.js +6 -0
- package/src/validator/per-kind/domain.js +15 -2
- package/src/validator/per-kind/pitch.js +7 -0
- package/src/validator/per-kind/requirement.js +5 -0
- package/src/validator/per-kind/task.js +7 -0
- package/src/validator/per-kind/widget.js +14 -0
- package/src/validator/projections/api-http-async.js +410 -0
- package/src/validator/projections/api-http-authz.js +88 -0
- package/src/validator/projections/api-http-core.js +205 -0
- package/src/validator/projections/api-http-policies.js +339 -0
- package/src/validator/projections/api-http-responses.js +233 -0
- package/src/validator/projections/api-http.js +44 -0
- package/src/validator/projections/db.js +353 -0
- package/src/validator/projections/generator-defaults.js +45 -0
- package/src/validator/projections/helpers.js +87 -0
- package/src/validator/projections/ui-helpers.js +214 -0
- package/src/validator/projections/ui-navigation.js +344 -0
- package/src/validator/projections/ui-structure.js +364 -0
- package/src/validator/projections/ui-widgets.js +493 -0
- package/src/validator/projections/ui.js +46 -0
- package/src/validator/registry.js +48 -1
- package/src/validator/utils.d.ts +20 -0
- package/src/validator/utils.js +115 -12
- package/src/widget-behavior.d.ts +1 -0
- package/src/workflows/import-app/api/collect.js +221 -0
- package/src/workflows/import-app/api/openapi.js +257 -0
- package/src/workflows/import-app/api/routes.js +327 -0
- package/src/workflows/import-app/api/sources.js +22 -0
- package/src/workflows/import-app/api.js +2 -797
- package/src/workflows/reconcile/adoption-plan/build.js +208 -0
- package/src/workflows/reconcile/adoption-plan/dependencies.js +75 -0
- package/src/workflows/reconcile/adoption-plan/outputs.js +143 -0
- package/src/workflows/reconcile/adoption-plan/paths.js +58 -0
- package/src/workflows/reconcile/adoption-plan/projection-patches.js +177 -0
- package/src/workflows/reconcile/adoption-plan/reasons.js +107 -0
- package/src/workflows/reconcile/adoption-plan.js +30 -740
- package/src/workflows/reconcile/auth/closures.js +115 -0
- package/src/workflows/reconcile/auth/formatters.js +142 -0
- package/src/workflows/reconcile/auth/inference.js +330 -0
- package/src/workflows/reconcile/auth/roles.js +122 -0
- package/src/workflows/reconcile/auth.js +35 -690
- package/src/workflows/reconcile/bundle-core/index.js +600 -0
- package/src/workflows/reconcile/bundle-core.js +12 -598
- package/src/workflows/reconcile/canonical-surface.js +1 -1
- package/src/workflows/reconcile/impacts/adoption-plan.js +192 -0
- package/src/workflows/reconcile/impacts/indexes.js +101 -0
- package/src/workflows/reconcile/impacts/patches.js +252 -0
- package/src/workflows/reconcile/impacts/reports.js +80 -0
- package/src/workflows/reconcile/impacts.js +14 -623
- package/src/workspace-docs.d.ts +29 -0
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { writeTemplateTrustRecord } from "../template-trust.js";
|
|
7
|
+
import { TEMPLATE_FILES_MANIFEST } from "./constants.js";
|
|
8
|
+
import { currentTemplateMetadata, projectTemplateMetadata } from "./metadata.js";
|
|
9
|
+
import { resolveTemplate } from "./template-resolution.js";
|
|
10
|
+
import { issueMessagesFromDiagnostics, templatePolicyDiagnosticsForProject, templateUpdateDiagnostic } from "./template-policy.js";
|
|
11
|
+
import { candidateTemplateFiles, currentTemplateOwnedFileHashes, currentTemplateOwnedFiles, fileHash, fileMatchesBaseline, fileSnapshot, includesTemplateImplementation, normalizeTemplateUpdateActionPath, readTemplateFilesManifest, unifiedTextDiff, updateTemplateFilesManifestRecord, writeCandidateFile, writeTemplateFilesManifest } from "./template-snapshots.js";
|
|
12
|
+
|
|
13
|
+
/** @typedef {import("./types.js").CreateNewProjectOptions} CreateNewProjectOptions */
|
|
14
|
+
/** @typedef {import("./types.js").TemplateUpdatePlanOptions} TemplateUpdatePlanOptions */
|
|
15
|
+
/** @typedef {import("./types.js").TemplateUpdateFileActionOptions} TemplateUpdateFileActionOptions */
|
|
16
|
+
/** @typedef {import("./types.js").TemplateOwnedFileRecord} TemplateOwnedFileRecord */
|
|
17
|
+
/** @typedef {import("./types.js").TemplateManifest} TemplateManifest */
|
|
18
|
+
/** @typedef {import("./types.js").TemplateTopologySummary} TemplateTopologySummary */
|
|
19
|
+
/** @typedef {import("./types.js").TemplatePolicy} TemplatePolicy */
|
|
20
|
+
/** @typedef {import("./types.js").TemplatePolicyInfo} TemplatePolicyInfo */
|
|
21
|
+
/** @typedef {import("./types.js").TemplateUpdateDiagnostic} TemplateUpdateDiagnostic */
|
|
22
|
+
/** @typedef {import("./types.js").ResolvedTemplate} ResolvedTemplate */
|
|
23
|
+
/** @typedef {import("./types.js").CatalogTemplateProvenance} CatalogTemplateProvenance */
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {TemplateUpdatePlanOptions} options
|
|
27
|
+
* @returns {{ ok: boolean, mode: "plan", writes: false, current: { id: string|null, version: string|null, source: string|null, sourceSpec: string|null, requested: string|null }, candidate: { id: string, version: string, source: string, sourceSpec: string, requested: string }, compatible: boolean, issues: string[], diagnostics: TemplateUpdateDiagnostic[], summary: { added: number, changed: number, currentOnly: number, unchanged: number }, files: Array<{ path: string, kind: "added"|"changed"|"current-only"|"unchanged", current: { sha256: string, size: number }|null, candidate: { sha256: string, size: number }|null, binary: boolean, diffOmitted: boolean, unifiedDiff: string|null }> }}
|
|
28
|
+
*/
|
|
29
|
+
export function buildTemplateUpdatePlan({
|
|
30
|
+
projectRoot,
|
|
31
|
+
projectConfig,
|
|
32
|
+
templateName = null,
|
|
33
|
+
templatesRoot
|
|
34
|
+
}) {
|
|
35
|
+
const currentTemplate = projectConfig.template || {};
|
|
36
|
+
const templateSpec = templateName || currentTemplate.sourceSpec || currentTemplate.requested || currentTemplate.id;
|
|
37
|
+
if (!templateSpec || typeof templateSpec !== "string") {
|
|
38
|
+
throw new Error("Cannot plan template update because topogram.project.json has no template source spec.");
|
|
39
|
+
}
|
|
40
|
+
const candidateTemplate = resolveTemplate(templateSpec, templatesRoot);
|
|
41
|
+
const candidateMetadata = projectTemplateMetadata(candidateTemplate);
|
|
42
|
+
/** @type {TemplateUpdateDiagnostic[]} */
|
|
43
|
+
const diagnostics = templatePolicyDiagnosticsForProject(projectRoot, candidateTemplate, "policy");
|
|
44
|
+
if (currentTemplate.id && currentTemplate.id !== candidateMetadata.id) {
|
|
45
|
+
diagnostics.push(templateUpdateDiagnostic({
|
|
46
|
+
code: "template_id_mismatch",
|
|
47
|
+
message: `Candidate template id '${candidateMetadata.id}' does not match current template id '${currentTemplate.id}'.`,
|
|
48
|
+
path: path.join(projectRoot, "topogram.project.json"),
|
|
49
|
+
suggestedFix: "Use a template with the same id, or create a new project from the other template.",
|
|
50
|
+
step: "resolve-candidate"
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
const candidateFiles = candidateTemplateFiles(candidateTemplate, projectConfig);
|
|
54
|
+
const currentFiles = currentTemplateOwnedFiles(
|
|
55
|
+
projectRoot,
|
|
56
|
+
Boolean(includesTemplateImplementation(projectConfig) || candidateMetadata.includesExecutableImplementation),
|
|
57
|
+
projectConfig
|
|
58
|
+
);
|
|
59
|
+
const allPaths = new Set([...candidateFiles.keys(), ...currentFiles.keys()]);
|
|
60
|
+
/** @type {Array<{ path: string, kind: "added"|"changed"|"current-only"|"unchanged", current: { sha256: string, size: number }|null, candidate: { sha256: string, size: number }|null, binary: boolean, diffOmitted: boolean, unifiedDiff: string|null }>} */
|
|
61
|
+
const files = [];
|
|
62
|
+
|
|
63
|
+
for (const relativePath of [...allPaths].sort((a, b) => a.localeCompare(b))) {
|
|
64
|
+
const candidateFile = candidateFiles.get(relativePath) || null;
|
|
65
|
+
const currentFile = currentFiles.get(relativePath) || null;
|
|
66
|
+
const candidateSnapshot = candidateFile
|
|
67
|
+
? fileSnapshot(candidateFile.absolutePath, candidateFile.content)
|
|
68
|
+
: null;
|
|
69
|
+
const currentSnapshot = currentFile
|
|
70
|
+
? fileSnapshot(currentFile.absolutePath, currentFile.content)
|
|
71
|
+
: null;
|
|
72
|
+
let kind = /** @type {"added"|"changed"|"current-only"|"unchanged"} */ ("unchanged");
|
|
73
|
+
if (!currentSnapshot && candidateSnapshot) {
|
|
74
|
+
kind = "added";
|
|
75
|
+
} else if (currentSnapshot && !candidateSnapshot) {
|
|
76
|
+
kind = "current-only";
|
|
77
|
+
} else if (currentSnapshot && candidateSnapshot && (
|
|
78
|
+
currentSnapshot.sha256 !== candidateSnapshot.sha256 ||
|
|
79
|
+
currentSnapshot.size !== candidateSnapshot.size
|
|
80
|
+
)) {
|
|
81
|
+
kind = "changed";
|
|
82
|
+
}
|
|
83
|
+
const binary = Boolean(currentSnapshot?.binary || candidateSnapshot?.binary);
|
|
84
|
+
const diffOmitted = binary || Boolean(currentSnapshot?.diffOmitted || candidateSnapshot?.diffOmitted);
|
|
85
|
+
files.push({
|
|
86
|
+
path: relativePath,
|
|
87
|
+
kind,
|
|
88
|
+
current: currentSnapshot ? { sha256: currentSnapshot.sha256, size: currentSnapshot.size } : null,
|
|
89
|
+
candidate: candidateSnapshot ? { sha256: candidateSnapshot.sha256, size: candidateSnapshot.size } : null,
|
|
90
|
+
binary,
|
|
91
|
+
diffOmitted,
|
|
92
|
+
unifiedDiff: diffOmitted
|
|
93
|
+
? null
|
|
94
|
+
: unifiedTextDiff(relativePath, currentSnapshot?.text || null, candidateSnapshot?.text || null)
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
const visibleFiles = files.filter((file) => file.kind !== "unchanged");
|
|
98
|
+
const summary = {
|
|
99
|
+
added: visibleFiles.filter((file) => file.kind === "added").length,
|
|
100
|
+
changed: visibleFiles.filter((file) => file.kind === "changed").length,
|
|
101
|
+
currentOnly: visibleFiles.filter((file) => file.kind === "current-only").length,
|
|
102
|
+
unchanged: files.filter((file) => file.kind === "unchanged").length
|
|
103
|
+
};
|
|
104
|
+
const issues = issueMessagesFromDiagnostics(diagnostics);
|
|
105
|
+
return {
|
|
106
|
+
ok: issues.length === 0,
|
|
107
|
+
mode: "plan",
|
|
108
|
+
writes: false,
|
|
109
|
+
current: currentTemplateMetadata(projectConfig),
|
|
110
|
+
candidate: {
|
|
111
|
+
id: candidateMetadata.id,
|
|
112
|
+
version: candidateMetadata.version,
|
|
113
|
+
source: candidateMetadata.source,
|
|
114
|
+
sourceSpec: candidateMetadata.sourceSpec,
|
|
115
|
+
requested: candidateMetadata.requested
|
|
116
|
+
},
|
|
117
|
+
compatible: issues.length === 0,
|
|
118
|
+
issues,
|
|
119
|
+
diagnostics,
|
|
120
|
+
summary,
|
|
121
|
+
files: visibleFiles
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @param {TemplateUpdatePlanOptions} options
|
|
127
|
+
* @returns {{ ok: boolean, mode: "check", writes: false, current: ReturnType<typeof buildTemplateUpdatePlan>["current"], candidate: ReturnType<typeof buildTemplateUpdatePlan>["candidate"], compatible: boolean, issues: string[], diagnostics: TemplateUpdateDiagnostic[], summary: ReturnType<typeof buildTemplateUpdatePlan>["summary"], files: ReturnType<typeof buildTemplateUpdatePlan>["files"] }}
|
|
128
|
+
*/
|
|
129
|
+
export function buildTemplateUpdateCheck(options) {
|
|
130
|
+
const plan = buildTemplateUpdatePlan(options);
|
|
131
|
+
const diagnostics = [...plan.diagnostics];
|
|
132
|
+
if (plan.ok && plan.files.length > 0) {
|
|
133
|
+
diagnostics.push(templateUpdateDiagnostic({
|
|
134
|
+
code: "template_update_available",
|
|
135
|
+
message: `Template update has ${plan.files.length} template-owned file change(s).`,
|
|
136
|
+
path: options.projectRoot,
|
|
137
|
+
suggestedFix: "Run `topogram template update --plan` to review, then `topogram template update --apply` after approval.",
|
|
138
|
+
step: "check"
|
|
139
|
+
}));
|
|
140
|
+
}
|
|
141
|
+
const issues = issueMessagesFromDiagnostics(diagnostics);
|
|
142
|
+
return {
|
|
143
|
+
...plan,
|
|
144
|
+
ok: issues.length === 0,
|
|
145
|
+
mode: "check",
|
|
146
|
+
writes: false,
|
|
147
|
+
issues,
|
|
148
|
+
diagnostics
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* @param {TemplateUpdatePlanOptions} options
|
|
154
|
+
* @param {ReturnType<typeof buildTemplateUpdatePlan>} plan
|
|
155
|
+
* @param {"apply"|"status"} mode
|
|
156
|
+
* @returns {{ diagnostics: TemplateUpdateDiagnostic[], issues: string[], skipped: Array<{ path: string, kind: "current-only", reason: string }>, conflicts: Array<{ path: string, reason: string }> }}
|
|
157
|
+
*/
|
|
158
|
+
function analyzeTemplateUpdateApplication(options, plan, mode) {
|
|
159
|
+
/** @type {Array<{ path: string, kind: "current-only", reason: string }>} */
|
|
160
|
+
const skipped = [];
|
|
161
|
+
/** @type {Array<{ path: string, reason: string }>} */
|
|
162
|
+
const conflicts = [];
|
|
163
|
+
/** @type {TemplateUpdateDiagnostic[]} */
|
|
164
|
+
const diagnostics = [...plan.diagnostics];
|
|
165
|
+
if (!plan.ok) {
|
|
166
|
+
return {
|
|
167
|
+
diagnostics,
|
|
168
|
+
issues: issueMessagesFromDiagnostics(diagnostics),
|
|
169
|
+
skipped,
|
|
170
|
+
conflicts
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const baselineManifest = readTemplateFilesManifest(options.projectRoot);
|
|
175
|
+
if (!baselineManifest) {
|
|
176
|
+
diagnostics.push(templateUpdateDiagnostic({
|
|
177
|
+
code: "template_baseline_missing",
|
|
178
|
+
message: `Cannot apply template update because ${TEMPLATE_FILES_MANIFEST} is missing. Review current template-owned files, then run 'topogram trust template' to record the baseline before applying template updates.`,
|
|
179
|
+
path: path.join(options.projectRoot, TEMPLATE_FILES_MANIFEST),
|
|
180
|
+
suggestedFix: "Review current template-owned files, then run `topogram trust template` to record the baseline before applying template updates.",
|
|
181
|
+
step: "baseline"
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
const baselineByPath = new Map((baselineManifest?.files || []).map((file) => [file.path, file]));
|
|
185
|
+
const currentHashes = currentTemplateOwnedFileHashes(options.projectRoot, options.projectConfig);
|
|
186
|
+
for (const file of plan.files) {
|
|
187
|
+
if (file.kind === "current-only") {
|
|
188
|
+
skipped.push({
|
|
189
|
+
path: file.path,
|
|
190
|
+
kind: "current-only",
|
|
191
|
+
reason: "Deletes are not applied by template update --apply in this milestone."
|
|
192
|
+
});
|
|
193
|
+
diagnostics.push(templateUpdateDiagnostic({
|
|
194
|
+
code: "template_current_only_skipped",
|
|
195
|
+
severity: "warning",
|
|
196
|
+
message: `Current-only file '${file.path}' needs manual delete review. Deletes are not applied by template update --apply in this milestone.`,
|
|
197
|
+
path: path.join(options.projectRoot, file.path),
|
|
198
|
+
suggestedFix: "Delete the file manually after review if it should be removed from this project.",
|
|
199
|
+
step: mode
|
|
200
|
+
}));
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (file.kind !== "added" && file.kind !== "changed") {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const baseline = baselineByPath.get(file.path) || null;
|
|
207
|
+
const currentHash = currentHashes.get(file.path) || null;
|
|
208
|
+
if (!fileMatchesBaseline(baseline, currentHash)) {
|
|
209
|
+
const reason = baseline
|
|
210
|
+
? "Current file differs from the last trusted template-owned baseline."
|
|
211
|
+
: "Current file is not part of the trusted template-owned baseline.";
|
|
212
|
+
conflicts.push({
|
|
213
|
+
path: file.path,
|
|
214
|
+
reason
|
|
215
|
+
});
|
|
216
|
+
diagnostics.push(templateUpdateDiagnostic({
|
|
217
|
+
code: "template_update_conflict",
|
|
218
|
+
message: `Template update conflict in '${file.path}': ${reason}`,
|
|
219
|
+
path: path.join(options.projectRoot, file.path),
|
|
220
|
+
suggestedFix: "Review local edits; keep them manually or refresh the baseline with `topogram trust template` after review.",
|
|
221
|
+
step: "conflict-check"
|
|
222
|
+
}));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
diagnostics,
|
|
227
|
+
issues: issueMessagesFromDiagnostics(diagnostics),
|
|
228
|
+
skipped,
|
|
229
|
+
conflicts
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* @param {TemplateUpdatePlanOptions} options
|
|
235
|
+
* @returns {{ ok: boolean, mode: "status", writes: false, current: ReturnType<typeof buildTemplateUpdatePlan>["current"], candidate: ReturnType<typeof buildTemplateUpdatePlan>["candidate"], compatible: boolean, issues: string[], diagnostics: TemplateUpdateDiagnostic[], summary: ReturnType<typeof buildTemplateUpdatePlan>["summary"], applied: Array<{ path: string, kind: "added"|"changed" }>, skipped: Array<{ path: string, kind: "current-only", reason: string }>, conflicts: Array<{ path: string, reason: string }>, files: ReturnType<typeof buildTemplateUpdatePlan>["files"] }}
|
|
236
|
+
*/
|
|
237
|
+
export function buildTemplateUpdateStatus(options) {
|
|
238
|
+
const plan = buildTemplateUpdatePlan(options);
|
|
239
|
+
const analysis = analyzeTemplateUpdateApplication(options, plan, "status");
|
|
240
|
+
const diagnostics = [...analysis.diagnostics];
|
|
241
|
+
if (plan.ok && plan.files.length > 0) {
|
|
242
|
+
diagnostics.push(templateUpdateDiagnostic({
|
|
243
|
+
code: "template_update_available",
|
|
244
|
+
message: `Template update has ${plan.files.length} template-owned file change(s).`,
|
|
245
|
+
path: options.projectRoot,
|
|
246
|
+
suggestedFix: "Run `topogram template update --plan` to review, then `topogram template update --apply` after approval.",
|
|
247
|
+
step: "status"
|
|
248
|
+
}));
|
|
249
|
+
}
|
|
250
|
+
const issues = issueMessagesFromDiagnostics(diagnostics);
|
|
251
|
+
return {
|
|
252
|
+
...plan,
|
|
253
|
+
ok: issues.length === 0,
|
|
254
|
+
mode: "status",
|
|
255
|
+
writes: false,
|
|
256
|
+
issues,
|
|
257
|
+
diagnostics,
|
|
258
|
+
applied: [],
|
|
259
|
+
skipped: analysis.skipped,
|
|
260
|
+
conflicts: analysis.conflicts
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* @param {string} projectRoot
|
|
266
|
+
* @param {string} action
|
|
267
|
+
* @returns {TemplateUpdateDiagnostic}
|
|
268
|
+
*/
|
|
269
|
+
function templateBaselineMissingDiagnostic(projectRoot, action) {
|
|
270
|
+
return templateUpdateDiagnostic({
|
|
271
|
+
code: "template_baseline_missing",
|
|
272
|
+
message: `Cannot ${action} because ${TEMPLATE_FILES_MANIFEST} is missing. Review current template-owned files, then run 'topogram trust template' to record the baseline before applying template updates.`,
|
|
273
|
+
path: path.join(projectRoot, TEMPLATE_FILES_MANIFEST),
|
|
274
|
+
suggestedFix: "Review current template-owned files, then run `topogram trust template` to record the baseline before applying template updates.",
|
|
275
|
+
step: "baseline"
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* @param {TemplateUpdateDiagnostic[]} diagnostics
|
|
281
|
+
* @param {ReturnType<typeof buildTemplateUpdatePlan>|null} plan
|
|
282
|
+
* @param {TemplateUpdateFileActionOptions["action"]} action
|
|
283
|
+
* @param {string} relativePath
|
|
284
|
+
* @param {Array<{ path: string, kind: "added"|"changed" }>} applied
|
|
285
|
+
* @param {Array<{ path: string, kind: "accepted-current" }>} accepted
|
|
286
|
+
* @param {Array<{ path: string, kind: "current-only" }>} deleted
|
|
287
|
+
* @param {Array<{ path: string, reason: string }>} conflicts
|
|
288
|
+
* @param {ReturnType<typeof currentTemplateMetadata>} [current]
|
|
289
|
+
* @returns {{ ok: boolean, mode: TemplateUpdateFileActionOptions["action"], writes: boolean, current: ReturnType<typeof currentTemplateMetadata>, candidate: ReturnType<typeof buildTemplateUpdatePlan>["candidate"]|null, compatible: boolean, issues: string[], diagnostics: TemplateUpdateDiagnostic[], summary: ReturnType<typeof buildTemplateUpdatePlan>["summary"], applied: Array<{ path: string, kind: "added"|"changed" }>, accepted: Array<{ path: string, kind: "accepted-current" }>, deleted: Array<{ path: string, kind: "current-only" }>, skipped: Array<{ path: string, kind: "current-only", reason: string }>, conflicts: Array<{ path: string, reason: string }>, files: ReturnType<typeof buildTemplateUpdatePlan>["files"], action: TemplateUpdateFileActionOptions["action"], path: string }}
|
|
290
|
+
*/
|
|
291
|
+
function templateUpdateFileActionResult(diagnostics, plan, action, relativePath, applied, accepted, deleted, conflicts, current = { id: null, version: null, source: null, sourceSpec: null, requested: null }) {
|
|
292
|
+
const issues = issueMessagesFromDiagnostics(diagnostics);
|
|
293
|
+
return {
|
|
294
|
+
...(plan || {}),
|
|
295
|
+
ok: issues.length === 0,
|
|
296
|
+
mode: action,
|
|
297
|
+
writes: applied.length > 0 || accepted.length > 0 || deleted.length > 0,
|
|
298
|
+
current: plan?.current || current,
|
|
299
|
+
candidate: plan?.candidate || null,
|
|
300
|
+
compatible: plan?.compatible || issues.length === 0,
|
|
301
|
+
issues,
|
|
302
|
+
diagnostics,
|
|
303
|
+
summary: plan?.summary || { added: 0, changed: 0, currentOnly: 0, unchanged: 0 },
|
|
304
|
+
applied,
|
|
305
|
+
accepted,
|
|
306
|
+
deleted,
|
|
307
|
+
skipped: [],
|
|
308
|
+
conflicts,
|
|
309
|
+
files: plan?.files || [],
|
|
310
|
+
action,
|
|
311
|
+
path: relativePath
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* @param {TemplateUpdateFileActionOptions} options
|
|
317
|
+
* @returns {{ ok: boolean, mode: "accept-current"|"accept-candidate"|"delete-current", writes: boolean, current: ReturnType<typeof currentTemplateMetadata>, candidate: ReturnType<typeof buildTemplateUpdatePlan>["candidate"]|null, compatible: boolean, issues: string[], diagnostics: TemplateUpdateDiagnostic[], summary: ReturnType<typeof buildTemplateUpdatePlan>["summary"], applied: Array<{ path: string, kind: "added"|"changed" }>, accepted: Array<{ path: string, kind: "accepted-current" }>, deleted: Array<{ path: string, kind: "current-only" }>, skipped: Array<{ path: string, kind: "current-only", reason: string }>, conflicts: Array<{ path: string, reason: string }>, files: ReturnType<typeof buildTemplateUpdatePlan>["files"], action: "accept-current"|"accept-candidate"|"delete-current", path: string }}
|
|
318
|
+
*/
|
|
319
|
+
export function applyTemplateUpdateFileAction(options) {
|
|
320
|
+
const relativePath = normalizeTemplateUpdateActionPath(options.filePath);
|
|
321
|
+
/** @type {TemplateUpdateDiagnostic[]} */
|
|
322
|
+
const diagnostics = [];
|
|
323
|
+
/** @type {Array<{ path: string, kind: "added"|"changed" }>} */
|
|
324
|
+
const applied = [];
|
|
325
|
+
/** @type {Array<{ path: string, kind: "accepted-current" }>} */
|
|
326
|
+
const accepted = [];
|
|
327
|
+
/** @type {Array<{ path: string, kind: "current-only" }>} */
|
|
328
|
+
const deleted = [];
|
|
329
|
+
/** @type {Array<{ path: string, reason: string }>} */
|
|
330
|
+
const conflicts = [];
|
|
331
|
+
const baselineManifest = readTemplateFilesManifest(options.projectRoot);
|
|
332
|
+
const current = currentTemplateMetadata(options.projectConfig);
|
|
333
|
+
if (!baselineManifest) {
|
|
334
|
+
diagnostics.push(templateBaselineMissingDiagnostic(options.projectRoot, options.action));
|
|
335
|
+
return templateUpdateFileActionResult(diagnostics, null, options.action, relativePath, applied, accepted, deleted, conflicts, current);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (options.action === "accept-current") {
|
|
339
|
+
const currentHashes = currentTemplateOwnedFileHashes(options.projectRoot, options.projectConfig);
|
|
340
|
+
const currentHash = currentHashes.get(relativePath) || null;
|
|
341
|
+
if (!currentHash) {
|
|
342
|
+
diagnostics.push(templateUpdateDiagnostic({
|
|
343
|
+
code: "template_file_not_current",
|
|
344
|
+
message: `Cannot accept current file '${relativePath}' because it is not a current template-owned file.`,
|
|
345
|
+
path: path.join(options.projectRoot, relativePath),
|
|
346
|
+
suggestedFix: "Pass a file under topogram/, topogram.project.json, or trusted implementation/.",
|
|
347
|
+
step: "accept-current"
|
|
348
|
+
}));
|
|
349
|
+
return templateUpdateFileActionResult(diagnostics, null, options.action, relativePath, applied, accepted, deleted, conflicts, current);
|
|
350
|
+
}
|
|
351
|
+
updateTemplateFilesManifestRecord(options.projectRoot, baselineManifest, relativePath, currentHash);
|
|
352
|
+
accepted.push({ path: relativePath, kind: "accepted-current" });
|
|
353
|
+
return templateUpdateFileActionResult(diagnostics, null, options.action, relativePath, applied, accepted, deleted, conflicts, current);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const plan = buildTemplateUpdatePlan(options);
|
|
357
|
+
diagnostics.push(...plan.diagnostics);
|
|
358
|
+
if (!plan.ok) {
|
|
359
|
+
return templateUpdateFileActionResult(diagnostics, plan, options.action, relativePath, applied, accepted, deleted, conflicts);
|
|
360
|
+
}
|
|
361
|
+
const file = plan.files.find((item) => item.path === relativePath) || null;
|
|
362
|
+
if (!file) {
|
|
363
|
+
diagnostics.push(templateUpdateDiagnostic({
|
|
364
|
+
code: "template_file_unchanged",
|
|
365
|
+
message: `Template-owned file '${relativePath}' has no candidate update action.`,
|
|
366
|
+
path: path.join(options.projectRoot, relativePath),
|
|
367
|
+
suggestedFix: "Run `topogram template update --status` to see files that need adoption.",
|
|
368
|
+
step: options.action
|
|
369
|
+
}));
|
|
370
|
+
return templateUpdateFileActionResult(diagnostics, plan, options.action, relativePath, applied, accepted, deleted, conflicts);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const baselineByPath = new Map(baselineManifest.files.map((record) => [record.path, record]));
|
|
374
|
+
const currentHashes = currentTemplateOwnedFileHashes(options.projectRoot, options.projectConfig);
|
|
375
|
+
const baseline = baselineByPath.get(relativePath) || null;
|
|
376
|
+
const currentHash = currentHashes.get(relativePath) || null;
|
|
377
|
+
|
|
378
|
+
if (options.action === "delete-current") {
|
|
379
|
+
if (file.kind !== "current-only") {
|
|
380
|
+
diagnostics.push(templateUpdateDiagnostic({
|
|
381
|
+
code: "template_delete_not_current_only",
|
|
382
|
+
message: `Cannot delete '${relativePath}' because it is not a current-only template-owned file.`,
|
|
383
|
+
path: path.join(options.projectRoot, relativePath),
|
|
384
|
+
suggestedFix: "Use delete-current only for files the candidate template removed.",
|
|
385
|
+
step: "delete-current"
|
|
386
|
+
}));
|
|
387
|
+
return templateUpdateFileActionResult(diagnostics, plan, options.action, relativePath, applied, accepted, deleted, conflicts);
|
|
388
|
+
}
|
|
389
|
+
if (!fileMatchesBaseline(baseline, currentHash)) {
|
|
390
|
+
const reason = baseline
|
|
391
|
+
? "Current file differs from the last trusted template-owned baseline."
|
|
392
|
+
: "Current file is not part of the trusted template-owned baseline.";
|
|
393
|
+
conflicts.push({ path: relativePath, reason });
|
|
394
|
+
diagnostics.push(templateUpdateDiagnostic({
|
|
395
|
+
code: "template_update_conflict",
|
|
396
|
+
message: `Template delete conflict in '${relativePath}': ${reason}`,
|
|
397
|
+
path: path.join(options.projectRoot, relativePath),
|
|
398
|
+
suggestedFix: "Review local edits before deleting, or accept current as the new baseline.",
|
|
399
|
+
step: "delete-current"
|
|
400
|
+
}));
|
|
401
|
+
return templateUpdateFileActionResult(diagnostics, plan, options.action, relativePath, applied, accepted, deleted, conflicts);
|
|
402
|
+
}
|
|
403
|
+
fs.rmSync(path.join(options.projectRoot, relativePath));
|
|
404
|
+
updateTemplateFilesManifestRecord(options.projectRoot, baselineManifest, relativePath, null);
|
|
405
|
+
deleted.push({ path: relativePath, kind: "current-only" });
|
|
406
|
+
return templateUpdateFileActionResult(diagnostics, plan, options.action, relativePath, applied, accepted, deleted, conflicts);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (file.kind !== "added" && file.kind !== "changed") {
|
|
410
|
+
diagnostics.push(templateUpdateDiagnostic({
|
|
411
|
+
code: "template_candidate_not_applicable",
|
|
412
|
+
message: `Cannot accept candidate for '${relativePath}' because the candidate has no added or changed file.`,
|
|
413
|
+
path: path.join(options.projectRoot, relativePath),
|
|
414
|
+
suggestedFix: "Use accept-candidate only for added or changed candidate files.",
|
|
415
|
+
step: "accept-candidate"
|
|
416
|
+
}));
|
|
417
|
+
return templateUpdateFileActionResult(diagnostics, plan, options.action, relativePath, applied, accepted, deleted, conflicts);
|
|
418
|
+
}
|
|
419
|
+
if (file.kind === "changed" && !fileMatchesBaseline(baseline, currentHash)) {
|
|
420
|
+
const reason = baseline
|
|
421
|
+
? "Current file differs from the last trusted template-owned baseline."
|
|
422
|
+
: "Current file is not part of the trusted template-owned baseline.";
|
|
423
|
+
conflicts.push({ path: relativePath, reason });
|
|
424
|
+
diagnostics.push(templateUpdateDiagnostic({
|
|
425
|
+
code: "template_update_conflict",
|
|
426
|
+
message: `Template candidate conflict in '${relativePath}': ${reason}`,
|
|
427
|
+
path: path.join(options.projectRoot, relativePath),
|
|
428
|
+
suggestedFix: "Review local edits before accepting the candidate file.",
|
|
429
|
+
step: "accept-candidate"
|
|
430
|
+
}));
|
|
431
|
+
return templateUpdateFileActionResult(diagnostics, plan, options.action, relativePath, applied, accepted, deleted, conflicts);
|
|
432
|
+
}
|
|
433
|
+
const currentTemplate = options.projectConfig.template || {};
|
|
434
|
+
const templateSpec = options.templateName || currentTemplate.sourceSpec || currentTemplate.requested || currentTemplate.id;
|
|
435
|
+
const candidateTemplate = resolveTemplate(templateSpec, options.templatesRoot);
|
|
436
|
+
const candidateFile = candidateTemplateFiles(candidateTemplate, options.projectConfig).get(relativePath);
|
|
437
|
+
if (!candidateFile) {
|
|
438
|
+
throw new Error(`Cannot accept missing candidate template file: ${relativePath}`);
|
|
439
|
+
}
|
|
440
|
+
writeCandidateFile(candidateFile, path.join(options.projectRoot, relativePath));
|
|
441
|
+
const nextHash = fileHash({
|
|
442
|
+
absolutePath: path.join(options.projectRoot, relativePath),
|
|
443
|
+
content: null
|
|
444
|
+
});
|
|
445
|
+
updateTemplateFilesManifestRecord(options.projectRoot, baselineManifest, relativePath, {
|
|
446
|
+
path: relativePath,
|
|
447
|
+
sha256: nextHash.sha256,
|
|
448
|
+
size: nextHash.size
|
|
449
|
+
});
|
|
450
|
+
applied.push({ path: relativePath, kind: file.kind });
|
|
451
|
+
return templateUpdateFileActionResult(diagnostics, plan, options.action, relativePath, applied, accepted, deleted, conflicts);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* @param {TemplateUpdatePlanOptions} options
|
|
456
|
+
* @returns {{ ok: boolean, mode: "apply", writes: boolean, current: ReturnType<typeof buildTemplateUpdatePlan>["current"], candidate: ReturnType<typeof buildTemplateUpdatePlan>["candidate"], compatible: boolean, issues: string[], diagnostics: TemplateUpdateDiagnostic[], summary: ReturnType<typeof buildTemplateUpdatePlan>["summary"], applied: Array<{ path: string, kind: "added"|"changed" }>, skipped: Array<{ path: string, kind: "current-only", reason: string }>, conflicts: Array<{ path: string, reason: string }>, files: ReturnType<typeof buildTemplateUpdatePlan>["files"] }}
|
|
457
|
+
*/
|
|
458
|
+
export function applyTemplateUpdate(options) {
|
|
459
|
+
const plan = buildTemplateUpdatePlan(options);
|
|
460
|
+
/** @type {Array<{ path: string, kind: "added"|"changed" }>} */
|
|
461
|
+
const applied = [];
|
|
462
|
+
const analysis = analyzeTemplateUpdateApplication(options, plan, "apply");
|
|
463
|
+
const { diagnostics, issues, skipped, conflicts } = analysis;
|
|
464
|
+
if (!plan.ok || issues.length > 0) {
|
|
465
|
+
return {
|
|
466
|
+
...plan,
|
|
467
|
+
ok: false,
|
|
468
|
+
mode: "apply",
|
|
469
|
+
writes: false,
|
|
470
|
+
applied,
|
|
471
|
+
skipped,
|
|
472
|
+
conflicts,
|
|
473
|
+
issues,
|
|
474
|
+
diagnostics
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const currentTemplate = options.projectConfig.template || {};
|
|
479
|
+
const templateSpec = options.templateName || currentTemplate.sourceSpec || currentTemplate.requested || currentTemplate.id;
|
|
480
|
+
const candidateTemplate = resolveTemplate(templateSpec, options.templatesRoot);
|
|
481
|
+
const candidateFiles = candidateTemplateFiles(candidateTemplate, options.projectConfig);
|
|
482
|
+
for (const file of plan.files) {
|
|
483
|
+
if (file.kind !== "added" && file.kind !== "changed") {
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
const candidateFile = candidateFiles.get(file.path);
|
|
487
|
+
if (!candidateFile) {
|
|
488
|
+
throw new Error(`Cannot apply missing candidate template file: ${file.path}`);
|
|
489
|
+
}
|
|
490
|
+
writeCandidateFile(candidateFile, path.join(options.projectRoot, file.path));
|
|
491
|
+
applied.push({ path: file.path, kind: file.kind });
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (applied.length > 0) {
|
|
495
|
+
const nextProjectConfig = JSON.parse(fs.readFileSync(path.join(options.projectRoot, "topogram.project.json"), "utf8"));
|
|
496
|
+
writeTemplateFilesManifest(options.projectRoot, nextProjectConfig);
|
|
497
|
+
if (nextProjectConfig.implementation) {
|
|
498
|
+
writeTemplateTrustRecord(options.projectRoot, nextProjectConfig);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return {
|
|
502
|
+
...plan,
|
|
503
|
+
ok: true,
|
|
504
|
+
mode: "apply",
|
|
505
|
+
writes: applied.length > 0,
|
|
506
|
+
issues,
|
|
507
|
+
diagnostics,
|
|
508
|
+
applied,
|
|
509
|
+
skipped,
|
|
510
|
+
conflicts
|
|
511
|
+
};
|
|
512
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export interface CreateNewProjectOptions {
|
|
2
|
+
targetPath: string;
|
|
3
|
+
templateName?: string;
|
|
4
|
+
engineRoot: string;
|
|
5
|
+
templatesRoot: string;
|
|
6
|
+
templateProvenance?: CatalogTemplateProvenance | null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface TemplateUpdatePlanOptions {
|
|
10
|
+
projectRoot: string;
|
|
11
|
+
projectConfig: Record<string, any>;
|
|
12
|
+
templateName?: string | null;
|
|
13
|
+
templatesRoot: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type TemplateUpdateFileActionOptions = TemplateUpdatePlanOptions & {
|
|
17
|
+
filePath: string;
|
|
18
|
+
action: "accept-current" | "accept-candidate" | "delete-current";
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export interface TemplateOwnedFileRecord {
|
|
22
|
+
path: string;
|
|
23
|
+
sha256: string;
|
|
24
|
+
size: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface TemplateManifest {
|
|
28
|
+
id: string;
|
|
29
|
+
version: string;
|
|
30
|
+
kind: string;
|
|
31
|
+
topogramVersion: string;
|
|
32
|
+
includesExecutableImplementation?: boolean;
|
|
33
|
+
description?: string;
|
|
34
|
+
starterScripts?: Record<string, string>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface TemplateTopologySummary {
|
|
38
|
+
surfaces: string[];
|
|
39
|
+
generators: string[];
|
|
40
|
+
stack: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface TemplatePolicy {
|
|
44
|
+
version: string;
|
|
45
|
+
allowedSources: Array<"local" | "package">;
|
|
46
|
+
allowedTemplateIds: string[];
|
|
47
|
+
allowedPackageScopes?: string[];
|
|
48
|
+
executableImplementation: "allow" | "warn" | "deny";
|
|
49
|
+
pinnedVersions?: Record<string, string>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface TemplatePolicyInfo {
|
|
53
|
+
path: string;
|
|
54
|
+
policy: TemplatePolicy | null;
|
|
55
|
+
exists: boolean;
|
|
56
|
+
diagnostics: TemplateUpdateDiagnostic[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface TemplateUpdateDiagnostic {
|
|
60
|
+
code: string;
|
|
61
|
+
severity: "error" | "warning";
|
|
62
|
+
message: string;
|
|
63
|
+
path: string | null;
|
|
64
|
+
suggestedFix: string | null;
|
|
65
|
+
step: string | null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface ResolvedTemplate {
|
|
69
|
+
requested: string;
|
|
70
|
+
root: string;
|
|
71
|
+
manifest: TemplateManifest;
|
|
72
|
+
source: "local" | "package";
|
|
73
|
+
packageSpec: string | null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface CatalogTemplateProvenance {
|
|
77
|
+
id: string;
|
|
78
|
+
source: string;
|
|
79
|
+
package: string;
|
|
80
|
+
version: string;
|
|
81
|
+
packageSpec: string;
|
|
82
|
+
includesExecutableImplementation?: boolean;
|
|
83
|
+
}
|