@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topogram/cli",
3
- "version": "0.3.79",
3
+ "version": "0.3.80",
4
4
  "description": "Topogram CLI for checking Topogram workspaces and generating app bundles.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -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, { from: options.from || null });
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 {
@@ -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"),
@@ -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;