@topogram/cli 0.3.64 → 0.3.66
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/adoption/plan/index.js +716 -0
- package/src/adoption/plan.js +12 -703
- package/src/adoption/reporting.js +1 -1
- package/src/agent-brief.js +7 -21
- 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 +677 -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/archive/jsonl.js +2 -2
- package/src/archive/resolver-bridge.js +1 -1
- package/src/archive/unarchive.js +2 -1
- package/src/catalog/constants.js +10 -0
- package/src/catalog/copy.js +65 -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 +123 -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/command-parsers/project.js +3 -0
- package/src/cli/command-parsers/shared.js +1 -1
- package/src/cli/commands/agent.js +2 -2
- 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/check.js +3 -3
- package/src/cli/commands/doctor.js +2 -9
- 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 +269 -0
- package/src/cli/commands/import/plan.js +292 -0
- package/src/cli/commands/import/refresh.js +471 -0
- package/src/cli/commands/import/status-history.js +196 -0
- package/src/cli/commands/import/workspace.js +233 -0
- package/src/cli/commands/import.js +33 -1732
- package/src/cli/commands/migrate.js +153 -0
- 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 +270 -0
- package/src/cli/commands/query.js +9 -1300
- package/src/cli/commands/source.js +3 -12
- package/src/cli/commands/template/baseline.js +100 -0
- package/src/cli/commands/template/check.js +467 -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-runner.js +6 -6
- package/src/cli/commands/template.js +41 -2143
- package/src/cli/commands/trust.js +1 -1
- package/src/cli/commands/workflow.js +6 -1
- package/src/cli/dispatcher.js +6 -1
- package/src/cli/help.js +15 -14
- package/src/cli/migration-guidance.js +1 -1
- package/src/cli/output-safety.js +2 -1
- package/src/cli/path-normalization.js +3 -13
- 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/domain-page.js +1 -1
- 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/context/task-mode.js +2 -2
- 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/sdlc/doc-page.js +1 -1
- package/src/generator/shared.d.ts +2 -0
- package/src/generator/surfaces/databases/lifecycle-shared.js +1 -1
- package/src/generator/surfaces/native/swiftui-templates/README.generated.md +1 -1
- 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/context.js +5 -7
- 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 +337 -0
- package/src/import/core/runner/options.js +22 -0
- package/src/import/core/runner/reports.js +51 -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 +393 -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 +90 -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 +351 -0
- package/src/new-project/template-policy.js +269 -0
- package/src/new-project/template-resolution.js +370 -0
- package/src/new-project/template-snapshots.js +442 -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 +591 -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/sdlc/adopt.js +6 -5
- package/src/sdlc/paths.js +3 -5
- package/src/sdlc/scaffold.js +2 -1
- 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 +212 -0
- package/src/workflows/reconcile/adoption-plan/dependencies.js +75 -0
- package/src/workflows/reconcile/adoption-plan/outputs.js +153 -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/candidate-model.js +18 -2
- package/src/workflows/reconcile/canonical-surface.js +1 -1
- package/src/workflows/reconcile/impacts/adoption-plan.js +196 -0
- package/src/workflows/reconcile/impacts/indexes.js +105 -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/workflows/reconcile/renderers.js +41 -6
- package/src/workflows/shared.js +5 -11
- package/src/workspace-docs.d.ts +29 -0
- package/src/workspace-paths.js +328 -0
package/src/catalog.js
CHANGED
|
@@ -1,748 +1,20 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const source = process.argv[1];
|
|
22
|
-
const token = process.env.TOPOGRAM_FETCH_TOKEN || "";
|
|
23
|
-
const tokenHosts = new Set(["github.com", "api.github.com", "raw.githubusercontent.com"]);
|
|
24
|
-
function tokenAllowed(url) {
|
|
25
|
-
const hostname = new URL(url).hostname.toLowerCase();
|
|
26
|
-
return tokenHosts.has(hostname) || hostname.endsWith(".github.com");
|
|
27
|
-
}
|
|
28
|
-
async function readUrl(url, redirects = 0) {
|
|
29
|
-
if (redirects > 5) {
|
|
30
|
-
throw new Error("Too many redirects.");
|
|
31
|
-
}
|
|
32
|
-
const headers = {};
|
|
33
|
-
if (token && tokenAllowed(url)) {
|
|
34
|
-
headers.authorization = "Bearer " + token;
|
|
35
|
-
}
|
|
36
|
-
const response = await fetch(url, { headers, redirect: "manual" });
|
|
37
|
-
if (response.status >= 300 && response.status < 400 && response.headers.get("location")) {
|
|
38
|
-
const next = new URL(response.headers.get("location"), url).toString();
|
|
39
|
-
return readUrl(next, redirects + 1);
|
|
40
|
-
}
|
|
41
|
-
const text = await response.text();
|
|
42
|
-
if (!response.ok) {
|
|
43
|
-
const preview = text.trim().slice(0, 400);
|
|
44
|
-
throw new Error(String(response.status) + " " + response.statusText + (preview ? "\\n" + preview : ""));
|
|
45
|
-
}
|
|
46
|
-
return text;
|
|
47
|
-
}
|
|
48
|
-
try {
|
|
49
|
-
process.stdout.write(await readUrl(source));
|
|
50
|
-
} catch (error) {
|
|
51
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
52
|
-
process.exit(1);
|
|
53
|
-
}
|
|
54
|
-
`;
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* @typedef {Object} CatalogTrust
|
|
58
|
-
* @property {string} scope
|
|
59
|
-
* @property {boolean} includesExecutableImplementation
|
|
60
|
-
* @property {string} [notes]
|
|
61
|
-
*/
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* @typedef {Object} CatalogEntry
|
|
65
|
-
* @property {string} id
|
|
66
|
-
* @property {"template"|"topogram"} kind
|
|
67
|
-
* @property {string} package
|
|
68
|
-
* @property {string} defaultVersion
|
|
69
|
-
* @property {string} description
|
|
70
|
-
* @property {string[]} tags
|
|
71
|
-
* @property {string[]} [surfaces]
|
|
72
|
-
* @property {string[]} [generators]
|
|
73
|
-
* @property {string} [stack]
|
|
74
|
-
* @property {CatalogTrust} trust
|
|
75
|
-
*/
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* @typedef {Object} TopogramCatalog
|
|
79
|
-
* @property {string} version
|
|
80
|
-
* @property {CatalogEntry[]} entries
|
|
81
|
-
*/
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* @typedef {Object} CatalogDiagnostic
|
|
85
|
-
* @property {string} code
|
|
86
|
-
* @property {"error"|"warning"} severity
|
|
87
|
-
* @property {string} message
|
|
88
|
-
* @property {string|null} path
|
|
89
|
-
* @property {string|null} suggestedFix
|
|
90
|
-
*/
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* @typedef {Object} CatalogValidationResult
|
|
94
|
-
* @property {boolean} ok
|
|
95
|
-
* @property {TopogramCatalog|null} catalog
|
|
96
|
-
* @property {CatalogDiagnostic[]} diagnostics
|
|
97
|
-
* @property {string[]} errors
|
|
98
|
-
*/
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* @typedef {Object} CatalogLoadResult
|
|
102
|
-
* @property {string} source
|
|
103
|
-
* @property {TopogramCatalog} catalog
|
|
104
|
-
* @property {CatalogDiagnostic[]} diagnostics
|
|
105
|
-
*/
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* @param {Record<string, unknown>} input
|
|
109
|
-
* @returns {CatalogDiagnostic}
|
|
110
|
-
*/
|
|
111
|
-
function catalogDiagnostic(input) {
|
|
112
|
-
return {
|
|
113
|
-
code: String(input.code || "catalog_invalid"),
|
|
114
|
-
severity: input.severity === "warning" ? "warning" : "error",
|
|
115
|
-
message: String(input.message || "Catalog is invalid."),
|
|
116
|
-
path: typeof input.path === "string" ? input.path : null,
|
|
117
|
-
suggestedFix: typeof input.suggestedFix === "string" ? input.suggestedFix : null
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* @param {string|undefined|null} source
|
|
123
|
-
* @returns {string}
|
|
124
|
-
*/
|
|
125
|
-
export function catalogSourceOrDefault(source = null) {
|
|
126
|
-
return source || process.env.TOPOGRAM_CATALOG_SOURCE || defaultCatalogSource();
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* @param {string|undefined|null} source
|
|
131
|
-
* @returns {boolean}
|
|
132
|
-
*/
|
|
133
|
-
export function isCatalogSourceDisabled(source) {
|
|
134
|
-
const normalized = String(source || "").trim().toLowerCase();
|
|
135
|
-
return normalized === "none" || normalized === "off" || normalized === "false";
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* @param {string} value
|
|
140
|
-
* @returns {boolean}
|
|
141
|
-
*/
|
|
142
|
-
function isPackageName(value) {
|
|
143
|
-
return /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i.test(value);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* @param {unknown} value
|
|
148
|
-
* @param {string} source
|
|
149
|
-
* @returns {CatalogValidationResult}
|
|
150
|
-
*/
|
|
151
|
-
export function validateCatalog(value, source = "") {
|
|
152
|
-
/** @type {CatalogDiagnostic[]} */
|
|
153
|
-
const diagnostics = [];
|
|
154
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
155
|
-
diagnostics.push(catalogDiagnostic({
|
|
156
|
-
code: "catalog_not_object",
|
|
157
|
-
message: "Catalog must contain a JSON object.",
|
|
158
|
-
path: source || null,
|
|
159
|
-
suggestedFix: `Create ${CATALOG_FILE_NAME} with version and entries[].`
|
|
160
|
-
}));
|
|
161
|
-
return validationResult(null, diagnostics);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const input = /** @type {Record<string, unknown>} */ (value);
|
|
165
|
-
const version = typeof input.version === "string" && input.version ? input.version : "";
|
|
166
|
-
if (!version) {
|
|
167
|
-
diagnostics.push(catalogDiagnostic({
|
|
168
|
-
code: "catalog_version_missing",
|
|
169
|
-
message: "Catalog is missing required string field 'version'.",
|
|
170
|
-
path: source || null,
|
|
171
|
-
suggestedFix: "Add a version string such as \"0.1\"."
|
|
172
|
-
}));
|
|
173
|
-
}
|
|
174
|
-
if (!Array.isArray(input.entries)) {
|
|
175
|
-
diagnostics.push(catalogDiagnostic({
|
|
176
|
-
code: "catalog_entries_missing",
|
|
177
|
-
message: "Catalog is missing required array field 'entries'.",
|
|
178
|
-
path: source || null,
|
|
179
|
-
suggestedFix: "Add entries[] with template and topogram package references."
|
|
180
|
-
}));
|
|
181
|
-
return validationResult({ version, entries: [] }, diagnostics);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/** @type {CatalogEntry[]} */
|
|
185
|
-
const entries = [];
|
|
186
|
-
const ids = new Set();
|
|
187
|
-
input.entries.forEach((entryValue, index) => {
|
|
188
|
-
const entryPath = `entries[${index}]`;
|
|
189
|
-
if (!entryValue || typeof entryValue !== "object" || Array.isArray(entryValue)) {
|
|
190
|
-
diagnostics.push(catalogDiagnostic({
|
|
191
|
-
code: "catalog_entry_not_object",
|
|
192
|
-
message: `Catalog ${entryPath} must be an object.`,
|
|
193
|
-
path: source || null,
|
|
194
|
-
suggestedFix: "Replace the entry with an object containing id, kind, package, defaultVersion, description, tags, and trust."
|
|
195
|
-
}));
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
const entry = /** @type {Record<string, unknown>} */ (entryValue);
|
|
199
|
-
const id = stringField(entry, "id");
|
|
200
|
-
const kind = stringField(entry, "kind");
|
|
201
|
-
const packageName = stringField(entry, "package");
|
|
202
|
-
const defaultVersion = stringField(entry, "defaultVersion");
|
|
203
|
-
const description = stringField(entry, "description");
|
|
204
|
-
const tags = Array.isArray(entry.tags) ? entry.tags.map(String).filter(Boolean) : [];
|
|
205
|
-
const surfaces = Array.isArray(entry.surfaces) ? entry.surfaces.map(String).filter(Boolean) : [];
|
|
206
|
-
const generators = Array.isArray(entry.generators) ? entry.generators.map(String).filter(Boolean) : [];
|
|
207
|
-
const stack = stringField(entry, "stack");
|
|
208
|
-
const trust = trustField(entry.trust);
|
|
209
|
-
|
|
210
|
-
for (const field of ["id", "kind", "package", "defaultVersion", "description"]) {
|
|
211
|
-
if (!stringField(entry, field)) {
|
|
212
|
-
diagnostics.push(catalogDiagnostic({
|
|
213
|
-
code: "catalog_entry_field_missing",
|
|
214
|
-
message: `Catalog ${entryPath} is missing required string field '${field}'.`,
|
|
215
|
-
path: source || null,
|
|
216
|
-
suggestedFix: `Add ${field} to ${entryPath}.`
|
|
217
|
-
}));
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
if (id && ids.has(id)) {
|
|
221
|
-
diagnostics.push(catalogDiagnostic({
|
|
222
|
-
code: "catalog_duplicate_id",
|
|
223
|
-
message: `Catalog entry id '${id}' is duplicated.`,
|
|
224
|
-
path: source || null,
|
|
225
|
-
suggestedFix: "Use stable unique ids for catalog entries."
|
|
226
|
-
}));
|
|
227
|
-
}
|
|
228
|
-
if (id) {
|
|
229
|
-
ids.add(id);
|
|
230
|
-
}
|
|
231
|
-
if (kind && kind !== "template" && kind !== "topogram") {
|
|
232
|
-
diagnostics.push(catalogDiagnostic({
|
|
233
|
-
code: "catalog_invalid_kind",
|
|
234
|
-
message: `Catalog entry '${id || entryPath}' has invalid kind '${kind}'.`,
|
|
235
|
-
path: source || null,
|
|
236
|
-
suggestedFix: "Use kind \"template\" or \"topogram\"."
|
|
237
|
-
}));
|
|
238
|
-
}
|
|
239
|
-
if (packageName && !isPackageName(packageName)) {
|
|
240
|
-
diagnostics.push(catalogDiagnostic({
|
|
241
|
-
code: "catalog_invalid_package",
|
|
242
|
-
message: `Catalog entry '${id || entryPath}' package must be an npm package name, not '${packageName}'.`,
|
|
243
|
-
path: source || null,
|
|
244
|
-
suggestedFix: "Use package plus defaultVersion separately, for example @scope/topogram-template-name and 0.1.0."
|
|
245
|
-
}));
|
|
246
|
-
}
|
|
247
|
-
if (defaultVersion && /\s/.test(defaultVersion)) {
|
|
248
|
-
diagnostics.push(catalogDiagnostic({
|
|
249
|
-
code: "catalog_invalid_default_version",
|
|
250
|
-
message: `Catalog entry '${id || entryPath}' defaultVersion must not contain whitespace.`,
|
|
251
|
-
path: source || null,
|
|
252
|
-
suggestedFix: "Use an exact version or npm dist-tag."
|
|
253
|
-
}));
|
|
254
|
-
}
|
|
255
|
-
if (!Array.isArray(entry.tags)) {
|
|
256
|
-
diagnostics.push(catalogDiagnostic({
|
|
257
|
-
code: "catalog_tags_missing",
|
|
258
|
-
message: `Catalog entry '${id || entryPath}' is missing required tags array.`,
|
|
259
|
-
path: source || null,
|
|
260
|
-
suggestedFix: "Add tags as an array of strings."
|
|
261
|
-
}));
|
|
262
|
-
}
|
|
263
|
-
if (Object.prototype.hasOwnProperty.call(entry, "surfaces") && !Array.isArray(entry.surfaces)) {
|
|
264
|
-
diagnostics.push(catalogDiagnostic({
|
|
265
|
-
code: "catalog_optional_surfaces_invalid",
|
|
266
|
-
severity: "warning",
|
|
267
|
-
message: `Catalog entry '${id || entryPath}' surfaces should be an array of surface ids.`,
|
|
268
|
-
path: source || null,
|
|
269
|
-
suggestedFix: "Use surfaces such as [\"web\"], [\"api\"], [\"database\"], or [\"native\"]."
|
|
270
|
-
}));
|
|
271
|
-
}
|
|
272
|
-
for (const surface of surfaces) {
|
|
273
|
-
if (!KNOWN_CATALOG_SURFACES.has(surface)) {
|
|
274
|
-
diagnostics.push(catalogDiagnostic({
|
|
275
|
-
code: "catalog_optional_surface_unknown",
|
|
276
|
-
severity: "warning",
|
|
277
|
-
message: `Catalog entry '${id || entryPath}' has unknown surface '${surface}'.`,
|
|
278
|
-
path: source || null,
|
|
279
|
-
suggestedFix: "Use known surface ids: web, api, database, native."
|
|
280
|
-
}));
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
if (Object.prototype.hasOwnProperty.call(entry, "generators") && !Array.isArray(entry.generators)) {
|
|
284
|
-
diagnostics.push(catalogDiagnostic({
|
|
285
|
-
code: "catalog_optional_generators_invalid",
|
|
286
|
-
severity: "warning",
|
|
287
|
-
message: `Catalog entry '${id || entryPath}' generators should be an array of generator ids.`,
|
|
288
|
-
path: source || null,
|
|
289
|
-
suggestedFix: "Use package-backed generator ids such as [\"@topogram/generator-sveltekit-web\", \"@topogram/generator-hono-api\"]."
|
|
290
|
-
}));
|
|
291
|
-
}
|
|
292
|
-
if (Object.prototype.hasOwnProperty.call(entry, "stack") && typeof entry.stack !== "string") {
|
|
293
|
-
diagnostics.push(catalogDiagnostic({
|
|
294
|
-
code: "catalog_optional_stack_invalid",
|
|
295
|
-
severity: "warning",
|
|
296
|
-
message: `Catalog entry '${id || entryPath}' stack should be a string.`,
|
|
297
|
-
path: source || null,
|
|
298
|
-
suggestedFix: "Use a short stack label such as \"SvelteKit + Hono + Postgres\"."
|
|
299
|
-
}));
|
|
300
|
-
}
|
|
301
|
-
if (!trust) {
|
|
302
|
-
diagnostics.push(catalogDiagnostic({
|
|
303
|
-
code: "catalog_trust_missing",
|
|
304
|
-
message: `Catalog entry '${id || entryPath}' is missing required trust metadata.`,
|
|
305
|
-
path: source || null,
|
|
306
|
-
suggestedFix: "Add trust.scope and trust.includesExecutableImplementation."
|
|
307
|
-
}));
|
|
308
|
-
} else if (kind === "topogram" && trust.includesExecutableImplementation) {
|
|
309
|
-
diagnostics.push(catalogDiagnostic({
|
|
310
|
-
code: "catalog_topogram_executable_not_supported",
|
|
311
|
-
message: `Catalog topogram entry '${id || entryPath}' cannot include executable implementation in v1.`,
|
|
312
|
-
path: source || null,
|
|
313
|
-
suggestedFix: "Move executable code into a template package, or set includesExecutableImplementation to false."
|
|
314
|
-
}));
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
entries.push({
|
|
318
|
-
id,
|
|
319
|
-
kind: kind === "topogram" ? "topogram" : "template",
|
|
320
|
-
package: packageName,
|
|
321
|
-
defaultVersion,
|
|
322
|
-
description,
|
|
323
|
-
tags,
|
|
324
|
-
...(surfaces.length > 0 ? { surfaces } : {}),
|
|
325
|
-
...(generators.length > 0 ? { generators } : {}),
|
|
326
|
-
...(stack ? { stack } : {}),
|
|
327
|
-
trust: trust || { scope: "", includesExecutableImplementation: false }
|
|
328
|
-
});
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
return validationResult({ version, entries }, diagnostics);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* @param {TopogramCatalog|null} catalog
|
|
336
|
-
* @param {CatalogDiagnostic[]} diagnostics
|
|
337
|
-
* @returns {CatalogValidationResult}
|
|
338
|
-
*/
|
|
339
|
-
function validationResult(catalog, diagnostics) {
|
|
340
|
-
const errors = diagnostics
|
|
341
|
-
.filter((diagnostic) => diagnostic.severity === "error")
|
|
342
|
-
.map((diagnostic) => diagnostic.message);
|
|
343
|
-
return {
|
|
344
|
-
ok: errors.length === 0,
|
|
345
|
-
catalog: errors.length === 0 ? catalog : null,
|
|
346
|
-
diagnostics,
|
|
347
|
-
errors
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* @param {Record<string, unknown>} input
|
|
353
|
-
* @param {string} field
|
|
354
|
-
* @returns {string}
|
|
355
|
-
*/
|
|
356
|
-
function stringField(input, field) {
|
|
357
|
-
const value = input[field];
|
|
358
|
-
return typeof value === "string" ? value.trim() : "";
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
/**
|
|
362
|
-
* @param {unknown} value
|
|
363
|
-
* @returns {CatalogTrust|null}
|
|
364
|
-
*/
|
|
365
|
-
function trustField(value) {
|
|
366
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
367
|
-
return null;
|
|
368
|
-
}
|
|
369
|
-
const trust = /** @type {Record<string, unknown>} */ (value);
|
|
370
|
-
if (typeof trust.scope !== "string" || !trust.scope) {
|
|
371
|
-
return null;
|
|
372
|
-
}
|
|
373
|
-
if (typeof trust.includesExecutableImplementation !== "boolean") {
|
|
374
|
-
return null;
|
|
375
|
-
}
|
|
376
|
-
const result = {
|
|
377
|
-
scope: trust.scope,
|
|
378
|
-
includesExecutableImplementation: trust.includesExecutableImplementation
|
|
379
|
-
};
|
|
380
|
-
if (typeof trust.notes === "string" && trust.notes) {
|
|
381
|
-
return { ...result, notes: trust.notes };
|
|
382
|
-
}
|
|
383
|
-
return result;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
/**
|
|
387
|
-
* @param {string|undefined|null} sourceInput
|
|
388
|
-
* @returns {CatalogLoadResult}
|
|
389
|
-
*/
|
|
390
|
-
export function loadCatalog(sourceInput = null) {
|
|
391
|
-
const source = catalogSourceOrDefault(sourceInput);
|
|
392
|
-
if (isCatalogSourceDisabled(source)) {
|
|
393
|
-
throw new Error("Catalog source is disabled.");
|
|
394
|
-
}
|
|
395
|
-
const text = readCatalogText(source);
|
|
396
|
-
const parsed = JSON.parse(text);
|
|
397
|
-
const validation = validateCatalog(parsed, source);
|
|
398
|
-
if (!validation.ok || !validation.catalog) {
|
|
399
|
-
throw new Error(validation.errors.join("\n") || `Catalog '${source}' is invalid.`);
|
|
400
|
-
}
|
|
401
|
-
return {
|
|
402
|
-
source,
|
|
403
|
-
catalog: validation.catalog,
|
|
404
|
-
diagnostics: validation.diagnostics
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
/**
|
|
409
|
-
* @param {string} source
|
|
410
|
-
* @returns {CatalogValidationResult & { source: string }}
|
|
411
|
-
*/
|
|
412
|
-
export function checkCatalogSource(source) {
|
|
413
|
-
const text = readCatalogText(source);
|
|
414
|
-
const parsed = JSON.parse(text);
|
|
415
|
-
return {
|
|
416
|
-
source,
|
|
417
|
-
...validateCatalog(parsed, source)
|
|
418
|
-
};
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* @param {string} source
|
|
423
|
-
* @returns {string}
|
|
424
|
-
*/
|
|
425
|
-
function readCatalogText(source) {
|
|
426
|
-
if (source.startsWith("github:")) {
|
|
427
|
-
return readGithubCatalogText(source);
|
|
428
|
-
}
|
|
429
|
-
if (source.startsWith("https://") || source.startsWith("http://")) {
|
|
430
|
-
return readUrlText(source);
|
|
431
|
-
}
|
|
432
|
-
const resolvedPath = path.resolve(source);
|
|
433
|
-
return fs.readFileSync(resolvedPath, "utf8");
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/**
|
|
437
|
-
* @param {string} source
|
|
438
|
-
* @returns {string}
|
|
439
|
-
*/
|
|
440
|
-
function readGithubCatalogText(source) {
|
|
441
|
-
return readGithubCatalogSourceText(source);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
/**
|
|
445
|
-
* @param {string} source
|
|
446
|
-
* @returns {string}
|
|
447
|
-
*/
|
|
448
|
-
function readUrlText(source) {
|
|
449
|
-
const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN || "";
|
|
450
|
-
const tokenEnv = token && githubTokenAllowedForCatalogUrl(source)
|
|
451
|
-
? { TOPOGRAM_FETCH_TOKEN: token }
|
|
452
|
-
: {};
|
|
453
|
-
const result = childProcess.spawnSync(process.execPath, ["--input-type=module", "-e", FETCH_URL_SCRIPT, source], {
|
|
454
|
-
encoding: "utf8",
|
|
455
|
-
env: {
|
|
456
|
-
...process.env,
|
|
457
|
-
...tokenEnv,
|
|
458
|
-
PATH: process.env.PATH || ""
|
|
459
|
-
}
|
|
460
|
-
});
|
|
461
|
-
if (result.status !== 0) {
|
|
462
|
-
const reason = result.error?.message || result.stderr || result.stdout || "unknown error";
|
|
463
|
-
throw new Error(`Failed to read catalog URL '${source}'.\n${reason}`.trim());
|
|
464
|
-
}
|
|
465
|
-
return result.stdout;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
/**
|
|
469
|
-
* @param {string} source
|
|
470
|
-
* @returns {boolean}
|
|
471
|
-
*/
|
|
472
|
-
function githubTokenAllowedForCatalogUrl(source) {
|
|
473
|
-
try {
|
|
474
|
-
const hostname = new URL(source).hostname.toLowerCase();
|
|
475
|
-
return GITHUB_TOKEN_HOSTS.has(hostname) || hostname.endsWith(".github.com");
|
|
476
|
-
} catch {
|
|
477
|
-
return false;
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
/**
|
|
482
|
-
* @param {TopogramCatalog} catalog
|
|
483
|
-
* @param {string} id
|
|
484
|
-
* @param {"template"|"topogram"|null} [kind]
|
|
485
|
-
* @returns {CatalogEntry|null}
|
|
486
|
-
*/
|
|
487
|
-
export function findCatalogEntry(catalog, id, kind = null) {
|
|
488
|
-
return catalog.entries.find((entry) => entry.id === id && (!kind || entry.kind === kind)) || null;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
/**
|
|
492
|
-
* @param {CatalogEntry} entry
|
|
493
|
-
* @param {string|null|undefined} version
|
|
494
|
-
* @returns {string}
|
|
495
|
-
*/
|
|
496
|
-
export function catalogEntryPackageSpec(entry, version = null) {
|
|
497
|
-
return `${entry.package}@${version || entry.defaultVersion}`;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
/**
|
|
501
|
-
* @param {CatalogEntry} entry
|
|
502
|
-
* @returns {{ id: string, version: string, source: "catalog", name: string, package: string, defaultVersion: string, description: string, tags: string[], surfaces?: string[], generators?: string[], stack?: string, includesExecutableImplementation: boolean, trust: CatalogTrust }}
|
|
503
|
-
*/
|
|
504
|
-
export function catalogTemplateListItem(entry) {
|
|
505
|
-
return {
|
|
506
|
-
id: entry.id,
|
|
507
|
-
version: entry.defaultVersion,
|
|
508
|
-
source: "catalog",
|
|
509
|
-
name: entry.id,
|
|
510
|
-
package: entry.package,
|
|
511
|
-
defaultVersion: entry.defaultVersion,
|
|
512
|
-
description: entry.description,
|
|
513
|
-
tags: entry.tags,
|
|
514
|
-
...(entry.surfaces ? { surfaces: entry.surfaces } : {}),
|
|
515
|
-
...(entry.generators ? { generators: entry.generators } : {}),
|
|
516
|
-
...(entry.stack ? { stack: entry.stack } : {}),
|
|
517
|
-
includesExecutableImplementation: entry.trust.includesExecutableImplementation,
|
|
518
|
-
trust: entry.trust
|
|
519
|
-
};
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
/**
|
|
523
|
-
* @param {CatalogEntry} entry
|
|
524
|
-
* @param {string} targetPath
|
|
525
|
-
* @param {{ version?: string|null, catalogSource?: string|null }} [options]
|
|
526
|
-
* @returns {{ ok: boolean, id: string, kind: "topogram", packageSpec: string, targetPath: string, provenancePath: string, files: string[] }}
|
|
527
|
-
*/
|
|
528
|
-
export function copyCatalogTopogramEntry(entry, targetPath, options = {}) {
|
|
529
|
-
if (entry.kind !== "topogram") {
|
|
530
|
-
throw new Error(`Catalog entry '${entry.id}' is a ${entry.kind}, not a topogram.`);
|
|
531
|
-
}
|
|
532
|
-
const packageSpec = catalogEntryPackageSpec(entry, options.version || null);
|
|
533
|
-
const packageRoot = installPackageSpec(packageSpec);
|
|
534
|
-
const implementationRoot = path.join(packageRoot, "implementation");
|
|
535
|
-
if (fs.existsSync(implementationRoot)) {
|
|
536
|
-
throw new Error(
|
|
537
|
-
`Catalog topogram entry '${entry.id}' package '${packageSpec}' contains implementation/, which is not allowed for v1 topogram entries.`
|
|
538
|
-
);
|
|
539
|
-
}
|
|
540
|
-
const topogramRoot = path.join(packageRoot, "topogram");
|
|
541
|
-
if (!fs.existsSync(topogramRoot) || !fs.statSync(topogramRoot).isDirectory()) {
|
|
542
|
-
throw new Error(`Catalog topogram entry '${entry.id}' package '${packageSpec}' is missing topogram/.`);
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
const resolvedTarget = path.resolve(targetPath);
|
|
546
|
-
ensureEmptyDirectory(resolvedTarget);
|
|
547
|
-
/** @type {string[]} */
|
|
548
|
-
const files = [];
|
|
549
|
-
copyPath(topogramRoot, path.join(resolvedTarget, "topogram"), "topogram", files);
|
|
550
|
-
for (const fileName of ["topogram.project.json", "README.md"]) {
|
|
551
|
-
const sourcePath = path.join(packageRoot, fileName);
|
|
552
|
-
if (fs.existsSync(sourcePath) && fs.statSync(sourcePath).isFile()) {
|
|
553
|
-
copyPath(sourcePath, path.join(resolvedTarget, fileName), fileName, files);
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
const provenance = writeTopogramSourceRecord(resolvedTarget, {
|
|
557
|
-
catalogSource: options.catalogSource || null,
|
|
558
|
-
entry,
|
|
559
|
-
packageSpec,
|
|
560
|
-
version: options.version || entry.defaultVersion
|
|
561
|
-
});
|
|
562
|
-
return {
|
|
563
|
-
ok: true,
|
|
564
|
-
id: entry.id,
|
|
565
|
-
kind: "topogram",
|
|
566
|
-
packageSpec,
|
|
567
|
-
targetPath: resolvedTarget,
|
|
568
|
-
provenancePath: provenance.path,
|
|
569
|
-
files: files.sort((a, b) => a.localeCompare(b))
|
|
570
|
-
};
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
/**
|
|
574
|
-
* @param {string} projectRoot
|
|
575
|
-
* @param {{ catalogSource: string|null, entry: CatalogEntry, packageSpec: string, version: string }} input
|
|
576
|
-
* @returns {{ path: string, record: Record<string, any> }}
|
|
577
|
-
*/
|
|
578
|
-
function writeTopogramSourceRecord(projectRoot, input) {
|
|
579
|
-
const record = {
|
|
580
|
-
version: "0.1",
|
|
581
|
-
kind: "topogram",
|
|
582
|
-
copiedAt: new Date().toISOString(),
|
|
583
|
-
catalog: {
|
|
584
|
-
id: input.entry.id,
|
|
585
|
-
source: input.catalogSource
|
|
586
|
-
},
|
|
587
|
-
package: {
|
|
588
|
-
name: input.entry.package,
|
|
589
|
-
version: input.version,
|
|
590
|
-
spec: input.packageSpec
|
|
591
|
-
},
|
|
592
|
-
trust: {
|
|
593
|
-
includesExecutableImplementation: false
|
|
594
|
-
},
|
|
595
|
-
files: collectSourceFileRecords(projectRoot)
|
|
596
|
-
};
|
|
597
|
-
const sourcePath = path.join(projectRoot, TOPOGRAM_SOURCE_FILE);
|
|
598
|
-
fs.writeFileSync(sourcePath, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
599
|
-
return { path: sourcePath, record };
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
/**
|
|
603
|
-
* @param {string} projectRoot
|
|
604
|
-
* @returns {{ ok: true, exists: boolean, path: string, status: "missing"|"clean"|"changed", source: Record<string, any>|null, content: { changed: string[], added: string[], removed: string[] }, diagnostics: any[], errors: [] }}
|
|
605
|
-
*/
|
|
606
|
-
export function buildTopogramSourceStatus(projectRoot) {
|
|
607
|
-
const resolvedRoot = path.resolve(projectRoot);
|
|
608
|
-
const sourcePath = path.join(resolvedRoot, TOPOGRAM_SOURCE_FILE);
|
|
609
|
-
if (!fs.existsSync(sourcePath)) {
|
|
610
|
-
return {
|
|
611
|
-
ok: true,
|
|
612
|
-
exists: false,
|
|
613
|
-
path: sourcePath,
|
|
614
|
-
status: "missing",
|
|
615
|
-
source: null,
|
|
616
|
-
content: { changed: [], added: [], removed: [] },
|
|
617
|
-
diagnostics: [{
|
|
618
|
-
code: "topogram_source_missing",
|
|
619
|
-
severity: "warning",
|
|
620
|
-
message: `${TOPOGRAM_SOURCE_FILE} was not found. This project may not have been copied from a catalog topogram entry.`,
|
|
621
|
-
path: sourcePath,
|
|
622
|
-
suggestedFix: "Run `topogram catalog copy <id> <target>` to create a project with source provenance."
|
|
623
|
-
}],
|
|
624
|
-
errors: []
|
|
625
|
-
};
|
|
626
|
-
}
|
|
627
|
-
const source = JSON.parse(fs.readFileSync(sourcePath, "utf8"));
|
|
628
|
-
const trustedFiles = Array.isArray(source.files) ? source.files : [];
|
|
629
|
-
const trustedByPath = new Map(trustedFiles.map((file) => [String(file.path), file]));
|
|
630
|
-
const currentByPath = new Map(collectSourceFileRecords(resolvedRoot).map((file) => [file.path, file]));
|
|
631
|
-
/** @type {string[]} */
|
|
632
|
-
const changed = [];
|
|
633
|
-
/** @type {string[]} */
|
|
634
|
-
const added = [];
|
|
635
|
-
/** @type {string[]} */
|
|
636
|
-
const removed = [];
|
|
637
|
-
for (const [filePath, current] of currentByPath) {
|
|
638
|
-
const trusted = trustedByPath.get(filePath);
|
|
639
|
-
if (!trusted) {
|
|
640
|
-
added.push(filePath);
|
|
641
|
-
} else if (trusted.sha256 !== current.sha256 || trusted.size !== current.size) {
|
|
642
|
-
changed.push(filePath);
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
for (const filePath of trustedByPath.keys()) {
|
|
646
|
-
if (!currentByPath.has(filePath)) {
|
|
647
|
-
removed.push(filePath);
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
const content = {
|
|
651
|
-
changed: changed.sort((a, b) => a.localeCompare(b)),
|
|
652
|
-
added: added.sort((a, b) => a.localeCompare(b)),
|
|
653
|
-
removed: removed.sort((a, b) => a.localeCompare(b))
|
|
654
|
-
};
|
|
655
|
-
return {
|
|
656
|
-
ok: true,
|
|
657
|
-
exists: true,
|
|
658
|
-
path: sourcePath,
|
|
659
|
-
status: content.changed.length || content.added.length || content.removed.length ? "changed" : "clean",
|
|
660
|
-
source,
|
|
661
|
-
content,
|
|
662
|
-
diagnostics: [],
|
|
663
|
-
errors: []
|
|
664
|
-
};
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
/**
|
|
668
|
-
* @param {string} projectRoot
|
|
669
|
-
* @returns {Array<{ path: string, sha256: string, size: number }>}
|
|
670
|
-
*/
|
|
671
|
-
function collectSourceFileRecords(projectRoot) {
|
|
672
|
-
/** @type {string[]} */
|
|
673
|
-
const files = [];
|
|
674
|
-
for (const sourceRoot of ["topogram", "topogram.project.json", "README.md"]) {
|
|
675
|
-
const sourcePath = path.join(projectRoot, sourceRoot);
|
|
676
|
-
if (fs.existsSync(sourcePath)) {
|
|
677
|
-
collectFiles(sourcePath, sourceRoot, files);
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
return files
|
|
681
|
-
.sort((a, b) => a.localeCompare(b))
|
|
682
|
-
.map((relativePath) => ({
|
|
683
|
-
path: relativePath,
|
|
684
|
-
...fileHash(path.join(projectRoot, relativePath))
|
|
685
|
-
}));
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
/**
|
|
689
|
-
* @param {string} filePath
|
|
690
|
-
* @returns {{ sha256: string, size: number }}
|
|
691
|
-
*/
|
|
692
|
-
function fileHash(filePath) {
|
|
693
|
-
const bytes = fs.readFileSync(filePath);
|
|
694
|
-
return {
|
|
695
|
-
sha256: crypto.createHash("sha256").update(bytes).digest("hex"),
|
|
696
|
-
size: bytes.length
|
|
697
|
-
};
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
/**
|
|
701
|
-
* @param {string} targetPath
|
|
702
|
-
* @returns {void}
|
|
703
|
-
*/
|
|
704
|
-
function ensureEmptyDirectory(targetPath) {
|
|
705
|
-
if (!fs.existsSync(targetPath)) {
|
|
706
|
-
fs.mkdirSync(targetPath, { recursive: true });
|
|
707
|
-
return;
|
|
708
|
-
}
|
|
709
|
-
if (!fs.statSync(targetPath).isDirectory()) {
|
|
710
|
-
throw new Error(`Cannot copy catalog topogram into non-directory path '${targetPath}'.`);
|
|
711
|
-
}
|
|
712
|
-
const entries = fs.readdirSync(targetPath).filter((entry) => entry !== ".DS_Store");
|
|
713
|
-
if (entries.length > 0) {
|
|
714
|
-
throw new Error(`Refusing to copy catalog topogram into non-empty directory '${targetPath}'.`);
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
/**
|
|
719
|
-
* @param {string} sourcePath
|
|
720
|
-
* @param {string} targetPath
|
|
721
|
-
* @param {string} relativePath
|
|
722
|
-
* @param {string[]} files
|
|
723
|
-
* @returns {void}
|
|
724
|
-
*/
|
|
725
|
-
function copyPath(sourcePath, targetPath, relativePath, files) {
|
|
726
|
-
fs.cpSync(sourcePath, targetPath, { recursive: true });
|
|
727
|
-
collectFiles(targetPath, relativePath, files);
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
/**
|
|
731
|
-
* @param {string} currentPath
|
|
732
|
-
* @param {string} relativePath
|
|
733
|
-
* @param {string[]} files
|
|
734
|
-
* @returns {void}
|
|
735
|
-
*/
|
|
736
|
-
function collectFiles(currentPath, relativePath, files) {
|
|
737
|
-
const stat = fs.statSync(currentPath);
|
|
738
|
-
if (stat.isFile()) {
|
|
739
|
-
files.push(relativePath.replace(/\\/g, "/"));
|
|
740
|
-
return;
|
|
741
|
-
}
|
|
742
|
-
if (!stat.isDirectory()) {
|
|
743
|
-
return;
|
|
744
|
-
}
|
|
745
|
-
for (const entry of fs.readdirSync(currentPath)) {
|
|
746
|
-
collectFiles(path.join(currentPath, entry), path.join(relativePath, entry), files);
|
|
747
|
-
}
|
|
748
|
-
}
|
|
3
|
+
export {
|
|
4
|
+
CATALOG_FILE_NAME,
|
|
5
|
+
TOPOGRAM_SOURCE_FILE
|
|
6
|
+
} from "./catalog/constants.js";
|
|
7
|
+
export {
|
|
8
|
+
catalogEntryPackageSpec,
|
|
9
|
+
catalogTemplateListItem,
|
|
10
|
+
findCatalogEntry
|
|
11
|
+
} from "./catalog/entries.js";
|
|
12
|
+
export {
|
|
13
|
+
catalogSourceOrDefault,
|
|
14
|
+
checkCatalogSource,
|
|
15
|
+
isCatalogSourceDisabled,
|
|
16
|
+
loadCatalog
|
|
17
|
+
} from "./catalog/source.js";
|
|
18
|
+
export { buildTopogramSourceStatus } from "./catalog/provenance.js";
|
|
19
|
+
export { copyCatalogTopogramEntry } from "./catalog/copy.js";
|
|
20
|
+
export { validateCatalog } from "./catalog/validation.js";
|