@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.
@@ -57,14 +57,80 @@ import { railsControllerEnricher } from "../enrichers/rails-controllers.js";
57
57
  import { workflowTargetStateEnricher } from "../enrichers/workflow-target-state.js";
58
58
  import { docLinkingEnricher } from "../enrichers/doc-linking.js";
59
59
 
60
- export const extractorRegistry = {
61
- db: [prismaExtractor, djangoModelsExtractor, efCoreExtractor, roomExtractor, swiftDataExtractor, dotnetModelsExtractor, flutterEntitiesExtractor, reactNativeEntitiesExtractor, railsSchemaExtractor, liquibaseExtractor, myBatisXmlExtractor, jpaExtractor, drizzleExtractor, sqlExtractor, snapshotExtractor],
62
- api: [openApiExtractor, openApiCodeExtractor, graphQlSdlExtractor, graphQlCodeFirstExtractor, trpcExtractor, aspNetCoreExtractor, retrofitExtractor, swiftWebApiExtractor, flutterDioExtractor, reactNativeRepositoryExtractor, fastifyExtractor, expressExtractor, djangoRoutesExtractor, railsRoutesExtractor, micronautExtractor, jaxRsExtractor, springWebExtractor, nextRouteExtractor, genericRouteFallbackExtractor, nextServerActionExtractor, nextAuthExtractor],
63
- ui: [nextAppRouterUiExtractor, nextPagesRouterUiExtractor, androidComposeUiExtractor, blazorUiExtractor, razorPagesUiExtractor, swiftUiExtractor, uiKitExtractor, mauiXamlUiExtractor, flutterScreensUiExtractor, reactNativeScreensExtractor, reactRouterUiExtractor, svelteKitUiExtractor, backendOnlyUiExtractor],
64
- cli: [genericCliExtractor],
65
- workflows: [genericWorkflowExtractor],
66
- verification: [genericVerificationExtractor]
67
- };
60
+ function extractorPack(id, tracks, extractors, candidateKinds, stack = {}, capabilities = {}) {
61
+ return {
62
+ manifest: {
63
+ id,
64
+ version: "1",
65
+ tracks,
66
+ source: "bundled",
67
+ extractors: extractors.map((extractor) => extractor.id),
68
+ stack,
69
+ capabilities,
70
+ candidateKinds,
71
+ evidenceTypes: ["runtime_source", "parser_config", "docs", "tests", "fixtures"]
72
+ },
73
+ extractors
74
+ };
75
+ }
76
+
77
+ export const BUILTIN_EXTRACTOR_PACKS = [
78
+ extractorPack(
79
+ "topogram/db-extractors",
80
+ ["db"],
81
+ [prismaExtractor, djangoModelsExtractor, efCoreExtractor, roomExtractor, swiftDataExtractor, dotnetModelsExtractor, flutterEntitiesExtractor, reactNativeEntitiesExtractor, railsSchemaExtractor, liquibaseExtractor, myBatisXmlExtractor, jpaExtractor, drizzleExtractor, sqlExtractor, snapshotExtractor],
82
+ ["entity", "enum", "relation", "index", "maintained_db_migration_seam"],
83
+ { domain: "database" },
84
+ { schema: true, migrations: true, maintainedSeams: true }
85
+ ),
86
+ extractorPack(
87
+ "topogram/api-extractors",
88
+ ["api"],
89
+ [openApiExtractor, openApiCodeExtractor, graphQlSdlExtractor, graphQlCodeFirstExtractor, trpcExtractor, aspNetCoreExtractor, retrofitExtractor, swiftWebApiExtractor, flutterDioExtractor, reactNativeRepositoryExtractor, fastifyExtractor, expressExtractor, djangoRoutesExtractor, railsRoutesExtractor, micronautExtractor, jaxRsExtractor, springWebExtractor, nextRouteExtractor, genericRouteFallbackExtractor, nextServerActionExtractor, nextAuthExtractor],
90
+ ["capability", "route", "stack"],
91
+ { domain: "api" },
92
+ { routes: true, openapi: true, graphql: true }
93
+ ),
94
+ extractorPack(
95
+ "topogram/ui-extractors",
96
+ ["ui"],
97
+ [nextAppRouterUiExtractor, nextPagesRouterUiExtractor, androidComposeUiExtractor, blazorUiExtractor, razorPagesUiExtractor, swiftUiExtractor, uiKitExtractor, mauiXamlUiExtractor, flutterScreensUiExtractor, reactNativeScreensExtractor, reactRouterUiExtractor, svelteKitUiExtractor, backendOnlyUiExtractor],
98
+ ["screen", "route", "action", "flow", "widget", "shape", "stack"],
99
+ { domain: "ui" },
100
+ { screens: true, widgets: true, flows: true }
101
+ ),
102
+ extractorPack(
103
+ "topogram/cli-extractors",
104
+ ["cli"],
105
+ [genericCliExtractor],
106
+ ["command", "capability", "cli_surface"],
107
+ { domain: "cli" },
108
+ { commands: true, options: true, effects: true }
109
+ ),
110
+ extractorPack(
111
+ "topogram/workflow-extractors",
112
+ ["workflows"],
113
+ [genericWorkflowExtractor],
114
+ ["workflow", "workflow_state", "workflow_transition"],
115
+ { domain: "workflow" },
116
+ { workflows: true }
117
+ ),
118
+ extractorPack(
119
+ "topogram/verification-extractors",
120
+ ["verification"],
121
+ [genericVerificationExtractor],
122
+ ["verification", "scenario", "framework", "script"],
123
+ { domain: "verification" },
124
+ { verifications: true }
125
+ )
126
+ ];
127
+
128
+ export const extractorRegistry = Object.fromEntries(
129
+ ["db", "api", "ui", "cli", "workflows", "verification"].map((track) => [
130
+ track,
131
+ BUILTIN_EXTRACTOR_PACKS.flatMap((pack) => pack.extractors).filter((extractor) => extractor.track === track)
132
+ ])
133
+ );
68
134
 
