@topogram/cli 0.3.78 → 0.3.80
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/CHANGELOG.md +20 -0
- package/package.json +2 -2
- package/src/agent-brief.js +29 -23
- package/src/agent-ops/query-builders/change-risk/{import-plan.js → extract-plan.js} +1 -1
- package/src/agent-ops/query-builders/change-risk/review-packets.js +5 -5
- package/src/agent-ops/query-builders/change-risk.js +1 -1
- package/src/agent-ops/query-builders/common.js +2 -2
- package/src/agent-ops/query-builders/multi-agent.js +1 -1
- package/src/agent-ops/query-builders/workflow-context-shared.js +4 -4
- package/src/catalog/provenance.js +1 -1
- package/src/cli/catalog-alias.d.ts +2 -0
- package/src/cli/catalog-alias.js +2 -2
- package/src/cli/command-parser.js +2 -0
- package/src/cli/command-parsers/core.js +9 -5
- package/src/cli/command-parsers/extractor.js +40 -0
- package/src/cli/command-parsers/import.js +11 -17
- package/src/cli/command-parsers/project.js +0 -3
- package/src/cli/commands/catalog/copy.js +3 -3
- package/src/cli/commands/catalog/help.js +1 -2
- package/src/cli/commands/catalog/list.js +7 -4
- package/src/cli/commands/catalog/show.js +4 -4
- package/src/cli/commands/copy.js +356 -0
- package/src/cli/commands/doctor.js +1 -1
- package/src/cli/commands/extractor.js +451 -0
- package/src/cli/commands/import/adopt.js +9 -9
- package/src/cli/commands/import/check.js +15 -15
- package/src/cli/commands/import/diff.js +6 -6
- package/src/cli/commands/import/help.js +45 -34
- package/src/cli/commands/import/paths.js +3 -3
- package/src/cli/commands/import/plan.js +8 -8
- package/src/cli/commands/import/refresh.js +25 -24
- package/src/cli/commands/import/status-history.js +4 -4
- package/src/cli/commands/import/workspace.js +24 -18
- package/src/cli/commands/import-runner.js +10 -7
- package/src/cli/commands/import.js +4 -1
- package/src/cli/commands/init.js +67 -0
- package/src/cli/commands/query/{import-adopt.js → extract-adopt.js} +2 -2
- package/src/cli/commands/query/runner/change.js +2 -2
- package/src/cli/commands/query/runner/{import-adopt.js → extract-adopt.js} +9 -9
- package/src/cli/commands/query/runner/index.js +1 -1
- package/src/cli/commands/query/runner/workflow.js +7 -7
- package/src/cli/commands/query/workspace.js +4 -4
- package/src/cli/commands/release-status.js +2 -2
- package/src/cli/commands/source.js +2 -2
- package/src/cli/commands/template/check.js +2 -2
- package/src/cli/commands/template/list-show.js +4 -4
- package/src/cli/dispatcher.js +32 -3
- package/src/cli/help-dispatch.js +33 -8
- package/src/cli/help.js +79 -52
- package/src/cli/migration-guidance.js +9 -0
- package/src/cli/options.js +17 -0
- package/src/extractor/check.js +155 -0
- package/src/extractor/packages.js +295 -0
- package/src/extractor/registry.js +196 -0
- package/src/extractor-policy.js +249 -0
- package/src/generator/check.js +24 -87
- package/src/generator/context/bundle.js +14 -7
- package/src/generator/context/diff.js +8 -1
- package/src/generator/context/digest.js +10 -1
- package/src/generator/context/shared/domain-sdlc.js +5 -1
- package/src/generator/context/shared/relationships.js +20 -5
- package/src/generator/context/shared/summaries.js +26 -0
- package/src/generator/context/shared.d.ts +1 -0
- package/src/generator/context/shared.js +1 -0
- package/src/generator/context/slice/core.js +9 -5
- package/src/generator/context/slice/sdlc.js +31 -2
- package/src/generator/context/task-mode.js +3 -3
- package/src/generator/registry/index.js +16 -75
- package/src/generator-policy.js +9 -57
- package/src/import/core/registry.d.ts +3 -0
- package/src/import/core/registry.js +82 -8
- package/src/import/core/runner/reports.js +4 -4
- package/src/import/core/runner/run.js +2 -0
- package/src/import/core/runner/tracks.js +66 -4
- package/src/import/provenance.js +18 -17
- package/src/init-project.js +215 -0
- package/src/new-project/constants.js +1 -1
- package/src/new-project/create.js +2 -2
- package/src/new-project/project-files.js +7 -7
- package/src/package-adapters/adapter.js +64 -0
- package/src/package-adapters/file-map.js +30 -0
- package/src/package-adapters/index.js +27 -0
- package/src/package-adapters/manifest.js +108 -0
- package/src/package-adapters/policy.js +81 -0
- package/src/package-adapters/spec.js +51 -0
- package/src/reconcile/journeys.js +8 -3
- package/src/record-blocks.js +125 -0
- package/src/resolver/index.js +3 -0
- package/src/resolver/journeys.js +74 -0
- package/src/resolver/normalize.js +25 -0
- package/src/sdlc/adopt.js +1 -1
- package/src/validator/common.js +34 -1
- package/src/validator/index.js +4 -0
- package/src/validator/kinds.d.ts +2 -0
- package/src/validator/kinds.js +34 -1
- package/src/validator/per-kind/journey.js +233 -0
- package/src/workflows/docs-generate.js +4 -1
- package/src/workflows/reconcile/bundle-core/index.js +4 -2
- package/src/workflows/reconcile/canonical-surface.js +4 -1
- package/src/cli/commands/new.js +0 -94
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
isPathSpec,
|
|
9
|
+
loadInstalledPackageAdapter,
|
|
10
|
+
loadLocalPackageAdapter,
|
|
11
|
+
packageNameFromSpec
|
|
12
|
+
} from "../package-adapters/index.js";
|
|
13
|
+
import {
|
|
14
|
+
EXTRACTOR_MANIFESTS,
|
|
15
|
+
getExtractorManifest,
|
|
16
|
+
loadPackageExtractorManifest,
|
|
17
|
+
validateExtractorManifest
|
|
18
|
+
} from "./registry.js";
|
|
19
|
+
import {
|
|
20
|
+
effectiveExtractorPolicy,
|
|
21
|
+
extractorPackageAllowed,
|
|
22
|
+
extractorPolicyDiagnosticsForPackages,
|
|
23
|
+
loadExtractorPolicy
|
|
24
|
+
} from "../extractor-policy.js";
|
|
25
|
+
import { createImportContext } from "../import/core/context.js";
|
|
26
|
+
import { getBundledExtractorById, getBundledExtractorPack } from "../import/core/registry.js";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {import("./registry.js").ExtractorManifest} ExtractorManifest
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} packageRoot
|
|
34
|
+
* @returns {string|null}
|
|
35
|
+
*/
|
|
36
|
+
function packageNameForRoot(packageRoot) {
|
|
37
|
+
const packageJsonPath = path.join(packageRoot, "package.json");
|
|
38
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
43
|
+
return typeof packageJson.name === "string" && packageJson.name.length > 0 ? packageJson.name : null;
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {any} adapter
|
|
51
|
+
* @param {ExtractorManifest} manifest
|
|
52
|
+
* @returns {{ extractors: any[], errors: string[] }}
|
|
53
|
+
*/
|
|
54
|
+
export function validateExtractorAdapter(adapter, manifest) {
|
|
55
|
+
const errors = [];
|
|
56
|
+
if (!adapter || typeof adapter !== "object" || Array.isArray(adapter)) {
|
|
57
|
+
return { extractors: [], errors: ["Extractor adapter must export an object."] };
|
|
58
|
+
}
|
|
59
|
+
const adapterManifest = adapter.manifest;
|
|
60
|
+
if (!adapterManifest || adapterManifest.id !== manifest.id || adapterManifest.version !== manifest.version) {
|
|
61
|
+
errors.push("Extractor adapter must export manifest matching topogram-extractor.json.");
|
|
62
|
+
}
|
|
63
|
+
if (!Array.isArray(adapter.extractors) || adapter.extractors.length === 0) {
|
|
64
|
+
errors.push("Extractor adapter must export a non-empty extractors array.");
|
|
65
|
+
}
|
|
66
|
+
const manifestExtractorIds = new Set(manifest.extractors || []);
|
|
67
|
+
const manifestTracks = new Set(manifest.tracks || []);
|
|
68
|
+
const extractors = Array.isArray(adapter.extractors) ? adapter.extractors : [];
|
|
69
|
+
for (const extractor of extractors) {
|
|
70
|
+
if (!extractor || typeof extractor !== "object") {
|
|
71
|
+
errors.push("Extractor entries must be objects.");
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (typeof extractor.id !== "string" || extractor.id.length === 0) {
|
|
75
|
+
errors.push("Extractor entries must have a non-empty id.");
|
|
76
|
+
} else if (!manifestExtractorIds.has(extractor.id)) {
|
|
77
|
+
errors.push(`Extractor '${extractor.id}' is not declared by manifest extractors.`);
|
|
78
|
+
}
|
|
79
|
+
if (typeof extractor.track !== "string" || !manifestTracks.has(extractor.track)) {
|
|
80
|
+
errors.push(`Extractor '${extractor.id || "unknown"}' has track '${extractor.track || "unknown"}' not declared by manifest tracks.`);
|
|
81
|
+
}
|
|
82
|
+
if (typeof extractor.detect !== "function") {
|
|
83
|
+
errors.push(`Extractor '${extractor.id || "unknown"}' must expose detect(context).`);
|
|
84
|
+
}
|
|
85
|
+
if (typeof extractor.extract !== "function") {
|
|
86
|
+
errors.push(`Extractor '${extractor.id || "unknown"}' must expose extract(context).`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return { extractors, errors };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {string} sourceSpec
|
|
94
|
+
* @param {{ cwd?: string }} [options]
|
|
95
|
+
* @returns {{ source: "path"|"package", packageName: string|null, packageRoot: string|null, manifestPath: string|null, manifest: ExtractorManifest|null, errors: string[] }}
|
|
96
|
+
*/
|
|
97
|
+
export function loadExtractorPackageManifestForSpec(sourceSpec, options = {}) {
|
|
98
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
99
|
+
if (isPathSpec(sourceSpec, cwd)) {
|
|
100
|
+
const packageRoot = path.resolve(cwd, sourceSpec);
|
|
101
|
+
const manifestPath = path.join(packageRoot, "topogram-extractor.json");
|
|
102
|
+
if (!fs.existsSync(packageRoot) || !fs.statSync(packageRoot).isDirectory()) {
|
|
103
|
+
return { source: "path", packageName: null, packageRoot, manifestPath, manifest: null, errors: [`Extractor path '${packageRoot}' must be a directory.`] };
|
|
104
|
+
}
|
|
105
|
+
if (!fs.existsSync(manifestPath)) {
|
|
106
|
+
return { source: "path", packageName: packageNameForRoot(packageRoot), packageRoot, manifestPath, manifest: null, errors: [`Extractor path '${packageRoot}' is missing topogram-extractor.json.`] };
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
110
|
+
const validation = validateExtractorManifest(manifest);
|
|
111
|
+
return {
|
|
112
|
+
source: "path",
|
|
113
|
+
packageName: typeof manifest.package === "string" ? manifest.package : packageNameForRoot(packageRoot),
|
|
114
|
+
packageRoot,
|
|
115
|
+
manifestPath,
|
|
116
|
+
manifest: validation.ok ? manifest : null,
|
|
117
|
+
errors: validation.errors
|
|
118
|
+
};
|
|
119
|
+
} catch (error) {
|
|
120
|
+
return {
|
|
121
|
+
source: "path",
|
|
122
|
+
packageName: packageNameForRoot(packageRoot),
|
|
123
|
+
packageRoot,
|
|
124
|
+
manifestPath,
|
|
125
|
+
manifest: null,
|
|
126
|
+
errors: [`Extractor manifest could not be read: ${error instanceof Error ? error.message : String(error)}`]
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const packageName = packageNameFromSpec(sourceSpec);
|
|
132
|
+
const loaded = loadPackageExtractorManifest(packageName, cwd);
|
|
133
|
+
return {
|
|
134
|
+
source: "package",
|
|
135
|
+
packageName,
|
|
136
|
+
packageRoot: loaded.packageRoot,
|
|
137
|
+
manifestPath: loaded.manifestPath,
|
|
138
|
+
manifest: loaded.manifest,
|
|
139
|
+
errors: loaded.errors
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @param {string} sourceSpec
|
|
145
|
+
* @param {{ cwd?: string }} [options]
|
|
146
|
+
* @returns {{ adapter: any|null, source: "path"|"package", packageName: string|null, packageRoot: string|null, manifestPath: string|null, manifest: ExtractorManifest|null, errors: string[] }}
|
|
147
|
+
*/
|
|
148
|
+
export function loadExtractorPackageAdapterForSpec(sourceSpec, options = {}) {
|
|
149
|
+
const loadedManifest = loadExtractorPackageManifestForSpec(sourceSpec, options);
|
|
150
|
+
if (!loadedManifest.manifest) {
|
|
151
|
+
return { ...loadedManifest, adapter: null };
|
|
152
|
+
}
|
|
153
|
+
const loadedAdapter = loadedManifest.source === "path"
|
|
154
|
+
? loadLocalPackageAdapter({
|
|
155
|
+
packageRoot: loadedManifest.packageRoot || path.resolve(options.cwd || process.cwd(), sourceSpec),
|
|
156
|
+
exportName: loadedManifest.manifest.export,
|
|
157
|
+
packageLabel: "Extractor package"
|
|
158
|
+
})
|
|
159
|
+
: loadInstalledPackageAdapter({
|
|
160
|
+
packageName: loadedManifest.packageName || packageNameFromSpec(sourceSpec),
|
|
161
|
+
rootDir: path.resolve(options.cwd || process.cwd()),
|
|
162
|
+
exportName: loadedManifest.manifest.export,
|
|
163
|
+
packageLabel: "Extractor package"
|
|
164
|
+
});
|
|
165
|
+
return {
|
|
166
|
+
...loadedManifest,
|
|
167
|
+
adapter: loadedAdapter.adapter,
|
|
168
|
+
errors: [...loadedManifest.errors, ...(loadedAdapter.error ? [loadedAdapter.error] : [])]
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* @param {string} packageName
|
|
174
|
+
* @param {string} version
|
|
175
|
+
* @returns {import("../extractor-policy.js").PackageExtractorBinding}
|
|
176
|
+
*/
|
|
177
|
+
function packageBinding(packageName, version) {
|
|
178
|
+
return {
|
|
179
|
+
packageName,
|
|
180
|
+
version
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* @param {any} context
|
|
186
|
+
* @returns {any}
|
|
187
|
+
*/
|
|
188
|
+
function packageExtractorContext(context) {
|
|
189
|
+
return {
|
|
190
|
+
paths: context.paths,
|
|
191
|
+
options: context.options || {},
|
|
192
|
+
priorResults: context.priorResults || {},
|
|
193
|
+
scanDocsSummary: context.scanDocsSummary || null,
|
|
194
|
+
helpers: {
|
|
195
|
+
path: context.helpers?.path,
|
|
196
|
+
readTextIfExists: context.helpers?.readTextIfExists,
|
|
197
|
+
readJsonIfExists: context.helpers?.readJsonIfExists
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* @param {any} context
|
|
204
|
+
* @returns {{ extractors: any[], diagnostics: any[], provenance: any[] }}
|
|
205
|
+
*/
|
|
206
|
+
export function packageExtractorsForContext(context) {
|
|
207
|
+
if (context.packageExtractorState) {
|
|
208
|
+
return context.packageExtractorState;
|
|
209
|
+
}
|
|
210
|
+
const cwd = path.resolve(context.options?.cwd || process.cwd());
|
|
211
|
+
const policyInfo = loadExtractorPolicy(cwd, context.options?.extractorPolicyPath || null);
|
|
212
|
+
const policy = effectiveExtractorPolicy(policyInfo);
|
|
213
|
+
const specs = [
|
|
214
|
+
...(Array.isArray(policy.enabledPackages) ? policy.enabledPackages : []),
|
|
215
|
+
...(Array.isArray(context.options?.extractorSpecs) ? context.options.extractorSpecs : [])
|
|
216
|
+
].filter(Boolean);
|
|
217
|
+
/** @type {any[]} */
|
|
218
|
+
const extractors = [];
|
|
219
|
+
/** @type {any[]} */
|
|
220
|
+
const diagnostics = [...policyInfo.diagnostics];
|
|
221
|
+
/** @type {any[]} */
|
|
222
|
+
const provenance = [];
|
|
223
|
+
const seen = new Set();
|
|
224
|
+
|
|
225
|
+
for (const spec of specs) {
|
|
226
|
+
if (seen.has(spec)) continue;
|
|
227
|
+
seen.add(spec);
|
|
228
|
+
const bundledPack = getBundledExtractorPack(spec);
|
|
229
|
+
if (bundledPack) {
|
|
230
|
+
extractors.push(...bundledPack.extractors);
|
|
231
|
+
provenance.push({ source: "bundled", id: bundledPack.manifest.id, version: bundledPack.manifest.version, extractors: bundledPack.manifest.extractors });
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const bundledExtractor = getBundledExtractorById(spec);
|
|
235
|
+
if (bundledExtractor) {
|
|
236
|
+
extractors.push(bundledExtractor);
|
|
237
|
+
provenance.push({ source: "bundled", id: bundledExtractor.id, version: "1", extractors: [bundledExtractor.id] });
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const packageManifest = loadExtractorPackageManifestForSpec(spec, { cwd });
|
|
241
|
+
if (!packageManifest.manifest) {
|
|
242
|
+
diagnostics.push({ severity: "error", code: "extractor_package_manifest_failed", message: packageManifest.errors.join(" "), packageName: packageManifest.packageName, spec });
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const packageName = packageManifest.packageName || packageManifest.manifest.package || spec;
|
|
246
|
+
const policyDiagnostics = extractorPolicyDiagnosticsForPackages(policyInfo, [packageBinding(packageName, packageManifest.manifest.version)], "extractor-policy");
|
|
247
|
+
const errors = policyDiagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
248
|
+
diagnostics.push(...policyDiagnostics);
|
|
249
|
+
if (errors.length > 0 || !extractorPackageAllowed(policy, packageName)) {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const adapter = loadExtractorPackageAdapterForSpec(spec, { cwd });
|
|
253
|
+
if (!adapter.adapter || adapter.errors.length > 0) {
|
|
254
|
+
diagnostics.push(...adapter.errors.map((message) => ({ severity: "error", code: "extractor_package_load_failed", message, packageName, spec })));
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const adapterValidation = validateExtractorAdapter(adapter.adapter, packageManifest.manifest);
|
|
258
|
+
if (adapterValidation.errors.length > 0) {
|
|
259
|
+
diagnostics.push(...adapterValidation.errors.map((message) => ({ severity: "error", code: "extractor_adapter_invalid", message, packageName, spec })));
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
for (const extractor of adapterValidation.extractors) {
|
|
263
|
+
extractors.push({
|
|
264
|
+
...extractor,
|
|
265
|
+
source: "package",
|
|
266
|
+
manifestId: packageManifest.manifest.id,
|
|
267
|
+
packageName,
|
|
268
|
+
packageContext: packageExtractorContext
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
provenance.push({
|
|
272
|
+
source: "package",
|
|
273
|
+
id: packageManifest.manifest.id,
|
|
274
|
+
version: packageManifest.manifest.version,
|
|
275
|
+
packageName,
|
|
276
|
+
manifestPath: packageManifest.manifestPath,
|
|
277
|
+
packageRoot: packageManifest.packageRoot,
|
|
278
|
+
extractors: packageManifest.manifest.extractors
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
context.packageExtractorState = { extractors, diagnostics, provenance };
|
|
283
|
+
return context.packageExtractorState;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* @returns {any}
|
|
288
|
+
*/
|
|
289
|
+
export function createExtractorSmokeContext() {
|
|
290
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "topogram-extractor-check."));
|
|
291
|
+
fs.writeFileSync(path.join(root, "package.json"), "{\"scripts\":{\"check\":\"topogram check\"}}\n", "utf8");
|
|
292
|
+
return packageExtractorContext(createImportContext(root, { from: "cli" }));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export { EXTRACTOR_MANIFESTS, getExtractorManifest };
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
loadPackageManifest,
|
|
5
|
+
packageInstallCommand,
|
|
6
|
+
packageInstallHint,
|
|
7
|
+
resolvePackageManifestPath
|
|
8
|
+
} from "../package-adapters/index.js";
|
|
9
|
+
import { BUILTIN_EXTRACTOR_PACKS } from "../import/core/registry.js";
|
|
10
|
+
|
|
11
|
+
export const EXTRACTOR_TRACKS = ["db", "api", "ui", "cli", "workflows", "verification"];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {Object} ExtractorManifest
|
|
15
|
+
* @property {string} id
|
|
16
|
+
* @property {string} version
|
|
17
|
+
* @property {string[]} tracks
|
|
18
|
+
* @property {"bundled"|"package"} source
|
|
19
|
+
* @property {string[]} extractors
|
|
20
|
+
* @property {Record<string, string>} stack
|
|
21
|
+
* @property {Record<string, boolean>} capabilities
|
|
22
|
+
* @property {string[]} candidateKinds
|
|
23
|
+
* @property {string[]} evidenceTypes
|
|
24
|
+
* @property {string} [package]
|
|
25
|
+
* @property {string} [export]
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {Object} ExtractorBinding
|
|
30
|
+
* @property {string} id
|
|
31
|
+
* @property {string} track
|
|
32
|
+
* @property {string} packageName
|
|
33
|
+
* @property {string} version
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {Object} ResolvedExtractorManifest
|
|
38
|
+
* @property {ExtractorManifest|null} manifest
|
|
39
|
+
* @property {string[]} errors
|
|
40
|
+
* @property {"bundled"|"package"|null} source
|
|
41
|
+
* @property {string|null} manifestPath
|
|
42
|
+
* @property {string|null} packageRoot
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/** @type {ExtractorManifest[]} */
|
|
46
|
+
export const EXTRACTOR_MANIFESTS = BUILTIN_EXTRACTOR_PACKS.map((pack) => pack.manifest);
|
|
47
|
+
|
|
48
|
+
const EXTRACTOR_MANIFEST_BY_ID = new Map(EXTRACTOR_MANIFESTS.map((manifest) => [manifest.id, manifest]));
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {any} value
|
|
52
|
+
* @param {boolean} [nonEmpty]
|
|
53
|
+
* @returns {boolean}
|
|
54
|
+
*/
|
|
55
|
+
function isStringArray(value, nonEmpty = false) {
|
|
56
|
+
return Array.isArray(value) &&
|
|
57
|
+
(!nonEmpty || value.length > 0) &&
|
|
58
|
+
value.every((entry) => typeof entry === "string" && entry.length > 0);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {any} value
|
|
63
|
+
* @returns {value is Record<string, string>}
|
|
64
|
+
*/
|
|
65
|
+
function isStringRecord(value) {
|
|
66
|
+
return Boolean(value) &&
|
|
67
|
+
typeof value === "object" &&
|
|
68
|
+
!Array.isArray(value) &&
|
|
69
|
+
Object.values(value).every((entry) => typeof entry === "string");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {any} value
|
|
74
|
+
* @returns {value is Record<string, boolean>}
|
|
75
|
+
*/
|
|
76
|
+
function isBooleanRecord(value) {
|
|
77
|
+
return Boolean(value) &&
|
|
78
|
+
typeof value === "object" &&
|
|
79
|
+
!Array.isArray(value) &&
|
|
80
|
+
Object.values(value).every((entry) => typeof entry === "boolean");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {any} manifest
|
|
85
|
+
* @returns {{ ok: boolean, errors: string[] }}
|
|
86
|
+
*/
|
|
87
|
+
export function validateExtractorManifest(manifest) {
|
|
88
|
+
const errors = [];
|
|
89
|
+
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
|
|
90
|
+
return { ok: false, errors: ["Extractor manifest must be an object."] };
|
|
91
|
+
}
|
|
92
|
+
if (typeof manifest.id !== "string" || manifest.id.length === 0) {
|
|
93
|
+
errors.push("Extractor manifest id must be a non-empty string.");
|
|
94
|
+
}
|
|
95
|
+
if (typeof manifest.version !== "string" || manifest.version.length === 0) {
|
|
96
|
+
errors.push("Extractor manifest version must be a non-empty string.");
|
|
97
|
+
}
|
|
98
|
+
if (!isStringArray(manifest.tracks, true)) {
|
|
99
|
+
errors.push("Extractor manifest tracks must be a non-empty array of strings.");
|
|
100
|
+
} else {
|
|
101
|
+
for (const track of manifest.tracks) {
|
|
102
|
+
if (!EXTRACTOR_TRACKS.includes(track)) {
|
|
103
|
+
errors.push(`Extractor manifest track '${track}' is not supported.`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (manifest.source !== "bundled" && manifest.source !== "package") {
|
|
108
|
+
errors.push("Extractor manifest source must be 'bundled' or 'package'.");
|
|
109
|
+
}
|
|
110
|
+
if (!isStringArray(manifest.extractors, true)) {
|
|
111
|
+
errors.push("Extractor manifest extractors must be a non-empty array of strings.");
|
|
112
|
+
}
|
|
113
|
+
if (!isStringRecord(manifest.stack)) {
|
|
114
|
+
errors.push("Extractor manifest stack must be an object of string values.");
|
|
115
|
+
}
|
|
116
|
+
if (!isBooleanRecord(manifest.capabilities)) {
|
|
117
|
+
errors.push("Extractor manifest capabilities must be an object of boolean values.");
|
|
118
|
+
}
|
|
119
|
+
if (!isStringArray(manifest.candidateKinds)) {
|
|
120
|
+
errors.push("Extractor manifest candidateKinds must be an array of strings.");
|
|
121
|
+
}
|
|
122
|
+
if (!isStringArray(manifest.evidenceTypes)) {
|
|
123
|
+
errors.push("Extractor manifest evidenceTypes must be an array of strings.");
|
|
124
|
+
}
|
|
125
|
+
if (manifest.package != null && (typeof manifest.package !== "string" || manifest.package.length === 0)) {
|
|
126
|
+
errors.push("Extractor manifest package must be a non-empty string when present.");
|
|
127
|
+
}
|
|
128
|
+
if (manifest.export != null && (typeof manifest.export !== "string" || manifest.export.length === 0)) {
|
|
129
|
+
errors.push("Extractor manifest export must be a non-empty string when present.");
|
|
130
|
+
}
|
|
131
|
+
return { ok: errors.length === 0, errors };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* @param {string} extractorId
|
|
136
|
+
* @returns {ExtractorManifest|null}
|
|
137
|
+
*/
|
|
138
|
+
export function getExtractorManifest(extractorId) {
|
|
139
|
+
return EXTRACTOR_MANIFEST_BY_ID.get(extractorId) || null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @param {string|null|undefined} packageName
|
|
144
|
+
* @returns {string|null}
|
|
145
|
+
*/
|
|
146
|
+
export function packageExtractorInstallCommand(packageName) {
|
|
147
|
+
return packageInstallCommand(packageName);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* @param {string|null|undefined} packageName
|
|
152
|
+
* @returns {string|null}
|
|
153
|
+
*/
|
|
154
|
+
export function packageExtractorInstallHint(packageName) {
|
|
155
|
+
return packageInstallHint(packageName);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @param {string} packageName
|
|
160
|
+
* @param {string|null|undefined} rootDir
|
|
161
|
+
* @returns {{ manifestPath: string|null, packageRoot: string|null, error: string|null }}
|
|
162
|
+
*/
|
|
163
|
+
export function resolvePackageExtractorManifestPath(packageName, rootDir = process.cwd()) {
|
|
164
|
+
return resolvePackageManifestPath(packageName, "topogram-extractor.json", rootDir, "Extractor package");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* @param {string} packageName
|
|
169
|
+
* @param {string|null|undefined} rootDir
|
|
170
|
+
* @returns {{ manifest: ExtractorManifest|null, errors: string[], manifestPath: string|null, packageRoot: string|null }}
|
|
171
|
+
*/
|
|
172
|
+
export function loadPackageExtractorManifest(packageName, rootDir = process.cwd()) {
|
|
173
|
+
return loadPackageManifest({
|
|
174
|
+
packageName,
|
|
175
|
+
rootDir,
|
|
176
|
+
manifestFile: "topogram-extractor.json",
|
|
177
|
+
packageLabel: "Extractor package",
|
|
178
|
+
validateManifest: validateExtractorManifest
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* @param {Record<string, any>|null|undefined} manifest
|
|
184
|
+
* @returns {ExtractorBinding[]}
|
|
185
|
+
*/
|
|
186
|
+
export function extractorBindingsForManifest(manifest) {
|
|
187
|
+
if (!manifest || manifest.source !== "package" || typeof manifest.package !== "string") {
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
return (manifest.extractors || []).map((/** @type {any} */ extractorId) => ({
|
|
191
|
+
id: String(extractorId),
|
|
192
|
+
track: Array.isArray(manifest.tracks) && manifest.tracks.length === 1 ? String(manifest.tracks[0]) : "multiple",
|
|
193
|
+
packageName: String(manifest.package),
|
|
194
|
+
version: String(manifest.version || "unknown")
|
|
195
|
+
}));
|
|
196
|
+
}
|