@topogram/cli 0.3.79 → 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/package.json +1 -1
- package/src/cli/command-parser.js +2 -0
- package/src/cli/command-parsers/extractor.js +40 -0
- package/src/cli/commands/extractor.js +451 -0
- package/src/cli/commands/import/help.js +3 -1
- package/src/cli/commands/import/workspace.js +8 -2
- package/src/cli/commands/import-runner.js +4 -2
- package/src/cli/dispatcher.js +14 -0
- package/src/cli/help-dispatch.js +11 -0
- package/src/cli/help.js +12 -1
- 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/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/run.js +2 -0
- package/src/import/core/runner/tracks.js +66 -4
- package/src/import/provenance.js +2 -1
- 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/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
3
|
import { parseCoreCommandArgs } from "./command-parsers/core.js";
|
|
4
|
+
import { parseExtractorCommandArgs } from "./command-parsers/extractor.js";
|
|
4
5
|
import { parseGeneratorCommandArgs } from "./command-parsers/generator.js";
|
|
5
6
|
import { parseImportCommandArgs } from "./command-parsers/import.js";
|
|
6
7
|
import { parseLegacyWorkflowCommandArgs } from "./command-parsers/legacy-workflow.js";
|
|
@@ -10,6 +11,7 @@ import { parseTemplateCommandArgs } from "./command-parsers/template.js";
|
|
|
10
11
|
|
|
11
12
|
const COMMAND_PARSERS = [
|
|
12
13
|
parseCoreCommandArgs,
|
|
14
|
+
parseExtractorCommandArgs,
|
|
13
15
|
parseGeneratorCommandArgs,
|
|
14
16
|
parseTemplateCommandArgs,
|
|
15
17
|
parseProjectCommandArgs,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { commandPath } from "./shared.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {string[]} args
|
|
7
|
+
* @returns {import("./shared.js").SplitCommandArgs|null}
|
|
8
|
+
*/
|
|
9
|
+
export function parseExtractorCommandArgs(args) {
|
|
10
|
+
if (args[0] === "extractor" && args[1] === "list") {
|
|
11
|
+
return { extractorCommand: "list", inputPath: null };
|
|
12
|
+
}
|
|
13
|
+
if (args[0] === "extractor" && args[1] === "show") {
|
|
14
|
+
return { extractorCommand: "show", inputPath: args[2] };
|
|
15
|
+
}
|
|
16
|
+
if (args[0] === "extractor" && args[1] === "check") {
|
|
17
|
+
return { extractorCommand: "check", inputPath: args[2] };
|
|
18
|
+
}
|
|
19
|
+
if (args[0] === "extractor" && args[1] === "policy" && args[2] === "init") {
|
|
20
|
+
return { extractorPolicyCommand: "init", inputPath: commandPath(args, 3, ".") };
|
|
21
|
+
}
|
|
22
|
+
if (args[0] === "extractor" && args[1] === "policy" && args[2] === "status") {
|
|
23
|
+
return { extractorPolicyCommand: "status", inputPath: commandPath(args, 3, ".") };
|
|
24
|
+
}
|
|
25
|
+
if (args[0] === "extractor" && args[1] === "policy" && args[2] === "check") {
|
|
26
|
+
return { extractorPolicyCommand: "check", inputPath: commandPath(args, 3, ".") };
|
|
27
|
+
}
|
|
28
|
+
if (args[0] === "extractor" && args[1] === "policy" && args[2] === "explain") {
|
|
29
|
+
return { extractorPolicyCommand: "explain", inputPath: commandPath(args, 3, ".") };
|
|
30
|
+
}
|
|
31
|
+
if (args[0] === "extractor" && args[1] === "policy" && args[2] === "pin") {
|
|
32
|
+
return {
|
|
33
|
+
extractorPolicyCommand: "pin",
|
|
34
|
+
extractorPolicyPinSpec: args[3] && !args[3].startsWith("-") ? args[3] : null,
|
|
35
|
+
inputPath: commandPath(args, 4, ".")
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { stableStringify } from "../../format.js";
|
|
7
|
+
import { checkExtractorPack } from "../../extractor/check.js";
|
|
8
|
+
import {
|
|
9
|
+
EXTRACTOR_MANIFESTS,
|
|
10
|
+
getExtractorManifest,
|
|
11
|
+
loadPackageExtractorManifest,
|
|
12
|
+
packageExtractorInstallCommand
|
|
13
|
+
} from "../../extractor/registry.js";
|
|
14
|
+
import {
|
|
15
|
+
defaultExtractorPolicy,
|
|
16
|
+
effectiveExtractorPolicy,
|
|
17
|
+
EXTRACTOR_POLICY_FILE,
|
|
18
|
+
extractorPackageAllowed,
|
|
19
|
+
extractorPolicyDiagnosticsForPackages,
|
|
20
|
+
loadExtractorPolicy,
|
|
21
|
+
packageScopeFromName,
|
|
22
|
+
parseExtractorPolicyPin,
|
|
23
|
+
writeExtractorPolicy
|
|
24
|
+
} from "../../extractor-policy.js";
|
|
25
|
+
|
|
26
|
+
export function printExtractorHelp() {
|
|
27
|
+
console.log("Usage: topogram extractor list [--json]");
|
|
28
|
+
console.log(" or: topogram extractor show <id-or-package> [--json]");
|
|
29
|
+
console.log(" or: topogram extractor check <path-or-package> [--json]");
|
|
30
|
+
console.log(" or: topogram extractor policy init [path] [--json]");
|
|
31
|
+
console.log(" or: topogram extractor policy status [path] [--json]");
|
|
32
|
+
console.log(" or: topogram extractor policy check [path] [--json]");
|
|
33
|
+
console.log(" or: topogram extractor policy explain [path] [--json]");
|
|
34
|
+
console.log(" or: topogram extractor policy pin [package@version] [path] [--json]");
|
|
35
|
+
console.log("");
|
|
36
|
+
console.log("Inspects extractor manifests and checks extractor pack conformance.");
|
|
37
|
+
console.log("");
|
|
38
|
+
console.log("Notes:");
|
|
39
|
+
console.log(" - extractor packages execute only during `topogram extract` or `topogram extractor check`.");
|
|
40
|
+
console.log(" - extractor packages emit review-only candidates; core owns persistence, reconcile, and adoption.");
|
|
41
|
+
console.log(` - package-backed extractors are governed by ${EXTRACTOR_POLICY_FILE}; bundled topogram/* extractors are allowed.`);
|
|
42
|
+
console.log("");
|
|
43
|
+
console.log("Examples:");
|
|
44
|
+
console.log(" topogram extractor list");
|
|
45
|
+
console.log(" topogram extractor show topogram/api-extractors");
|
|
46
|
+
console.log(" topogram extractor check ./extractor-package");
|
|
47
|
+
console.log(" topogram extractor policy init");
|
|
48
|
+
console.log(" topogram extractor policy pin @topogram/extractor-node-cli@1");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {string} cwd
|
|
53
|
+
* @returns {string[]}
|
|
54
|
+
*/
|
|
55
|
+
function declaredExtractorPackages(cwd) {
|
|
56
|
+
const packagePath = path.join(cwd, "package.json");
|
|
57
|
+
if (!fs.existsSync(packagePath)) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
|
61
|
+
const dependencyBuckets = [
|
|
62
|
+
packageJson.dependencies,
|
|
63
|
+
packageJson.devDependencies,
|
|
64
|
+
packageJson.optionalDependencies,
|
|
65
|
+
packageJson.peerDependencies
|
|
66
|
+
];
|
|
67
|
+
const packages = new Set();
|
|
68
|
+
for (const dependencies of dependencyBuckets) {
|
|
69
|
+
if (!dependencies || typeof dependencies !== "object" || Array.isArray(dependencies)) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
for (const name of Object.keys(dependencies)) {
|
|
73
|
+
if (name.includes("topogram-extractor") || name.startsWith("@topogram/extractor-")) {
|
|
74
|
+
packages.add(name);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return [...packages].sort();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {any} manifest
|
|
83
|
+
* @param {{ installed?: boolean, manifestPath?: string|null, packageRoot?: string|null, errors?: string[] }} [metadata]
|
|
84
|
+
* @returns {Record<string, any>}
|
|
85
|
+
*/
|
|
86
|
+
function extractorManifestSummary(manifest, metadata = {}) {
|
|
87
|
+
const installCommand = manifest.package ? packageExtractorInstallCommand(manifest.package) : null;
|
|
88
|
+
return {
|
|
89
|
+
id: manifest.id,
|
|
90
|
+
version: manifest.version,
|
|
91
|
+
tracks: manifest.tracks || [],
|
|
92
|
+
extractors: manifest.extractors || [],
|
|
93
|
+
stack: manifest.stack || {},
|
|
94
|
+
capabilities: manifest.capabilities || {},
|
|
95
|
+
candidateKinds: manifest.candidateKinds || [],
|
|
96
|
+
evidenceTypes: manifest.evidenceTypes || [],
|
|
97
|
+
source: manifest.source,
|
|
98
|
+
loadsAdapter: false,
|
|
99
|
+
executesPackageCode: false,
|
|
100
|
+
...(manifest.package ? { package: manifest.package } : {}),
|
|
101
|
+
...(installCommand ? { installCommand } : {}),
|
|
102
|
+
installed: metadata.installed !== false,
|
|
103
|
+
manifestPath: metadata.manifestPath || null,
|
|
104
|
+
packageRoot: metadata.packageRoot || null,
|
|
105
|
+
errors: metadata.errors || []
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {string} cwd
|
|
111
|
+
* @returns {{ ok: boolean, cwd: string, extractors: Record<string, any>[], summary: Record<string, number> }}
|
|
112
|
+
*/
|
|
113
|
+
export function buildExtractorListPayload(cwd) {
|
|
114
|
+
const extractors = EXTRACTOR_MANIFESTS
|
|
115
|
+
.map((manifest) => extractorManifestSummary(manifest))
|
|
116
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
117
|
+
for (const packageName of declaredExtractorPackages(cwd)) {
|
|
118
|
+
const loaded = loadPackageExtractorManifest(packageName, cwd);
|
|
119
|
+
if (loaded.manifest) {
|
|
120
|
+
extractors.push(extractorManifestSummary(loaded.manifest, {
|
|
121
|
+
installed: true,
|
|
122
|
+
manifestPath: loaded.manifestPath,
|
|
123
|
+
packageRoot: loaded.packageRoot,
|
|
124
|
+
errors: loaded.errors
|
|
125
|
+
}));
|
|
126
|
+
} else {
|
|
127
|
+
extractors.push({
|
|
128
|
+
id: null,
|
|
129
|
+
version: null,
|
|
130
|
+
tracks: [],
|
|
131
|
+
extractors: [],
|
|
132
|
+
stack: {},
|
|
133
|
+
capabilities: {},
|
|
134
|
+
candidateKinds: [],
|
|
135
|
+
evidenceTypes: [],
|
|
136
|
+
source: "package",
|
|
137
|
+
package: packageName,
|
|
138
|
+
installCommand: packageExtractorInstallCommand(packageName),
|
|
139
|
+
installed: false,
|
|
140
|
+
manifestPath: loaded.manifestPath,
|
|
141
|
+
packageRoot: loaded.packageRoot,
|
|
142
|
+
errors: loaded.errors
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
extractors.sort((left, right) => String(left.id || left.package || "").localeCompare(String(right.id || right.package || "")));
|
|
147
|
+
return {
|
|
148
|
+
ok: extractors.every((extractor) => extractor.errors.length === 0),
|
|
149
|
+
cwd,
|
|
150
|
+
extractors,
|
|
151
|
+
summary: {
|
|
152
|
+
total: extractors.length,
|
|
153
|
+
bundled: extractors.filter((extractor) => extractor.source === "bundled").length,
|
|
154
|
+
package: extractors.filter((extractor) => extractor.source === "package").length,
|
|
155
|
+
installed: extractors.filter((extractor) => extractor.installed).length
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* @param {string} spec
|
|
162
|
+
* @param {string} cwd
|
|
163
|
+
* @returns {{ ok: boolean, sourceSpec: string, extractor: Record<string, any>|null, errors: string[] }}
|
|
164
|
+
*/
|
|
165
|
+
export function buildExtractorShowPayload(spec, cwd) {
|
|
166
|
+
if (!spec || spec.startsWith("-")) {
|
|
167
|
+
return { ok: false, sourceSpec: spec || "", extractor: null, errors: ["Usage: topogram extractor show <id-or-package>"] };
|
|
168
|
+
}
|
|
169
|
+
const bundled = getExtractorManifest(spec);
|
|
170
|
+
if (bundled) {
|
|
171
|
+
return { ok: true, sourceSpec: spec, extractor: extractorManifestSummary(bundled), errors: [] };
|
|
172
|
+
}
|
|
173
|
+
const loaded = loadPackageExtractorManifest(spec, cwd);
|
|
174
|
+
if (loaded.manifest) {
|
|
175
|
+
return {
|
|
176
|
+
ok: true,
|
|
177
|
+
sourceSpec: spec,
|
|
178
|
+
extractor: extractorManifestSummary(loaded.manifest, {
|
|
179
|
+
installed: true,
|
|
180
|
+
manifestPath: loaded.manifestPath,
|
|
181
|
+
packageRoot: loaded.packageRoot,
|
|
182
|
+
errors: loaded.errors
|
|
183
|
+
}),
|
|
184
|
+
errors: []
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
return { ok: false, sourceSpec: spec, extractor: null, errors: loaded.errors };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* @param {ReturnType<typeof buildExtractorListPayload>} payload
|
|
192
|
+
* @returns {void}
|
|
193
|
+
*/
|
|
194
|
+
export function printExtractorList(payload) {
|
|
195
|
+
console.log("Topogram extractors");
|
|
196
|
+
console.log(`Bundled: ${payload.summary.bundled}; package-backed: ${payload.summary.package}; installed: ${payload.summary.installed}`);
|
|
197
|
+
console.log("");
|
|
198
|
+
for (const extractor of payload.extractors) {
|
|
199
|
+
const id = extractor.id || extractor.package || "unknown";
|
|
200
|
+
const status = extractor.errors.length > 0
|
|
201
|
+
? "invalid"
|
|
202
|
+
: extractor.source === "package"
|
|
203
|
+
? (extractor.installed ? "package installed" : "package missing")
|
|
204
|
+
: "bundled";
|
|
205
|
+
console.log(`- ${id}${extractor.version ? `@${extractor.version}` : ""} (${extractor.tracks.join(", ") || "unknown"}, ${status})`);
|
|
206
|
+
console.log(` Source: ${extractor.source}`);
|
|
207
|
+
console.log(" Adapter loaded: no");
|
|
208
|
+
console.log(" Executes package code: no");
|
|
209
|
+
console.log(` Extractors: ${extractor.extractors.join(", ") || "none"}`);
|
|
210
|
+
if (extractor.package) console.log(` Package: ${extractor.package}`);
|
|
211
|
+
if (extractor.installCommand) console.log(` Install: ${extractor.installCommand}`);
|
|
212
|
+
for (const error of extractor.errors || []) console.log(` Error: ${error}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* @param {ReturnType<typeof buildExtractorShowPayload>} payload
|
|
218
|
+
* @returns {void}
|
|
219
|
+
*/
|
|
220
|
+
export function printExtractorShow(payload) {
|
|
221
|
+
if (!payload.ok || !payload.extractor) {
|
|
222
|
+
console.log("Extractor pack not found.");
|
|
223
|
+
for (const error of payload.errors || []) console.log(`- ${error}`);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const extractor = payload.extractor;
|
|
227
|
+
console.log(`Extractor pack: ${extractor.id}@${extractor.version}`);
|
|
228
|
+
console.log(`Tracks: ${extractor.tracks.join(", ") || "none"}`);
|
|
229
|
+
console.log(`Source: ${extractor.source}`);
|
|
230
|
+
console.log("Adapter loaded: no");
|
|
231
|
+
console.log("Executes package code: no");
|
|
232
|
+
if (extractor.package) console.log(`Package: ${extractor.package}`);
|
|
233
|
+
if (extractor.installCommand) console.log(`Install: ${extractor.installCommand}`);
|
|
234
|
+
if (extractor.manifestPath) console.log(`Manifest: ${extractor.manifestPath}`);
|
|
235
|
+
console.log(`Extractors: ${extractor.extractors.join(", ") || "none"}`);
|
|
236
|
+
console.log(`Candidate kinds: ${extractor.candidateKinds.join(", ") || "none"}`);
|
|
237
|
+
console.log(`Evidence types: ${extractor.evidenceTypes.join(", ") || "none"}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* @param {ReturnType<typeof checkExtractorPack>} payload
|
|
242
|
+
* @returns {void}
|
|
243
|
+
*/
|
|
244
|
+
export function printExtractorCheck(payload) {
|
|
245
|
+
console.log(payload.ok ? "Extractor check passed." : "Extractor check found issues.");
|
|
246
|
+
console.log(`Source: ${payload.sourceSpec}`);
|
|
247
|
+
console.log(`Type: ${payload.source}`);
|
|
248
|
+
if (payload.packageName) console.log(`Package: ${payload.packageName}`);
|
|
249
|
+
if (payload.manifestPath) console.log(`Manifest: ${payload.manifestPath}`);
|
|
250
|
+
if (payload.manifest) {
|
|
251
|
+
console.log(`Extractor pack: ${payload.manifest.id}@${payload.manifest.version}`);
|
|
252
|
+
console.log(`Tracks: ${payload.manifest.tracks.join(", ")}`);
|
|
253
|
+
console.log(`Source mode: ${payload.manifest.source}`);
|
|
254
|
+
}
|
|
255
|
+
console.log("Executes package code: yes (loads adapter and runs smoke extract)");
|
|
256
|
+
console.log("");
|
|
257
|
+
console.log("Checks:");
|
|
258
|
+
for (const check of payload.checks || []) {
|
|
259
|
+
console.log(`- ${check.ok ? "PASS" : "FAIL"} ${check.name}: ${check.message}`);
|
|
260
|
+
}
|
|
261
|
+
if (payload.smoke) {
|
|
262
|
+
console.log("");
|
|
263
|
+
console.log(`Smoke output: ${payload.smoke.extractors} extractor(s), ${payload.smoke.findings} finding(s), ${payload.smoke.candidateKeys} candidate bucket(s), ${payload.smoke.diagnostics} diagnostic(s)`);
|
|
264
|
+
}
|
|
265
|
+
for (const error of payload.errors || []) console.log(`Error: ${error}`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* @param {string} projectPath
|
|
270
|
+
* @returns {{ ok: boolean, path: string, exists: boolean, policy: any, defaulted: boolean, packages: any[], diagnostics: any[], errors: string[], summary: Record<string, number> }}
|
|
271
|
+
*/
|
|
272
|
+
export function buildExtractorPolicyStatusPayload(projectPath) {
|
|
273
|
+
const root = path.resolve(projectPath || ".");
|
|
274
|
+
const policyInfo = loadExtractorPolicy(root);
|
|
275
|
+
const policy = effectiveExtractorPolicy(policyInfo);
|
|
276
|
+
const packages = policy.enabledPackages.map((packageName) => {
|
|
277
|
+
const loaded = loadPackageExtractorManifest(packageName, root);
|
|
278
|
+
const version = loaded.manifest?.version || "unknown";
|
|
279
|
+
const diagnostics = extractorPolicyDiagnosticsForPackages(policyInfo, [{ packageName, version }], "extractor-policy");
|
|
280
|
+
return {
|
|
281
|
+
packageName,
|
|
282
|
+
version,
|
|
283
|
+
allowed: extractorPackageAllowed(policy, packageName),
|
|
284
|
+
installed: Boolean(loaded.manifest),
|
|
285
|
+
manifestPath: loaded.manifestPath,
|
|
286
|
+
packageRoot: loaded.packageRoot,
|
|
287
|
+
errors: [...loaded.errors, ...diagnostics.filter((diagnostic) => diagnostic.severity === "error").map((diagnostic) => diagnostic.message)]
|
|
288
|
+
};
|
|
289
|
+
});
|
|
290
|
+
const diagnostics = [
|
|
291
|
+
...policyInfo.diagnostics,
|
|
292
|
+
...packages.flatMap((item) => item.errors.map((message) => ({
|
|
293
|
+
code: "extractor_package_failed",
|
|
294
|
+
severity: "error",
|
|
295
|
+
message,
|
|
296
|
+
path: item.manifestPath,
|
|
297
|
+
suggestedFix: `Review or remove '${item.packageName}' from ${EXTRACTOR_POLICY_FILE}.`,
|
|
298
|
+
step: "extractor-policy",
|
|
299
|
+
packageName: item.packageName,
|
|
300
|
+
version: item.version
|
|
301
|
+
})))
|
|
302
|
+
];
|
|
303
|
+
const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error").map((diagnostic) => diagnostic.message);
|
|
304
|
+
return {
|
|
305
|
+
ok: errors.length === 0,
|
|
306
|
+
path: policyInfo.path,
|
|
307
|
+
exists: policyInfo.exists,
|
|
308
|
+
policy,
|
|
309
|
+
defaulted: !policyInfo.exists,
|
|
310
|
+
packages,
|
|
311
|
+
diagnostics,
|
|
312
|
+
errors,
|
|
313
|
+
summary: {
|
|
314
|
+
enabledPackages: policy.enabledPackages.length,
|
|
315
|
+
installed: packages.filter((item) => item.installed).length,
|
|
316
|
+
allowed: packages.filter((item) => item.allowed).length,
|
|
317
|
+
denied: packages.filter((item) => !item.allowed).length
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* @param {string} projectPath
|
|
324
|
+
* @returns {{ ok: boolean, path: string, policy: any, diagnostics: any[], errors: string[] }}
|
|
325
|
+
*/
|
|
326
|
+
export function buildExtractorPolicyInitPayload(projectPath) {
|
|
327
|
+
const root = path.resolve(projectPath || ".");
|
|
328
|
+
const policy = writeExtractorPolicy(root, defaultExtractorPolicy());
|
|
329
|
+
return { ok: true, path: path.join(root, EXTRACTOR_POLICY_FILE), policy, diagnostics: [], errors: [] };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* @param {string} projectPath
|
|
334
|
+
* @param {string|null|undefined} spec
|
|
335
|
+
* @returns {{ ok: boolean, path: string, policy: any, pinned: Array<{ packageName: string, version: string }>, diagnostics: any[], errors: string[] }}
|
|
336
|
+
*/
|
|
337
|
+
export function buildExtractorPolicyPinPayload(projectPath, spec) {
|
|
338
|
+
const root = path.resolve(projectPath || ".");
|
|
339
|
+
const policyInfo = loadExtractorPolicy(root);
|
|
340
|
+
const policy = policyInfo.policy || defaultExtractorPolicy();
|
|
341
|
+
let pin;
|
|
342
|
+
try {
|
|
343
|
+
pin = parseExtractorPolicyPin(spec || "");
|
|
344
|
+
} catch (error) {
|
|
345
|
+
return {
|
|
346
|
+
ok: false,
|
|
347
|
+
path: policyInfo.path,
|
|
348
|
+
policy,
|
|
349
|
+
pinned: [],
|
|
350
|
+
diagnostics: [{ severity: "error", code: "extractor_policy_pin_invalid", message: error instanceof Error ? error.message : String(error), path: policyInfo.path }],
|
|
351
|
+
errors: [error instanceof Error ? error.message : String(error)]
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
const nextPolicy = {
|
|
355
|
+
...policy,
|
|
356
|
+
allowedPackages: policy.allowedPackages.includes(pin.packageName) ? policy.allowedPackages : [...policy.allowedPackages, pin.packageName],
|
|
357
|
+
enabledPackages: policy.enabledPackages.includes(pin.packageName) ? policy.enabledPackages : [...policy.enabledPackages, pin.packageName],
|
|
358
|
+
pinnedVersions: { ...policy.pinnedVersions, [pin.packageName]: pin.version }
|
|
359
|
+
};
|
|
360
|
+
writeExtractorPolicy(root, nextPolicy);
|
|
361
|
+
return { ok: true, path: path.join(root, EXTRACTOR_POLICY_FILE), policy: nextPolicy, pinned: [pin], diagnostics: [], errors: [] };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* @param {ReturnType<typeof buildExtractorPolicyStatusPayload>} payload
|
|
366
|
+
* @returns {void}
|
|
367
|
+
*/
|
|
368
|
+
export function printExtractorPolicyStatus(payload) {
|
|
369
|
+
console.log(payload.ok ? "Extractor policy status: allowed" : "Extractor policy status: denied");
|
|
370
|
+
console.log(`Policy file: ${payload.path}`);
|
|
371
|
+
console.log(`Policy file exists: ${payload.exists ? "yes" : "no"}`);
|
|
372
|
+
console.log(`Default policy active: ${payload.defaulted ? "yes" : "no"}`);
|
|
373
|
+
console.log(`Enabled packages: ${payload.summary.enabledPackages}`);
|
|
374
|
+
for (const item of payload.packages) {
|
|
375
|
+
console.log(`- ${item.packageName}@${item.version}: ${item.installed ? "installed" : "missing"}, ${item.allowed ? "allowed" : "denied"}`);
|
|
376
|
+
}
|
|
377
|
+
for (const diagnostic of payload.diagnostics) {
|
|
378
|
+
console.log(`[${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* @param {ReturnType<typeof buildExtractorPolicyInitPayload>} payload
|
|
384
|
+
* @returns {void}
|
|
385
|
+
*/
|
|
386
|
+
export function printExtractorPolicyInit(payload) {
|
|
387
|
+
console.log(`Wrote extractor policy: ${payload.path}`);
|
|
388
|
+
console.log(`Allowed package scopes: ${payload.policy.allowedPackageScopes.join(", ") || "(none)"}`);
|
|
389
|
+
console.log(`Allowed packages: ${payload.policy.allowedPackages.join(", ") || "(none)"}`);
|
|
390
|
+
console.log(`Enabled packages: ${payload.policy.enabledPackages.join(", ") || "(none)"}`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* @param {ReturnType<typeof buildExtractorPolicyPinPayload>} payload
|
|
395
|
+
* @returns {void}
|
|
396
|
+
*/
|
|
397
|
+
export function printExtractorPolicyPin(payload) {
|
|
398
|
+
console.log(payload.ok ? "Extractor policy pin updated" : "Extractor policy pin failed");
|
|
399
|
+
console.log(`Policy: ${payload.path}`);
|
|
400
|
+
for (const pin of payload.pinned || []) {
|
|
401
|
+
console.log(`Pinned: ${pin.packageName}@${pin.version}`);
|
|
402
|
+
}
|
|
403
|
+
for (const diagnostic of payload.diagnostics || []) {
|
|
404
|
+
console.log(`[${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* @param {{ commandArgs: Record<string, any>, inputPath: string|null|undefined, json: boolean, cwd: string }} context
|
|
410
|
+
* @returns {number}
|
|
411
|
+
*/
|
|
412
|
+
export function runExtractorCommand(context) {
|
|
413
|
+
const { commandArgs, inputPath, json, cwd } = context;
|
|
414
|
+
if (commandArgs.extractorCommand === "check") {
|
|
415
|
+
const payload = checkExtractorPack(inputPath || "", { cwd });
|
|
416
|
+
if (json) console.log(stableStringify(payload));
|
|
417
|
+
else printExtractorCheck(payload);
|
|
418
|
+
return payload.ok ? 0 : 1;
|
|
419
|
+
}
|
|
420
|
+
if (commandArgs.extractorCommand === "list") {
|
|
421
|
+
const payload = buildExtractorListPayload(cwd);
|
|
422
|
+
if (json) console.log(stableStringify(payload));
|
|
423
|
+
else printExtractorList(payload);
|
|
424
|
+
return payload.ok ? 0 : 1;
|
|
425
|
+
}
|
|
426
|
+
if (commandArgs.extractorCommand === "show") {
|
|
427
|
+
const payload = buildExtractorShowPayload(inputPath || "", cwd);
|
|
428
|
+
if (json) console.log(stableStringify(payload));
|
|
429
|
+
else printExtractorShow(payload);
|
|
430
|
+
return payload.ok ? 0 : 1;
|
|
431
|
+
}
|
|
432
|
+
if (commandArgs.extractorPolicyCommand === "init") {
|
|
433
|
+
const payload = buildExtractorPolicyInitPayload(inputPath || ".");
|
|
434
|
+
if (json) console.log(stableStringify(payload));
|
|
435
|
+
else printExtractorPolicyInit(payload);
|
|
436
|
+
return 0;
|
|
437
|
+
}
|
|
438
|
+
if (commandArgs.extractorPolicyCommand === "status" || commandArgs.extractorPolicyCommand === "check" || commandArgs.extractorPolicyCommand === "explain") {
|
|
439
|
+
const payload = buildExtractorPolicyStatusPayload(inputPath || ".");
|
|
440
|
+
if (json) console.log(stableStringify(payload));
|
|
441
|
+
else printExtractorPolicyStatus(payload);
|
|
442
|
+
return payload.ok ? 0 : 1;
|
|
443
|
+
}
|
|
444
|
+
if (commandArgs.extractorPolicyCommand === "pin") {
|
|
445
|
+
const payload = buildExtractorPolicyPinPayload(inputPath || ".", commandArgs.extractorPolicyPinSpec);
|
|
446
|
+
if (json) console.log(stableStringify(payload));
|
|
447
|
+
else printExtractorPolicyPin(payload);
|
|
448
|
+
return payload.ok ? 0 : 1;
|
|
449
|
+
}
|
|
450
|
+
throw new Error(`Unknown extractor command '${commandArgs.extractorCommand || commandArgs.extractorPolicyCommand}'`);
|
|
451
|
+
}
|
|
@@ -4,7 +4,7 @@ import { TOPOGRAM_IMPORT_FILE } from "../../../import/provenance.js";
|
|
|
4
4
|
import { TOPOGRAM_IMPORT_ADOPTIONS_FILE } from "./paths.js";
|
|
5
5
|
|
|
6
6
|
export function printExtractHelp() {
|
|
7
|
-
console.log("Usage: topogram extract <app-path> --out <target> [--from <track[,track]>] [--json]");
|
|
7
|
+
console.log("Usage: topogram extract <app-path> --out <target> [--from <track[,track]>] [--extractor <id-or-package-or-path>] [--json]");
|
|
8
8
|
console.log(" or: topogram extract refresh [path] [--from <app-path>] [--dry-run] [--json]");
|
|
9
9
|
console.log(" or: topogram extract diff [path] [--json]");
|
|
10
10
|
console.log(" or: topogram extract check [path] [--json]");
|
|
@@ -26,6 +26,8 @@ export function printExtractHelp() {
|
|
|
26
26
|
console.log(" topogram extract ./existing-app --out ./extracted-topogram");
|
|
27
27
|
console.log(" topogram extract ./existing-app --out ./extracted-topogram --from db,api,ui");
|
|
28
28
|
console.log(" topogram extract ./existing-cli --out ./extracted-topogram --from cli");
|
|
29
|
+
console.log(" topogram extract ./existing-cli --out ./extracted-topogram --from cli --extractor @topogram/extractor-node-cli");
|
|
30
|
+
console.log(" topogram extract ./existing-app --out ./extracted-topogram --extractor-policy ./topogram.extractor-policy.json");
|
|
29
31
|
console.log(" topogram extract diff ./extracted-topogram");
|
|
30
32
|
console.log(" topogram extract refresh ./extracted-topogram --from ./existing-app --dry-run");
|
|
31
33
|
console.log(" topogram extract refresh ./extracted-topogram --from ./existing-app");
|
|
@@ -149,7 +149,7 @@ export function countFilesRecursive(rootPath) {
|
|
|
149
149
|
/**
|
|
150
150
|
* @param {string} sourcePath
|
|
151
151
|
* @param {string} targetPath
|
|
152
|
-
* @param {{ from?: string|null }} [options]
|
|
152
|
+
* @param {{ from?: string|null, extractorSpecs?: string[], extractorPolicyPath?: string|null, cwd?: string|null }} [options]
|
|
153
153
|
* @returns {{ ok: boolean, sourcePath: string, targetPath: string, workspaceRoot: string, topogramRoot: string, projectConfigPath: string, provenancePath: string, tracks: string[], sourceFiles: number, rawCandidateFiles: number, reconcileFiles: number, writtenFiles: string[], candidateCounts: Record<string, number>, nextCommands: string[] }}
|
|
154
154
|
*/
|
|
155
155
|
export function buildBrownfieldImportWorkspacePayload(sourcePath, targetPath, options = {}) {
|
|
@@ -166,7 +166,12 @@ export function buildBrownfieldImportWorkspacePayload(sourcePath, targetPath, op
|
|
|
166
166
|
const topogramRoot = path.join(targetRoot, DEFAULT_TOPO_FOLDER_NAME);
|
|
167
167
|
fs.mkdirSync(topogramRoot, { recursive: true });
|
|
168
168
|
const sourceFiles = collectImportSourceFileRecords(sourceRoot, { excludeRoots: [targetRoot] });
|
|
169
|
-
const importResult = runWorkflow("import-app", sourceRoot, {
|
|
169
|
+
const importResult = runWorkflow("import-app", sourceRoot, {
|
|
170
|
+
from: options.from || null,
|
|
171
|
+
extractorSpecs: options.extractorSpecs || [],
|
|
172
|
+
extractorPolicyPath: options.extractorPolicyPath || null,
|
|
173
|
+
cwd: options.cwd || process.cwd()
|
|
174
|
+
});
|
|
170
175
|
const rawCandidateFiles = writeRelativeFiles(topogramRoot, importResult.files || {});
|
|
171
176
|
|
|
172
177
|
const projectConfigPath = path.join(targetRoot, "topogram.project.json");
|
|
@@ -182,6 +187,7 @@ export function buildBrownfieldImportWorkspacePayload(sourcePath, targetPath, op
|
|
|
182
187
|
tracks: importResult.summary.tracks || [],
|
|
183
188
|
findingsCount: importResult.summary.findings_count || 0,
|
|
184
189
|
candidateCounts,
|
|
190
|
+
extractorPackages: importResult.summary.package_extractors || [],
|
|
185
191
|
files: sourceFiles
|
|
186
192
|
});
|
|
187
193
|
const writtenFiles = [
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
} from "./import.js";
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
* @param {{ commandArgs: Record<string, any>, inputPath: string|null|undefined, outPath?: string|null, fromValue?: string|null, reasonValue?: string|null, refreshAdopted?: boolean, dryRun?: boolean, write?: boolean, force?: boolean, json?: boolean }} context
|
|
28
|
+
* @param {{ commandArgs: Record<string, any>, inputPath: string|null|undefined, outPath?: string|null, fromValue?: string|null, extractorSpecs?: string[], extractorPolicyPath?: string|null, reasonValue?: string|null, refreshAdopted?: boolean, dryRun?: boolean, write?: boolean, force?: boolean, json?: boolean }} context
|
|
29
29
|
* @returns {number}
|
|
30
30
|
*/
|
|
31
31
|
export function runImportCommand(context) {
|
|
@@ -34,6 +34,8 @@ export function runImportCommand(context) {
|
|
|
34
34
|
inputPath,
|
|
35
35
|
outPath = null,
|
|
36
36
|
fromValue = null,
|
|
37
|
+
extractorSpecs = [],
|
|
38
|
+
extractorPolicyPath = null,
|
|
37
39
|
reasonValue = null,
|
|
38
40
|
refreshAdopted = false,
|
|
39
41
|
dryRun = false,
|
|
@@ -49,7 +51,7 @@ export function runImportCommand(context) {
|
|
|
49
51
|
printExtractHelp();
|
|
50
52
|
return 1;
|
|
51
53
|
}
|
|
52
|
-
const payload = buildBrownfieldImportWorkspacePayload(inputPath || "", outPath, { from: fromValue });
|
|
54
|
+
const payload = buildBrownfieldImportWorkspacePayload(inputPath || "", outPath, { from: fromValue, extractorSpecs, extractorPolicyPath, cwd: process.cwd() });
|
|
53
55
|
if (json) {
|
|
54
56
|
console.log(stableStringify(payload));
|
|
55
57
|
} else {
|
package/src/cli/dispatcher.js
CHANGED
|
@@ -8,6 +8,7 @@ import { runCheckCommand } from "./commands/check.js";
|
|
|
8
8
|
import { runCopyCommand } from "./commands/copy.js";
|
|
9
9
|
import { runEmitCommand } from "./commands/emit.js";
|
|
10
10
|
import { runGenerateAppCommand } from "./commands/generate.js";
|
|
11
|
+
import { runExtractorCommand } from "./commands/extractor.js";
|
|
11
12
|
import { runGeneratorCommand } from "./commands/generator.js";
|
|
12
13
|
import { runGeneratorPolicyCommand } from "./commands/generator-policy.js";
|
|
13
14
|
import { runImportCommand } from "./commands/import-runner.js";
|
|
@@ -81,6 +82,8 @@ export async function runCliDispatch(context) {
|
|
|
81
82
|
workflowName,
|
|
82
83
|
workflowId,
|
|
83
84
|
fromValue,
|
|
85
|
+
extractorSpecs,
|
|
86
|
+
extractorPolicyPath,
|
|
84
87
|
adoptValue,
|
|
85
88
|
reasonValue,
|
|
86
89
|
modeId,
|
|
@@ -225,6 +228,15 @@ export async function runCliDispatch(context) {
|
|
|
225
228
|
});
|
|
226
229
|
}
|
|
227
230
|
|
|
231
|
+
if (commandArgs?.extractorCommand || commandArgs?.extractorPolicyCommand) {
|
|
232
|
+
return runExtractorCommand({
|
|
233
|
+
commandArgs,
|
|
234
|
+
inputPath,
|
|
235
|
+
json: emitJson,
|
|
236
|
+
cwd: process.cwd()
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
228
240
|
if (commandArgs?.generatorPolicyCommand) {
|
|
229
241
|
return runGeneratorPolicyCommand({ commandArgs, inputPath, json: emitJson });
|
|
230
242
|
}
|
|
@@ -259,6 +271,8 @@ export async function runCliDispatch(context) {
|
|
|
259
271
|
inputPath,
|
|
260
272
|
outPath,
|
|
261
273
|
fromValue,
|
|
274
|
+
extractorSpecs,
|
|
275
|
+
extractorPolicyPath,
|
|
262
276
|
reasonValue,
|
|
263
277
|
refreshAdopted,
|
|
264
278
|
dryRun: args.includes("--dry-run"),
|
package/src/cli/help-dispatch.js
CHANGED
|
@@ -12,6 +12,9 @@ import {
|
|
|
12
12
|
import {
|
|
13
13
|
printDoctorHelp
|
|
14
14
|
} from "./commands/doctor.js";
|
|
15
|
+
import {
|
|
16
|
+
printExtractorHelp
|
|
17
|
+
} from "./commands/extractor.js";
|
|
15
18
|
import {
|
|
16
19
|
printGeneratorHelp
|
|
17
20
|
} from "./commands/generator.js";
|
|
@@ -86,6 +89,10 @@ export function printCommandHelp(command) {
|
|
|
86
89
|
printGeneratorHelp();
|
|
87
90
|
return true;
|
|
88
91
|
}
|
|
92
|
+
if (command === "extractor") {
|
|
93
|
+
printExtractorHelp();
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
89
96
|
if (command === "template") {
|
|
90
97
|
printTemplateHelp();
|
|
91
98
|
return true;
|
|
@@ -176,6 +183,10 @@ export function handleUnparsedCommandHelp(args) {
|
|
|
176
183
|
printGeneratorHelp();
|
|
177
184
|
return args[1] ? 1 : 0;
|
|
178
185
|
}
|
|
186
|
+
if (args[0] === "extractor") {
|
|
187
|
+
printExtractorHelp();
|
|
188
|
+
return args[1] ? 1 : 0;
|
|
189
|
+
}
|
|
179
190
|
if (args[0] === "template") {
|
|
180
191
|
printTemplateHelp();
|
|
181
192
|
return args[1] ? 1 : 0;
|