69
135
  export const enricherRegistry = {
70
136
  db: [railsModelEnricher],
@@ -82,3 +148,11 @@ export function getExtractorsForTrack(track) {
82
148
  export function getEnrichersForTrack(track) {
83
149
  return enricherRegistry[track] || [];
84
150
  }
151
+
152
+ export function getBundledExtractorPack(id) {
153
+ return BUILTIN_EXTRACTOR_PACKS.find((pack) => pack.manifest.id === id) || null;
154
+ }
155
+
156
+ export function getBundledExtractorById(id) {
157
+ return BUILTIN_EXTRACTOR_PACKS.flatMap((pack) => pack.extractors).find((extractor) => extractor.id === id) || null;
158
+ }
@@ -6,6 +6,7 @@ import { parseImportTracks } from "./options.js";
6
6
  import { appReportMarkdown, reportMarkdown } from "./reports.js";
7
7
  import { runTrack } from "./tracks.js";
8
8
  import { draftUiProjectionFiles } from "./ui-drafts.js";
9
+ import { packageExtractorsForContext } from "../../../extractor/packages.js";
9
10
 
10
11
  /**
11
12
  * @param {string} inputPath
@@ -64,6 +65,7 @@ export function runImportApp(inputPath, options = {}) {
64
65
  tracks,
65
66
  findings_count: Object.values(findings).reduce((total, entries) => total + entries.length, 0),
66
67
  extractor_detections: Object.fromEntries(Object.entries(resultsByTrack).map(([track, result]) => [track, result.extractor_detections])),
68
+ package_extractors: packageExtractorsForContext(context).provenance,
67
69
  candidates
68
70
  };
69
71
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { getEnrichersForTrack, getExtractorsForTrack } from "../registry.js";
4
4
  import { normalizeCandidatesForTrack } from "./candidates.js";
5
+ import { packageExtractorsForContext } from "../../../extractor/packages.js";
5
6
 
6
7
  /**
7
8
  * @param {any} context
@@ -10,7 +11,12 @@ import { normalizeCandidatesForTrack } from "./candidates.js";
10
11
  */
11
12
  function sortExtractors(context, extractors) {
12
13
  return extractors
13
- .map((extractor) => ({ extractor, detection: extractor.detect(context) || { score: 0, reasons: [] } }))
14
+ .map((extractor) => {
15
+ const extractorContext = extractor.source === "package" && typeof extractor.packageContext === "function"
16
+ ? extractor.packageContext(context)
17
+ : context;
18
+ return { extractor, detection: extractor.detect(extractorContext) || { score: 0, reasons: [] } };
19
+ })
14
20
  .filter((entry) => entry.detection.score > 0)
15
21
  .sort((a, b) => b.detection.score - a.detection.score || a.extractor.id.localeCompare(b.extractor.id));
16
22
  }
@@ -20,7 +26,7 @@ function sortExtractors(context, extractors) {
20
26
  * @param {Array<{ extractor: any, detection: any }>} detections
21
27
  * @returns {Array<{ extractor: any, detection: any }>}
22
28
  */
23
- function selectDetectionsForTrack(track, detections) {
29
+ function selectBundledDetectionsForTrack(track, detections) {
24
30
  if (track === "db") {
25
31
  const prisma = detections.find((entry) => entry.extractor.id === "db.prisma");
26
32
  if (prisma) return [prisma];
@@ -90,6 +96,20 @@ function selectDetectionsForTrack(track, detections) {
90
96
  return detections;
91
97
  }
92
98
 
99
+ /**
100
+ * @param {string} track
101
+ * @param {Array<{ extractor: any, detection: any }>} detections
102
+ * @returns {Array<{ extractor: any, detection: any }>}
103
+ */
104
+ function selectDetectionsForTrack(track, detections) {
105
+ const packageDetections = detections.filter((entry) => entry.extractor.source === "package");
106
+ const bundledDetections = selectBundledDetectionsForTrack(
107
+ track,
108
+ detections.filter((entry) => entry.extractor.source !== "package")
109
+ );
110
+ return [...bundledDetections, ...packageDetections];
111
+ }
112
+
93
113
  /**
94
114
  * @param {string} track
95
115
  * @returns {any}
@@ -113,6 +133,35 @@ function initialCandidatesForTrack(track) {
113
133
  return { workflows: [], workflow_states: [], workflow_transitions: [] };
114
134
  }
115
135
 
136
+ /**
137
+ * @param {any} extractor
138
+ * @param {any} result
139
+ * @returns {void}
140
+ */
141
+ function assertExtractorResultShape(extractor, result) {
142
+ const label = extractor?.id || "unknown";
143
+ if (!result || typeof result !== "object" || Array.isArray(result)) {
144
+ throw new Error(`Extractor '${label}' extract(context) must return an object.`);
145
+ }
146
+ if (result.findings != null && !Array.isArray(result.findings)) {
147
+ throw new Error(`Extractor '${label}' extract(context) findings must be an array when present.`);
148
+ }
149
+ if (result.diagnostics != null && !Array.isArray(result.diagnostics)) {
150
+ throw new Error(`Extractor '${label}' extract(context) diagnostics must be an array when present.`);
151
+ }
152
+ if (result.candidates == null) {
153
+ result.candidates = {};
154
+ }
155
+ if (!result.candidates || typeof result.candidates !== "object" || Array.isArray(result.candidates)) {
156
+ throw new Error(`Extractor '${label}' extract(context) candidates must be an object.`);
157
+ }
158
+ for (const [key, value] of Object.entries(result.candidates)) {
159
+ if (!Array.isArray(value)) {
160
+ throw new Error(`Extractor '${label}' extract(context) candidates.${key} must be an array.`);
161
+ }
162
+ }
163
+ }
164
+
116
165
  /**
117
166
  * @param {any} context
118
167
  * @param {string} track
@@ -121,9 +170,22 @@ function initialCandidatesForTrack(track) {
121
170
  export function runTrack(context, track) {
122
171
  const findings = [];
123
172
  const rawCandidates = initialCandidatesForTrack(track);
173
+ const packageState = packageExtractorsForContext(context);
174
+ const packageErrors = packageState.diagnostics.filter((diagnostic) => diagnostic.severity !== "warning");
175
+ if (packageErrors.length > 0) {
176
+ throw new Error(packageErrors.map((diagnostic) => diagnostic.message || String(diagnostic)).join("\n"));
177
+ }
178
+ const extractors = [
179
+ ...getExtractorsForTrack(track),
180
+ ...packageState.extractors.filter((extractor) => extractor.track === track)
181
+ ];
124
182
 
125
- for (const { extractor, detection } of selectDetectionsForTrack(track, sortExtractors(context, getExtractorsForTrack(track)))) {
126
- const result = extractor.extract(context) || { findings: [], candidates: {} };
183
+ for (const { extractor, detection } of selectDetectionsForTrack(track, sortExtractors(context, extractors))) {
184
+ const extractorContext = extractor.source === "package" && typeof extractor.packageContext === "function"
185
+ ? extractor.packageContext(context)
186
+ : context;
187
+ const result = extractor.extract(extractorContext) || { findings: [], candidates: {} };
188
+ assertExtractorResultShape(extractor, result);
127
189
  findings.push({
128
190
  extractor: extractor.id,
129
191
  detection,
@@ -53,7 +53,8 @@ export function writeTopogramImportRecord(projectRoot, input) {
53
53
  extract: {
54
54
  tracks: input.tracks || [],
55
55
  findingsCount: input.findingsCount || 0,
56
- candidateCounts: input.candidateCounts || {}
56
+ candidateCounts: input.candidateCounts || {},
57
+ extractorPackages: input.extractorPackages || []
57
58
  },
58
59
  ownership: {
59
60
  extractedArtifacts: "project-owned",
@@ -0,0 +1,64 @@
1
+ // @ts-check
2
+
3
+ import path from "node:path";
4
+ import { createRequire } from "node:module";
5
+
6
+ /**
7
+ * @param {any} moduleValue
8
+ * @param {string|null|undefined} exportName
9
+ * @returns {any}
10
+ */
11
+ export function selectPackageExport(moduleValue, exportName) {
12
+ if (exportName) {
13
+ return moduleValue?.[exportName] || moduleValue?.default?.[exportName] || null;
14
+ }
15
+ return moduleValue?.default || moduleValue;
16
+ }
17
+
18
+ /**
19
+ * @param {{
20
+ * packageRoot: string,
21
+ * exportName?: string|null,
22
+ * packageLabel: string
23
+ * }} options
24
+ * @returns {{ adapter: any|null, error: string|null }}
25
+ */
26
+ export function loadLocalPackageAdapter(options) {
27
+ try {
28
+ const packageJsonPath = path.join(options.packageRoot, "package.json");
29
+ const requireFromPackage = createRequire(packageJsonPath);
30
+ return {
31
+ adapter: selectPackageExport(requireFromPackage(options.packageRoot), options.exportName),
32
+ error: null
33
+ };
34
+ } catch (error) {
35
+ return {
36
+ adapter: null,
37
+ error: `${options.packageLabel} export could not be loaded from '${options.packageRoot}': ${error instanceof Error ? error.message : String(error)}`
38
+ };
39
+ }
40
+ }
41
+
42
+ /**
43
+ * @param {{
44
+ * packageName: string,
45
+ * rootDir: string,
46
+ * exportName?: string|null,
47
+ * packageLabel: string
48
+ * }} options
49
+ * @returns {{ adapter: any|null, error: string|null }}
50
+ */
51
+ export function loadInstalledPackageAdapter(options) {
52
+ try {
53
+ const requireFromRoot = createRequire(path.join(options.rootDir, "package.json"));
54
+ return {
55
+ adapter: selectPackageExport(requireFromRoot(options.packageName), options.exportName),
56
+ error: null
57
+ };
58
+ } catch (error) {
59
+ return {
60
+ adapter: null,
61
+ error: `${options.packageLabel} '${options.packageName}' export could not be loaded from '${options.rootDir}': ${error instanceof Error ? error.message : String(error)}`
62
+ };
63
+ }
64
+ }
@@ -0,0 +1,30 @@
1
+ // @ts-check
2
+
3
+ import path from "node:path";
4
+
5
+ /**
6
+ * @param {any} files
7
+ * @param {{ filePathMessage: string, contentMessage: (filePath: string) => string }} messages
8
+ * @returns {{ ok: boolean, message: string }}
9
+ */
10
+ export function validateRelativeStringFileMap(files, messages) {
11
+ if (!files || typeof files !== "object" || Array.isArray(files)) {
12
+ return { ok: false, message: "files must be an object" };
13
+ }
14
+ for (const [filePath, content] of Object.entries(files)) {
15
+ const normalizedPath = typeof filePath === "string" ? path.normalize(filePath) : "";
16
+ if (
17
+ typeof filePath !== "string" ||
18
+ filePath.length === 0 ||
19
+ path.isAbsolute(filePath) ||
20
+ normalizedPath === ".." ||
21
+ normalizedPath.startsWith(`..${path.sep}`)
22
+ ) {
23
+ return { ok: false, message: messages.filePathMessage };
24
+ }
25
+ if (typeof content !== "string") {
26
+ return { ok: false, message: messages.contentMessage(filePath) };
27
+ }
28
+ }
29
+ return { ok: true, message: "files are valid" };
30
+ }
@@ -0,0 +1,27 @@
1
+ // @ts-check
2
+
3
+ export {
4
+ loadInstalledPackageAdapter,
5
+ loadLocalPackageAdapter,
6
+ selectPackageExport
7
+ } from "./adapter.js";
8
+ export {
9
+ loadPackageManifest,
10
+ resolvePackageManifestPath
11
+ } from "./manifest.js";
12
+ export {
13
+ isPathSpec,
14
+ packageInstallCommand,
15
+ packageInstallHint,
16
+ packageNameFromSpec,
17
+ packageResolutionBase
18
+ } from "./spec.js";
19
+ export {
20
+ optionalStringArray,
21
+ optionalStringRecord,
22
+ packageAllowedByPolicy,
23
+ packageScopeFromName
24
+ } from "./policy.js";
25
+ export {
26
+ validateRelativeStringFileMap
27
+ } from "./file-map.js";
@@ -0,0 +1,108 @@
1
+ // @ts-check
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { createRequire } from "node:module";
6
+
7
+ import { packageInstallHint, packageResolutionBase } from "./spec.js";
8
+
9
+ /**
10
+ * @typedef {Object} PackageManifestResolution
11
+ * @property {string|null} manifestPath
12
+ * @property {string|null} packageRoot
13
+ * @property {string|null} error
14
+ */
15
+
16
+ /**
17
+ * @typedef {{ ok: boolean, errors: string[] }} ManifestValidation
18
+ */
19
+
20
+ /**
21
+ * @param {string} packageName
22
+ * @param {string} manifestFile
23
+ * @param {string|null|undefined} rootDir
24
+ * @param {string} packageLabel
25
+ * @returns {PackageManifestResolution}
26
+ */
27
+ export function resolvePackageManifestPath(packageName, manifestFile, rootDir = process.cwd(), packageLabel = "Package") {
28
+ const requireFromRoot = createRequire(packageResolutionBase(rootDir));
29
+ try {
30
+ const manifestPath = requireFromRoot.resolve(`${packageName}/${manifestFile}`);
31
+ return {
32
+ manifestPath,
33
+ packageRoot: path.dirname(manifestPath),
34
+ error: null
35
+ };
36
+ } catch (manifestError) {
37
+ try {
38
+ const packageJsonPath = requireFromRoot.resolve(`${packageName}/package.json`);
39
+ const packageRoot = path.dirname(packageJsonPath);
40
+ const manifestPath = path.join(packageRoot, manifestFile);
41
+ if (!fs.existsSync(manifestPath)) {
42
+ return {
43
+ manifestPath: null,
44
+ packageRoot,
45
+ error: `${packageLabel} '${packageName}' is missing ${manifestFile}`
46
+ };
47
+ }
48
+ return {
49
+ manifestPath,
50
+ packageRoot,
51
+ error: null
52
+ };
53
+ } catch {
54
+ const detail = manifestError instanceof Error ? manifestError.message : String(manifestError);
55
+ const installHint = packageInstallHint(packageName);
56
+ return {
57
+ manifestPath: null,
58
+ packageRoot: null,
59
+ error: `${packageLabel} '${packageName}' could not be resolved from '${rootDir || process.cwd()}': ${detail}${installHint ? `. ${installHint}` : ""}`
60
+ };
61
+ }
62
+ }
63
+ }
64
+
65
+ /**
66
+ * @template T
67
+ * @param {{
68
+ * packageName: string,
69
+ * rootDir?: string|null,
70
+ * manifestFile: string,
71
+ * packageLabel: string,
72
+ * validateManifest: (manifest: any) => ManifestValidation
73
+ * }} options
74
+ * @returns {{ manifest: T|null, errors: string[], manifestPath: string|null, packageRoot: string|null }}
75
+ */
76
+ export function loadPackageManifest(options) {
77
+ const resolved = resolvePackageManifestPath(
78
+ options.packageName,
79
+ options.manifestFile,
80
+ options.rootDir || process.cwd(),
81
+ options.packageLabel
82
+ );
83
+ if (!resolved.manifestPath) {
84
+ return {
85
+ manifest: null,
86
+ errors: [resolved.error || `${options.packageLabel} '${options.packageName}' could not be resolved`],
87
+ manifestPath: null,
88
+ packageRoot: resolved.packageRoot
89
+ };
90
+ }
91
+ try {
92
+ const manifest = JSON.parse(fs.readFileSync(resolved.manifestPath, "utf8"));
93
+ const validation = options.validateManifest(manifest);
94
+ return {
95
+ manifest: validation.ok ? /** @type {T} */ (manifest) : null,
96
+ errors: validation.errors,
97
+ manifestPath: resolved.manifestPath,
98
+ packageRoot: resolved.packageRoot
99
+ };
100
+ } catch (error) {
101
+ return {
102
+ manifest: null,
103
+ errors: [`${options.packageLabel} '${options.packageName}' manifest could not be read: ${error instanceof Error ? error.message : String(error)}`],
104
+ manifestPath: resolved.manifestPath,
105
+ packageRoot: resolved.packageRoot
106
+ };
107
+ }
108
+ }
@@ -0,0 +1,81 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * @typedef {Object} PackagePolicy
5
+ * @property {string} version
6
+ * @property {string[]} allowedPackageScopes
7
+ * @property {string[]} allowedPackages
8
+ * @property {Record<string, string>} pinnedVersions
9
+ */
10
+
11
+ /**
12
+ * @param {unknown} value
13
+ * @param {string} fieldName
14
+ * @param {string} policyPath
15
+ * @returns {string[]}
16
+ */
17
+ export function optionalStringArray(value, fieldName, policyPath) {
18
+ if (value == null) {
19
+ return [];
20
+ }
21
+ if (!Array.isArray(value)) {
22
+ throw new Error(`${policyPath} ${fieldName} must be an array of strings.`);
23
+ }
24
+ return value.map((item) => {
25
+ if (typeof item !== "string" || item.length === 0) {
26
+ throw new Error(`${policyPath} ${fieldName} must contain only non-empty strings.`);
27
+ }
28
+ return item;
29
+ });
30
+ }
31
+
32
+ /**
33
+ * @param {unknown} value
34
+ * @param {string} policyPath
35
+ * @param {string} [pinnedLabel]
36
+ * @returns {Record<string, string>}
37
+ */
38
+ export function optionalStringRecord(value, policyPath, pinnedLabel = "package ids") {
39
+ if (value == null) {
40
+ return {};
41
+ }
42
+ if (typeof value !== "object" || Array.isArray(value)) {
43
+ throw new Error(`${policyPath} pinnedVersions must be an object of ${pinnedLabel} to versions.`);
44
+ }
45
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => {
46
+ if (typeof item !== "string" || item.length === 0) {
47
+ throw new Error(`${policyPath} pinnedVersions['${key}'] must be a non-empty string.`);
48
+ }
49
+ return [key, item];
50
+ }));
51
+ }
52
+
53
+ /**
54
+ * @param {string} packageName
55
+ * @returns {string|null}
56
+ */
57
+ export function packageScopeFromName(packageName) {
58
+ return packageName.startsWith("@") ? packageName.split("/")[0] || null : null;
59
+ }
60
+
61
+ /**
62
+ * @param {string} allowed
63
+ * @param {string|null} scope
64
+ * @returns {boolean}
65
+ */
66
+ function packageScopeMatches(allowed, scope) {
67
+ return Boolean(scope && (allowed === scope || allowed === `${scope}/*`));
68
+ }
69
+
70
+ /**
71
+ * @param {PackagePolicy} policy
72
+ * @param {string} packageName
73
+ * @returns {boolean}
74
+ */
75
+ export function packageAllowedByPolicy(policy, packageName) {
76
+ if (policy.allowedPackages.includes(packageName)) {
77
+ return true;
78
+ }
79
+ const scope = packageScopeFromName(packageName);
80
+ return policy.allowedPackageScopes.some((allowed) => packageScopeMatches(allowed, scope));
81
+ }
@@ -0,0 +1,51 @@
1
+ // @ts-check
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+
6
+ /**
7
+ * @param {string} spec
8
+ * @param {string} cwd
9
+ * @returns {boolean}
10
+ */
11
+ export function isPathSpec(spec, cwd) {
12
+ return spec.startsWith(".") || spec.startsWith("/") || fs.existsSync(path.resolve(cwd, spec));
13
+ }
14
+
15
+ /**
16
+ * @param {string} spec
17
+ * @returns {string}
18
+ */
19
+ export function packageNameFromSpec(spec) {
20
+ if (spec.startsWith("@")) {
21
+ const versionIndex = spec.indexOf("@", 1);
22
+ return versionIndex > 0 ? spec.slice(0, versionIndex) : spec;
23
+ }
24
+ const versionIndex = spec.indexOf("@");
25
+ return versionIndex > 0 ? spec.slice(0, versionIndex) : spec;
26
+ }
27
+
28
+ /**
29
+ * @param {string|null|undefined} rootDir
30
+ * @returns {string}
31
+ */
32
+ export function packageResolutionBase(rootDir) {
33
+ return path.join(rootDir || process.cwd(), "package.json");
34
+ }
35
+
36
+ /**
37
+ * @param {string|null|undefined} packageName
38
+ * @returns {string|null}
39
+ */
40
+ export function packageInstallCommand(packageName) {
41
+ return packageName ? `npm install -D ${packageName}` : null;
42
+ }
43
+
44
+ /**
45
+ * @param {string|null|undefined} packageName
46
+ * @returns {string|null}
47
+ */
48
+ export function packageInstallHint(packageName) {
49
+ const command = packageInstallCommand(packageName);
50
+ return command ? `Install it from the project root with: ${command}` : null;
51
+ }
@@ -16,8 +16,17 @@ export const DEFAULT_FIRST_PARTY_GENERATOR_REPOS = [
16
16
  "topogram-generator-vanilla-web"
17
17
  ];
18
18
 
19
+ export const DEFAULT_FIRST_PARTY_EXTRACTOR_REPOS = [
20
+ "topogram-extractor-node-cli",
21
+ "topogram-extractor-react-router",
22
+ "topogram-extractor-prisma-db",
23
+ "topogram-extractor-express-api",
24
+ "topogram-extractor-drizzle-db"
25
+ ];
26
+
19
27
  export const DEFAULT_RELEASE_CONSUMER_REPOS = [
20
28
  ...DEFAULT_FIRST_PARTY_GENERATOR_REPOS,
29
+ ...DEFAULT_FIRST_PARTY_EXTRACTOR_REPOS,
21
30
  "topogram-starters",
22
31
  "topogram-template-todo",
23
32
  "topogram-demo-todo",
@@ -34,6 +43,11 @@ export const DEFAULT_RELEASE_CONSUMER_WORKFLOWS = {
34
43
  "topogram-generator-sveltekit-web": "Generator Verification",
35
44
  "topogram-generator-swiftui-native": "Generator Verification",
36
45
  "topogram-generator-vanilla-web": "Generator Verification",
46
+ "topogram-extractor-node-cli": "Extractor Verification",
47
+ "topogram-extractor-react-router": "Extractor Verification",
48
+ "topogram-extractor-prisma-db": "Extractor Verification",
49
+ "topogram-extractor-express-api": "Extractor Verification",
50
+ "topogram-extractor-drizzle-db": "Extractor Verification",
37
51
  "topogram-starters": "Starter Verification",
38
52
  "topogram-template-todo": "Template Verification",
39
53
  "topogram-demo-todo": "Demo Verification",