@topogram/cli 0.3.79 → 0.3.81

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.81",
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,621 @@
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 { FIRST_PARTY_EXTRACTOR_PACKAGES, firstPartyExtractorInfo } from "../../extractor/first-party.js";
9
+ import {
10
+ EXTRACTOR_MANIFESTS,
11
+ getExtractorManifest,
12
+ loadPackageExtractorManifest,
13
+ packageExtractorInstallCommand
14
+ } from "../../extractor/registry.js";
15
+ import {
16
+ defaultExtractorPolicy,
17
+ effectiveExtractorPolicy,
18
+ EXTRACTOR_POLICY_FILE,
19
+ extractorPackageAllowed,
20
+ extractorPolicyDiagnosticsForPackages,
21
+ loadExtractorPolicy,
22
+ packageScopeFromName,
23
+ parseExtractorPolicyPin,
24
+ writeExtractorPolicy
25
+ } from "../../extractor-policy.js";
26
+
27
+ const EXTRACTOR_TRACK_ORDER = ["db", "api", "ui", "cli", "workflows", "verification", "unknown"];
28
+
29
+ export function printExtractorHelp() {
30
+ console.log("Usage: topogram extractor list [--json]");
31
+ console.log(" or: topogram extractor show <id-or-package> [--json]");
32
+ console.log(" or: topogram extractor check <path-or-package> [--json]");
33
+ console.log(" or: topogram extractor policy init [path] [--json]");
34
+ console.log(" or: topogram extractor policy status [path] [--json]");
35
+ console.log(" or: topogram extractor policy check [path] [--json]");
36
+ console.log(" or: topogram extractor policy explain [path] [--json]");
37
+ console.log(" or: topogram extractor policy pin [package@version] [path] [--json]");
38
+ console.log("");
39
+ console.log("Inspects extractor manifests and checks extractor pack conformance.");
40
+ console.log("");
41
+ console.log("Notes:");
42
+ console.log(" - extractor packages execute only during `topogram extract` or `topogram extractor check`.");
43
+ console.log(" - extractor packages emit review-only candidates; core owns persistence, reconcile, and adoption.");
44
+ console.log(` - package-backed extractors are governed by ${EXTRACTOR_POLICY_FILE}; bundled topogram/* extractors are allowed.`);
45
+ console.log("");
46
+ console.log("Examples:");
47
+ console.log(" topogram extractor list");
48
+ console.log(" topogram extractor show topogram/api-extractors");
49
+ console.log(" topogram extractor show @topogram/extractor-prisma-db");
50
+ console.log(" topogram extractor check ./extractor-package");
51
+ console.log(" topogram extractor policy init");
52
+ console.log(" topogram extractor policy pin @topogram/extractor-node-cli@1");
53
+ console.log(" topogram extract ./express-api --out ./imported-topogram --from api --extractor @topogram/extractor-express-api");
54
+ }
55
+
56
+ /**
57
+ * @param {string} cwd
58
+ * @returns {string[]}
59
+ */
60
+ function declaredExtractorPackages(cwd) {
61
+ const packagePath = path.join(cwd, "package.json");
62
+ if (!fs.existsSync(packagePath)) {
63
+ return [];
64
+ }
65
+ const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
66
+ const dependencyBuckets = [
67
+ packageJson.dependencies,
68
+ packageJson.devDependencies,
69
+ packageJson.optionalDependencies,
70
+ packageJson.peerDependencies
71
+ ];
72
+ const packages = new Set();
73
+ for (const dependencies of dependencyBuckets) {
74
+ if (!dependencies || typeof dependencies !== "object" || Array.isArray(dependencies)) {
75
+ continue;
76
+ }
77
+ for (const name of Object.keys(dependencies)) {
78
+ if (name.includes("topogram-extractor") || name.startsWith("@topogram/extractor-")) {
79
+ packages.add(name);
80
+ }
81
+ }
82
+ }
83
+ return [...packages].sort();
84
+ }
85
+
86
+ /**
87
+ * @param {string|null|undefined} packageName
88
+ * @param {string|null|undefined} version
89
+ * @returns {string|null}
90
+ */
91
+ function extractorPolicyPinCommand(packageName, version) {
92
+ return packageName ? `topogram extractor policy pin ${packageName}@${version || "1"}` : null;
93
+ }
94
+
95
+ /**
96
+ * @param {string|null|undefined} packageName
97
+ * @param {string[]} tracks
98
+ * @param {string|null|undefined} exampleSource
99
+ * @returns {string|null}
100
+ */
101
+ function extractorRunCommand(packageName, tracks, exampleSource) {
102
+ if (!packageName) {
103
+ return null;
104
+ }
105
+ const trackList = tracks.length > 0 ? tracks.join(",") : "db,api,ui,cli";
106
+ return `topogram extract ${exampleSource || "./existing-app"} --out ./imported-topogram --from ${trackList} --extractor ${packageName}`;
107
+ }
108
+
109
+ /**
110
+ * @param {Record<string, any>} extractor
111
+ * @returns {{ id: string|null, package: string|null, label: string|null, source: string, installed: boolean, knownFirstParty: boolean, useWhen: string|null, extractCommand: string|null }}
112
+ */
113
+ function groupExtractorEntry(extractor) {
114
+ return {
115
+ id: extractor.id || null,
116
+ package: extractor.package || null,
117
+ label: extractor.label || null,
118
+ source: extractor.source,
119
+ installed: Boolean(extractor.installed),
120
+ knownFirstParty: Boolean(extractor.knownFirstParty),
121
+ useWhen: extractor.useWhen || null,
122
+ extractCommand: extractor.extractCommand || null
123
+ };
124
+ }
125
+
126
+ /**
127
+ * @param {Record<string, any>[]} extractors
128
+ * @returns {Record<string, ReturnType<typeof groupExtractorEntry>[]>}
129
+ */
130
+ function groupExtractorsByTrack(extractors) {
131
+ /** @type {Record<string, ReturnType<typeof groupExtractorEntry>[]>} */
132
+ const groups = {};
133
+ for (const track of EXTRACTOR_TRACK_ORDER) {
134
+ groups[track] = [];
135
+ }
136
+ for (const extractor of extractors) {
137
+ const tracks = Array.isArray(extractor.tracks) && extractor.tracks.length > 0 ? extractor.tracks : ["unknown"];
138
+ for (const track of tracks) {
139
+ if (!groups[track]) {
140
+ groups[track] = [];
141
+ }
142
+ groups[track].push(groupExtractorEntry(extractor));
143
+ }
144
+ }
145
+ for (const entries of Object.values(groups)) {
146
+ entries.sort((left, right) => String(left.label || left.id || left.package || "").localeCompare(String(right.label || right.id || right.package || "")));
147
+ }
148
+ return Object.fromEntries(Object.entries(groups).filter(([, entries]) => entries.length > 0));
149
+ }
150
+
151
+ /**
152
+ * @param {any} manifest
153
+ * @param {{ installed?: boolean, manifestPath?: string|null, packageRoot?: string|null, errors?: string[] }} [metadata]
154
+ * @returns {Record<string, any>}
155
+ */
156
+ function extractorManifestSummary(manifest, metadata = {}) {
157
+ const firstParty = firstPartyExtractorInfo(manifest.package || manifest.id);
158
+ const packageName = manifest.package || null;
159
+ const tracks = manifest.tracks || [];
160
+ const version = manifest.version || firstParty?.version || "1";
161
+ const installCommand = packageName ? packageExtractorInstallCommand(packageName) : null;
162
+ const policyPinCommand = extractorPolicyPinCommand(packageName, version);
163
+ const extractCommand = extractorRunCommand(packageName, tracks, firstParty?.exampleSource);
164
+ return {
165
+ id: manifest.id,
166
+ version,
167
+ label: firstParty?.label || null,
168
+ tracks,
169
+ extractors: manifest.extractors || [],
170
+ stack: manifest.stack || {},
171
+ capabilities: manifest.capabilities || {},
172
+ candidateKinds: manifest.candidateKinds || [],
173
+ evidenceTypes: manifest.evidenceTypes || [],
174
+ useWhen: firstParty?.useWhen || null,
175
+ extracts: firstParty?.extracts || [],
176
+ knownFirstParty: Boolean(firstParty),
177
+ source: manifest.source,
178
+ loadsAdapter: false,
179
+ executesPackageCode: false,
180
+ ...(packageName ? { package: packageName } : {}),
181
+ ...(installCommand ? { installCommand } : {}),
182
+ ...(policyPinCommand ? { policyPinCommand } : {}),
183
+ ...(extractCommand ? { extractCommand } : {}),
184
+ ...(packageName ? { showCommand: `topogram extractor show ${packageName}` } : {}),
185
+ installed: metadata.installed !== false,
186
+ manifestPath: metadata.manifestPath || null,
187
+ packageRoot: metadata.packageRoot || null,
188
+ errors: metadata.errors || []
189
+ };
190
+ }
191
+
192
+ /**
193
+ * @param {typeof FIRST_PARTY_EXTRACTOR_PACKAGES[number]} info
194
+ * @returns {Record<string, any>}
195
+ */
196
+ function firstPartyExtractorPlaceholder(info) {
197
+ return {
198
+ id: info.id,
199
+ version: info.version,
200
+ label: info.label,
201
+ tracks: info.tracks,
202
+ extractors: info.extractors,
203
+ stack: info.stack,
204
+ capabilities: info.capabilities,
205
+ candidateKinds: info.candidateKinds,
206
+ evidenceTypes: info.evidenceTypes,
207
+ useWhen: info.useWhen,
208
+ extracts: info.extracts,
209
+ knownFirstParty: true,
210
+ source: "package",
211
+ loadsAdapter: false,
212
+ executesPackageCode: false,
213
+ package: info.package,
214
+ installCommand: packageExtractorInstallCommand(info.package),
215
+ policyPinCommand: extractorPolicyPinCommand(info.package, info.version),
216
+ extractCommand: extractorRunCommand(info.package, info.tracks, info.exampleSource),
217
+ showCommand: `topogram extractor show ${info.package}`,
218
+ installed: false,
219
+ manifestPath: null,
220
+ packageRoot: null,
221
+ errors: []
222
+ };
223
+ }
224
+
225
+ /**
226
+ * @param {string} cwd
227
+ * @returns {{ ok: boolean, cwd: string, extractors: Record<string, any>[], groups: Record<string, ReturnType<typeof groupExtractorEntry>[]>, summary: Record<string, number> }}
228
+ */
229
+ export function buildExtractorListPayload(cwd) {
230
+ const extractors = EXTRACTOR_MANIFESTS
231
+ .map((manifest) => extractorManifestSummary(manifest))
232
+ .sort((left, right) => left.id.localeCompare(right.id));
233
+ const seenPackages = new Set(extractors.map((extractor) => extractor.package).filter(Boolean));
234
+ const seenIds = new Set(extractors.map((extractor) => extractor.id).filter(Boolean));
235
+ for (const packageName of declaredExtractorPackages(cwd)) {
236
+ const loaded = loadPackageExtractorManifest(packageName, cwd);
237
+ if (loaded.manifest) {
238
+ const summary = extractorManifestSummary(loaded.manifest, {
239
+ installed: true,
240
+ manifestPath: loaded.manifestPath,
241
+ packageRoot: loaded.packageRoot,
242
+ errors: loaded.errors
243
+ });
244
+ extractors.push(summary);
245
+ if (summary.package) seenPackages.add(summary.package);
246
+ if (summary.id) seenIds.add(summary.id);
247
+ } else {
248
+ const firstParty = firstPartyExtractorInfo(packageName);
249
+ const fallback = firstParty ? firstPartyExtractorPlaceholder(firstParty) : {
250
+ id: null,
251
+ version: null,
252
+ tracks: [],
253
+ extractors: [],
254
+ stack: {},
255
+ capabilities: {},
256
+ candidateKinds: [],
257
+ evidenceTypes: [],
258
+ useWhen: null,
259
+ extracts: [],
260
+ knownFirstParty: false,
261
+ source: "package",
262
+ package: packageName,
263
+ installCommand: packageExtractorInstallCommand(packageName),
264
+ policyPinCommand: null,
265
+ extractCommand: null,
266
+ showCommand: `topogram extractor show ${packageName}`,
267
+ installed: false,
268
+ manifestPath: loaded.manifestPath,
269
+ packageRoot: loaded.packageRoot,
270
+ errors: loaded.errors
271
+ };
272
+ extractors.push({ ...fallback, errors: loaded.errors });
273
+ seenPackages.add(packageName);
274
+ if (fallback.id) seenIds.add(fallback.id);
275
+ }
276
+ }
277
+ for (const firstParty of FIRST_PARTY_EXTRACTOR_PACKAGES) {
278
+ if (!seenPackages.has(firstParty.package) && !seenIds.has(firstParty.id)) {
279
+ extractors.push(firstPartyExtractorPlaceholder(firstParty));
280
+ }
281
+ }
282
+ extractors.sort((left, right) => String(left.id || left.package || "").localeCompare(String(right.id || right.package || "")));
283
+ const groups = groupExtractorsByTrack(extractors);
284
+ return {
285
+ ok: extractors.every((extractor) => extractor.errors.length === 0),
286
+ cwd,
287
+ extractors,
288
+ groups,
289
+ summary: {
290
+ total: extractors.length,
291
+ bundled: extractors.filter((extractor) => extractor.source === "bundled").length,
292
+ package: extractors.filter((extractor) => extractor.source === "package").length,
293
+ installed: extractors.filter((extractor) => extractor.installed).length,
294
+ knownFirstParty: extractors.filter((extractor) => extractor.knownFirstParty).length,
295
+ missingFirstParty: extractors.filter((extractor) => extractor.knownFirstParty && !extractor.installed).length
296
+ }
297
+ };
298
+ }
299
+
300
+ /**
301
+ * @param {string} spec
302
+ * @param {string} cwd
303
+ * @returns {{ ok: boolean, sourceSpec: string, extractor: Record<string, any>|null, errors: string[] }}
304
+ */
305
+ export function buildExtractorShowPayload(spec, cwd) {
306
+ if (!spec || spec.startsWith("-")) {
307
+ return { ok: false, sourceSpec: spec || "", extractor: null, errors: ["Usage: topogram extractor show <id-or-package>"] };
308
+ }
309
+ const bundled = getExtractorManifest(spec);
310
+ if (bundled) {
311
+ return { ok: true, sourceSpec: spec, extractor: extractorManifestSummary(bundled), errors: [] };
312
+ }
313
+ const loaded = loadPackageExtractorManifest(spec, cwd);
314
+ if (loaded.manifest) {
315
+ return {
316
+ ok: true,
317
+ sourceSpec: spec,
318
+ extractor: extractorManifestSummary(loaded.manifest, {
319
+ installed: true,
320
+ manifestPath: loaded.manifestPath,
321
+ packageRoot: loaded.packageRoot,
322
+ errors: loaded.errors
323
+ }),
324
+ errors: []
325
+ };
326
+ }
327
+ const firstParty = firstPartyExtractorInfo(spec);
328
+ if (firstParty) {
329
+ return { ok: true, sourceSpec: spec, extractor: firstPartyExtractorPlaceholder(firstParty), errors: [] };
330
+ }
331
+ return { ok: false, sourceSpec: spec, extractor: null, errors: loaded.errors };
332
+ }
333
+
334
+ /**
335
+ * @param {ReturnType<typeof buildExtractorListPayload>} payload
336
+ * @returns {void}
337
+ */
338
+ export function printExtractorList(payload) {
339
+ console.log("Topogram extractors");
340
+ console.log(`Bundled: ${payload.summary.bundled}; package-backed: ${payload.summary.package}; installed: ${payload.summary.installed}; first-party missing: ${payload.summary.missingFirstParty || 0}`);
341
+ console.log("Package-backed extractors are listed for discovery even before they are installed.");
342
+ console.log("");
343
+ for (const track of EXTRACTOR_TRACK_ORDER) {
344
+ const entries = (payload.groups || {})[track] || [];
345
+ if (entries.length === 0) {
346
+ continue;
347
+ }
348
+ console.log(`${track}:`);
349
+ for (const entry of entries) {
350
+ const extractor = /** @type {Record<string, any>} */ (payload.extractors.find((item) => (entry.package && item.package === entry.package) || (entry.id && item.id === entry.id)) || entry);
351
+ const id = extractor.id || extractor.package || "unknown";
352
+ const status = extractor.errors.length > 0
353
+ ? "invalid"
354
+ : extractor.source === "package"
355
+ ? (extractor.installed ? "package installed" : "package missing")
356
+ : "bundled";
357
+ console.log(`- ${extractor.label ? `${extractor.label} ` : ""}${id}${extractor.version ? `@${extractor.version}` : ""} (${status})`);
358
+ if (extractor.useWhen) console.log(` Use: ${extractor.useWhen}`);
359
+ console.log(` Source: ${extractor.source}`);
360
+ console.log(" Adapter loaded: no");
361
+ console.log(" Executes package code: no");
362
+ console.log(` Extractors: ${extractor.extractors.join(", ") || "none"}`);
363
+ if (extractor.package) console.log(` Package: ${extractor.package}`);
364
+ if (extractor.installCommand) console.log(` Install: ${extractor.installCommand}`);
365
+ if (extractor.policyPinCommand) console.log(` Policy: ${extractor.policyPinCommand}`);
366
+ if (extractor.extractCommand) console.log(` Extract: ${extractor.extractCommand}`);
367
+ for (const error of extractor.errors || []) console.log(` Error: ${error}`);
368
+ }
369
+ console.log("");
370
+ }
371
+ }
372
+
373
+ /**
374
+ * @param {ReturnType<typeof buildExtractorShowPayload>} payload
375
+ * @returns {void}
376
+ */
377
+ export function printExtractorShow(payload) {
378
+ if (!payload.ok || !payload.extractor) {
379
+ console.log("Extractor pack not found.");
380
+ for (const error of payload.errors || []) console.log(`- ${error}`);
381
+ return;
382
+ }
383
+ const extractor = payload.extractor;
384
+ console.log(`Extractor pack: ${extractor.id}@${extractor.version}`);
385
+ console.log(`Tracks: ${extractor.tracks.join(", ") || "none"}`);
386
+ console.log(`Source: ${extractor.source}`);
387
+ console.log("Adapter loaded: no");
388
+ console.log("Executes package code: no");
389
+ if (extractor.label) console.log(`Label: ${extractor.label}`);
390
+ if (extractor.useWhen) console.log(`Use: ${extractor.useWhen}`);
391
+ if (extractor.extracts?.length) console.log(`Extracts: ${extractor.extracts.join(", ")}`);
392
+ console.log(`Installed: ${extractor.installed ? "yes" : "no"}`);
393
+ if (extractor.package) console.log(`Package: ${extractor.package}`);
394
+ if (extractor.installCommand) console.log(`Install: ${extractor.installCommand}`);
395
+ if (extractor.policyPinCommand) console.log(`Policy: ${extractor.policyPinCommand}`);
396
+ if (extractor.extractCommand) console.log(`Extract: ${extractor.extractCommand}`);
397
+ if (extractor.manifestPath) console.log(`Manifest: ${extractor.manifestPath}`);
398
+ console.log(`Extractors: ${extractor.extractors.join(", ") || "none"}`);
399
+ console.log(`Candidate kinds: ${extractor.candidateKinds.join(", ") || "none"}`);
400
+ console.log(`Evidence types: ${extractor.evidenceTypes.join(", ") || "none"}`);
401
+ }
402
+
403
+ /**
404
+ * @param {ReturnType<typeof checkExtractorPack>} payload
405
+ * @returns {void}
406
+ */
407
+ export function printExtractorCheck(payload) {
408
+ console.log(payload.ok ? "Extractor check passed." : "Extractor check found issues.");
409
+ console.log(`Source: ${payload.sourceSpec}`);
410
+ console.log(`Type: ${payload.source}`);
411
+ if (payload.packageName) console.log(`Package: ${payload.packageName}`);
412
+ if (payload.manifestPath) console.log(`Manifest: ${payload.manifestPath}`);
413
+ if (payload.manifest) {
414
+ console.log(`Extractor pack: ${payload.manifest.id}@${payload.manifest.version}`);
415
+ console.log(`Tracks: ${payload.manifest.tracks.join(", ")}`);
416
+ console.log(`Source mode: ${payload.manifest.source}`);
417
+ }
418
+ console.log("Executes package code: yes (loads adapter and runs smoke extract)");
419
+ console.log("");
420
+ console.log("Checks:");
421
+ for (const check of payload.checks || []) {
422
+ console.log(`- ${check.ok ? "PASS" : "FAIL"} ${check.name}: ${check.message}`);
423
+ }
424
+ if (payload.smoke) {
425
+ console.log("");
426
+ 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)`);
427
+ }
428
+ for (const error of payload.errors || []) console.log(`Error: ${error}`);
429
+ }
430
+
431
+ /**
432
+ * @param {string} projectPath
433
+ * @returns {{ ok: boolean, path: string, exists: boolean, policy: any, defaulted: boolean, packages: any[], diagnostics: any[], errors: string[], summary: Record<string, number> }}
434
+ */
435
+ export function buildExtractorPolicyStatusPayload(projectPath) {
436
+ const root = path.resolve(projectPath || ".");
437
+ const policyInfo = loadExtractorPolicy(root);
438
+ const policy = effectiveExtractorPolicy(policyInfo);
439
+ const packages = policy.enabledPackages.map((packageName) => {
440
+ const loaded = loadPackageExtractorManifest(packageName, root);
441
+ const version = loaded.manifest?.version || "unknown";
442
+ const diagnostics = extractorPolicyDiagnosticsForPackages(policyInfo, [{ packageName, version }], "extractor-policy");
443
+ return {
444
+ packageName,
445
+ version,
446
+ allowed: extractorPackageAllowed(policy, packageName),
447
+ installed: Boolean(loaded.manifest),
448
+ knownFirstParty: Boolean(firstPartyExtractorInfo(packageName)),
449
+ installCommand: packageExtractorInstallCommand(packageName),
450
+ showCommand: `topogram extractor show ${packageName}`,
451
+ manifestPath: loaded.manifestPath,
452
+ packageRoot: loaded.packageRoot,
453
+ errors: [...loaded.errors, ...diagnostics.filter((diagnostic) => diagnostic.severity === "error").map((diagnostic) => diagnostic.message)]
454
+ };
455
+ });
456
+ const diagnostics = [
457
+ ...policyInfo.diagnostics,
458
+ ...packages.flatMap((item) => item.errors.map((message) => ({
459
+ code: "extractor_package_failed",
460
+ severity: "error",
461
+ message,
462
+ path: item.manifestPath,
463
+ suggestedFix: `Review or remove '${item.packageName}' from ${EXTRACTOR_POLICY_FILE}.`,
464
+ step: "extractor-policy",
465
+ packageName: item.packageName,
466
+ version: item.version
467
+ })))
468
+ ];
469
+ const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error").map((diagnostic) => diagnostic.message);
470
+ return {
471
+ ok: errors.length === 0,
472
+ path: policyInfo.path,
473
+ exists: policyInfo.exists,
474
+ policy,
475
+ defaulted: !policyInfo.exists,
476
+ packages,
477
+ diagnostics,
478
+ errors,
479
+ summary: {
480
+ enabledPackages: policy.enabledPackages.length,
481
+ installed: packages.filter((item) => item.installed).length,
482
+ allowed: packages.filter((item) => item.allowed).length,
483
+ denied: packages.filter((item) => !item.allowed).length
484
+ }
485
+ };
486
+ }
487
+
488
+ /**
489
+ * @param {string} projectPath
490
+ * @returns {{ ok: boolean, path: string, policy: any, diagnostics: any[], errors: string[] }}
491
+ */
492
+ export function buildExtractorPolicyInitPayload(projectPath) {
493
+ const root = path.resolve(projectPath || ".");
494
+ const policy = writeExtractorPolicy(root, defaultExtractorPolicy());
495
+ return { ok: true, path: path.join(root, EXTRACTOR_POLICY_FILE), policy, diagnostics: [], errors: [] };
496
+ }
497
+
498
+ /**
499
+ * @param {string} projectPath
500
+ * @param {string|null|undefined} spec
501
+ * @returns {{ ok: boolean, path: string, policy: any, pinned: Array<{ packageName: string, version: string }>, diagnostics: any[], errors: string[] }}
502
+ */
503
+ export function buildExtractorPolicyPinPayload(projectPath, spec) {
504
+ const root = path.resolve(projectPath || ".");
505
+ const policyInfo = loadExtractorPolicy(root);
506
+ const policy = policyInfo.policy || defaultExtractorPolicy();
507
+ let pin;
508
+ try {
509
+ pin = parseExtractorPolicyPin(spec || "");
510
+ } catch (error) {
511
+ return {
512
+ ok: false,
513
+ path: policyInfo.path,
514
+ policy,
515
+ pinned: [],
516
+ diagnostics: [{ severity: "error", code: "extractor_policy_pin_invalid", message: error instanceof Error ? error.message : String(error), path: policyInfo.path }],
517
+ errors: [error instanceof Error ? error.message : String(error)]
518
+ };
519
+ }
520
+ const nextPolicy = {
521
+ ...policy,
522
+ allowedPackages: policy.allowedPackages.includes(pin.packageName) ? policy.allowedPackages : [...policy.allowedPackages, pin.packageName],
523
+ enabledPackages: policy.enabledPackages.includes(pin.packageName) ? policy.enabledPackages : [...policy.enabledPackages, pin.packageName],
524
+ pinnedVersions: { ...policy.pinnedVersions, [pin.packageName]: pin.version }
525
+ };
526
+ writeExtractorPolicy(root, nextPolicy);
527
+ return { ok: true, path: path.join(root, EXTRACTOR_POLICY_FILE), policy: nextPolicy, pinned: [pin], diagnostics: [], errors: [] };
528
+ }
529
+
530
+ /**
531
+ * @param {ReturnType<typeof buildExtractorPolicyStatusPayload>} payload
532
+ * @returns {void}
533
+ */
534
+ export function printExtractorPolicyStatus(payload) {
535
+ console.log(payload.ok ? "Extractor policy status: allowed" : "Extractor policy status: denied");
536
+ console.log(`Policy file: ${payload.path}`);
537
+ console.log(`Policy file exists: ${payload.exists ? "yes" : "no"}`);
538
+ console.log(`Default policy active: ${payload.defaulted ? "yes" : "no"}`);
539
+ console.log(`Enabled packages: ${payload.summary.enabledPackages}`);
540
+ console.log("Default allowlist: bundled topogram/* extractors and first-party @topogram/extractor-* packages.");
541
+ console.log("Install behavior: Topogram does not install extractor packages automatically.");
542
+ for (const item of payload.packages) {
543
+ console.log(`- ${item.packageName}@${item.version}: ${item.installed ? "installed" : "missing"}, ${item.allowed ? "allowed" : "denied"}`);
544
+ if (!item.installed && item.installCommand) console.log(` Install: ${item.installCommand}`);
545
+ if (item.showCommand) console.log(` Show: ${item.showCommand}`);
546
+ }
547
+ for (const diagnostic of payload.diagnostics) {
548
+ console.log(`[${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`);
549
+ }
550
+ }
551
+
552
+ /**
553
+ * @param {ReturnType<typeof buildExtractorPolicyInitPayload>} payload
554
+ * @returns {void}
555
+ */
556
+ export function printExtractorPolicyInit(payload) {
557
+ console.log(`Wrote extractor policy: ${payload.path}`);
558
+ console.log(`Allowed package scopes: ${payload.policy.allowedPackageScopes.join(", ") || "(none)"}`);
559
+ console.log(`Allowed packages: ${payload.policy.allowedPackages.join(", ") || "(none)"}`);
560
+ console.log(`Enabled packages: ${payload.policy.enabledPackages.join(", ") || "(none)"}`);
561
+ }
562
+
563
+ /**
564
+ * @param {ReturnType<typeof buildExtractorPolicyPinPayload>} payload
565
+ * @returns {void}
566
+ */
567
+ export function printExtractorPolicyPin(payload) {
568
+ console.log(payload.ok ? "Extractor policy pin updated" : "Extractor policy pin failed");
569
+ console.log(`Policy: ${payload.path}`);
570
+ for (const pin of payload.pinned || []) {
571
+ console.log(`Pinned: ${pin.packageName}@${pin.version}`);
572
+ }
573
+ for (const diagnostic of payload.diagnostics || []) {
574
+ console.log(`[${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`);
575
+ }
576
+ }
577
+
578
+ /**
579
+ * @param {{ commandArgs: Record<string, any>, inputPath: string|null|undefined, json: boolean, cwd: string }} context
580
+ * @returns {number}
581
+ */
582
+ export function runExtractorCommand(context) {
583
+ const { commandArgs, inputPath, json, cwd } = context;
584
+ if (commandArgs.extractorCommand === "check") {
585
+ const payload = checkExtractorPack(inputPath || "", { cwd });
586
+ if (json) console.log(stableStringify(payload));
587
+ else printExtractorCheck(payload);
588
+ return payload.ok ? 0 : 1;
589
+ }
590
+ if (commandArgs.extractorCommand === "list") {
591
+ const payload = buildExtractorListPayload(cwd);
592
+ if (json) console.log(stableStringify(payload));
593
+ else printExtractorList(payload);
594
+ return payload.ok ? 0 : 1;
595
+ }
596
+ if (commandArgs.extractorCommand === "show") {
597
+ const payload = buildExtractorShowPayload(inputPath || "", cwd);
598
+ if (json) console.log(stableStringify(payload));
599
+ else printExtractorShow(payload);
600
+ return payload.ok ? 0 : 1;
601
+ }
602
+ if (commandArgs.extractorPolicyCommand === "init") {
603
+ const payload = buildExtractorPolicyInitPayload(inputPath || ".");
604
+ if (json) console.log(stableStringify(payload));
605
+ else printExtractorPolicyInit(payload);
606
+ return 0;
607
+ }
608
+ if (commandArgs.extractorPolicyCommand === "status" || commandArgs.extractorPolicyCommand === "check" || commandArgs.extractorPolicyCommand === "explain") {
609
+ const payload = buildExtractorPolicyStatusPayload(inputPath || ".");
610
+ if (json) console.log(stableStringify(payload));
611
+ else printExtractorPolicyStatus(payload);
612
+ return payload.ok ? 0 : 1;
613
+ }
614
+ if (commandArgs.extractorPolicyCommand === "pin") {
615
+ const payload = buildExtractorPolicyPinPayload(inputPath || ".", commandArgs.extractorPolicyPinSpec);
616
+ if (json) console.log(stableStringify(payload));
617
+ else printExtractorPolicyPin(payload);
618
+ return payload.ok ? 0 : 1;
619
+ }
620
+ throw new Error(`Unknown extractor command '${commandArgs.extractorCommand || commandArgs.extractorPolicyCommand}'`);
621
+ }