@principal-ai/codebase-composition 0.2.10 → 0.2.12

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.
Files changed (109) hide show
  1. package/dist/adapters/NodeFileSystemAdapter.d.ts +28 -0
  2. package/dist/adapters/NodeFileSystemAdapter.d.ts.map +1 -0
  3. package/dist/adapters/NodeFileSystemAdapter.js +128 -0
  4. package/dist/adapters/NodeFileSystemAdapter.js.map +1 -0
  5. package/dist/helpers/QualityMetricsCalculator.d.ts +51 -0
  6. package/dist/helpers/QualityMetricsCalculator.d.ts.map +1 -0
  7. package/dist/helpers/QualityMetricsCalculator.js +200 -0
  8. package/dist/helpers/QualityMetricsCalculator.js.map +1 -0
  9. package/dist/helpers/packageLayerHelpers.d.ts +30 -0
  10. package/dist/helpers/packageLayerHelpers.d.ts.map +1 -0
  11. package/dist/helpers/packageLayerHelpers.js +88 -0
  12. package/dist/helpers/packageLayerHelpers.js.map +1 -0
  13. package/dist/index.d.ts +11 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +14 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/modules/DependencyLayerModule.d.ts +44 -0
  18. package/dist/modules/DependencyLayerModule.d.ts.map +1 -0
  19. package/dist/modules/DependencyLayerModule.js +282 -0
  20. package/dist/modules/DependencyLayerModule.js.map +1 -0
  21. package/dist/modules/FileSystemModule.d.ts +30 -0
  22. package/dist/modules/FileSystemModule.d.ts.map +1 -0
  23. package/dist/modules/FileSystemModule.js +43 -0
  24. package/dist/modules/FileSystemModule.js.map +1 -0
  25. package/dist/modules/FileTypeLayerModule.d.ts +34 -0
  26. package/dist/modules/FileTypeLayerModule.d.ts.map +1 -0
  27. package/dist/modules/FileTypeLayerModule.js +165 -0
  28. package/dist/modules/FileTypeLayerModule.js.map +1 -0
  29. package/dist/modules/FrameworkLayerModule.d.ts +22 -0
  30. package/dist/modules/FrameworkLayerModule.d.ts.map +1 -0
  31. package/dist/modules/FrameworkLayerModule.js +384 -0
  32. package/dist/modules/FrameworkLayerModule.js.map +1 -0
  33. package/dist/modules/PackageLayerModule.d.ts +23 -0
  34. package/dist/modules/PackageLayerModule.d.ts.map +1 -0
  35. package/dist/modules/PackageLayerModule.js +866 -0
  36. package/dist/modules/PackageLayerModule.js.map +1 -0
  37. package/dist/modules/TypeExtractionModule.d.ts +37 -0
  38. package/dist/modules/TypeExtractionModule.d.ts.map +1 -0
  39. package/dist/modules/TypeExtractionModule.js +141 -0
  40. package/dist/modules/TypeExtractionModule.js.map +1 -0
  41. package/dist/modules/VersionControlLayerModule.d.ts +10 -0
  42. package/dist/modules/VersionControlLayerModule.d.ts.map +1 -0
  43. package/dist/modules/VersionControlLayerModule.js +26 -0
  44. package/dist/modules/VersionControlLayerModule.js.map +1 -0
  45. package/dist/modules/__fixtures__/typescript-packages/complex-types/src/index.d.ts +4 -0
  46. package/dist/modules/__fixtures__/typescript-packages/complex-types/src/index.d.ts.map +1 -0
  47. package/dist/modules/__fixtures__/typescript-packages/complex-types/src/index.js +3 -0
  48. package/dist/modules/__fixtures__/typescript-packages/complex-types/src/index.js.map +1 -0
  49. package/dist/modules/__fixtures__/typescript-packages/complex-types/src/models/category.d.ts +15 -0
  50. package/dist/modules/__fixtures__/typescript-packages/complex-types/src/models/category.d.ts.map +1 -0
  51. package/dist/modules/__fixtures__/typescript-packages/complex-types/src/models/category.js +2 -0
  52. package/dist/modules/__fixtures__/typescript-packages/complex-types/src/models/category.js.map +1 -0
  53. package/dist/modules/__fixtures__/typescript-packages/complex-types/src/models/product.d.ts +34 -0
  54. package/dist/modules/__fixtures__/typescript-packages/complex-types/src/models/product.d.ts.map +1 -0
  55. package/dist/modules/__fixtures__/typescript-packages/complex-types/src/models/product.js +19 -0
  56. package/dist/modules/__fixtures__/typescript-packages/complex-types/src/models/product.js.map +1 -0
  57. package/dist/modules/__fixtures__/typescript-packages/simple-types/index.d.ts +39 -0
  58. package/dist/modules/__fixtures__/typescript-packages/simple-types/index.d.ts.map +1 -0
  59. package/dist/modules/__fixtures__/typescript-packages/simple-types/index.js +36 -0
  60. package/dist/modules/__fixtures__/typescript-packages/simple-types/index.js.map +1 -0
  61. package/dist/modules/extractors/TypeScriptExtractor.d.ts +18 -0
  62. package/dist/modules/extractors/TypeScriptExtractor.d.ts.map +1 -0
  63. package/dist/modules/extractors/TypeScriptExtractor.js +322 -0
  64. package/dist/modules/extractors/TypeScriptExtractor.js.map +1 -0
  65. package/dist/modules/index.d.ts +13 -0
  66. package/dist/modules/index.d.ts.map +1 -0
  67. package/dist/modules/index.js +10 -0
  68. package/dist/modules/index.js.map +1 -0
  69. package/dist/providers/GitVersionControlProvider.d.ts +108 -0
  70. package/dist/providers/GitVersionControlProvider.d.ts.map +1 -0
  71. package/dist/providers/GitVersionControlProvider.js +375 -0
  72. package/dist/providers/GitVersionControlProvider.js.map +1 -0
  73. package/dist/providers/PackageManagerApiProvider.d.ts +78 -0
  74. package/dist/providers/PackageManagerApiProvider.d.ts.map +1 -0
  75. package/dist/providers/PackageManagerApiProvider.js +10 -0
  76. package/dist/providers/PackageManagerApiProvider.js.map +1 -0
  77. package/dist/providers/index.d.ts +4 -0
  78. package/dist/providers/index.d.ts.map +1 -0
  79. package/dist/providers/index.js +5 -0
  80. package/dist/providers/index.js.map +1 -0
  81. package/dist/services/FilesystemService.d.ts +59 -0
  82. package/dist/services/FilesystemService.d.ts.map +1 -0
  83. package/dist/services/FilesystemService.js +394 -0
  84. package/dist/services/FilesystemService.js.map +1 -0
  85. package/dist/services/index.d.ts +2 -0
  86. package/dist/services/index.d.ts.map +1 -0
  87. package/dist/services/index.js +3 -0
  88. package/dist/services/index.js.map +1 -0
  89. package/dist/types/file-system.d.ts +6 -0
  90. package/dist/types/file-system.d.ts.map +1 -0
  91. package/dist/types/file-system.js +6 -0
  92. package/dist/types/file-system.js.map +1 -0
  93. package/dist/types/index.d.ts +4 -0
  94. package/dist/types/index.d.ts.map +1 -0
  95. package/dist/types/index.js +2 -0
  96. package/dist/types/index.js.map +1 -0
  97. package/dist/types/layer-types.d.ts +212 -0
  98. package/dist/types/layer-types.d.ts.map +1 -0
  99. package/dist/types/layer-types.js +6 -0
  100. package/dist/types/layer-types.js.map +1 -0
  101. package/dist/types/version-control-layer.d.ts +53 -0
  102. package/dist/types/version-control-layer.d.ts.map +1 -0
  103. package/dist/types/version-control-layer.js +2 -0
  104. package/dist/types/version-control-layer.js.map +1 -0
  105. package/dist/types/workspace-boundaries.d.ts +17 -0
  106. package/dist/types/workspace-boundaries.d.ts.map +1 -0
  107. package/dist/types/workspace-boundaries.js +6 -0
  108. package/dist/types/workspace-boundaries.js.map +1 -0
  109. package/package.json +1 -1
@@ -0,0 +1,866 @@
1
+ import * as TOML from 'js-toml';
2
+ import { parsePipRequirementsFile } from 'pip-requirements-js';
3
+ import { QualityMetricsCalculator } from '../helpers/QualityMetricsCalculator.js';
4
+ // Node.js package.json parser
5
+ class NodePackageParser {
6
+ manifestFileName = 'package.json';
7
+ packageType = 'node';
8
+ canParse(filename) {
9
+ return filename.endsWith('package.json');
10
+ }
11
+ parseContent(rawContent) {
12
+ try {
13
+ return JSON.parse(rawContent);
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ }
19
+ extractPackageData(content, path) {
20
+ // Handle cases where path is just "package.json" (root package)
21
+ // or a proper path like "packages/foo/package.json"
22
+ let packagePath;
23
+ if (path === 'package.json') {
24
+ packagePath = ''; // Root directory - empty string for root
25
+ }
26
+ else if (path.endsWith('/package.json')) {
27
+ packagePath = path.slice(0, -13); // Remove '/package.json'
28
+ }
29
+ else {
30
+ // Shouldn't happen, but handle gracefully
31
+ packagePath = path;
32
+ }
33
+ const availableCommands = this.extractCommands(content, packagePath);
34
+ // Extract author - can be string or object
35
+ let author;
36
+ if (content.author) {
37
+ if (typeof content.author === 'string') {
38
+ author = content.author;
39
+ }
40
+ else if (content.author.name) {
41
+ // Format: "Name <email> (url)"
42
+ let authorStr = content.author.name;
43
+ if (content.author.email)
44
+ authorStr += ` <${content.author.email}>`;
45
+ if (content.author.url)
46
+ authorStr += ` (${content.author.url})`;
47
+ author = authorStr;
48
+ }
49
+ }
50
+ // Extract repository URL - can be string or object
51
+ let repository;
52
+ if (content.repository) {
53
+ if (typeof content.repository === 'string') {
54
+ repository = content.repository;
55
+ }
56
+ else if (content.repository.url) {
57
+ repository = content.repository.url;
58
+ }
59
+ }
60
+ return {
61
+ name: content.name || 'unnamed',
62
+ version: content.version,
63
+ path: packagePath,
64
+ manifestPath: path, // The full relative path to package.json
65
+ packageManager: 'unknown',
66
+ dependencies: content.dependencies || {},
67
+ devDependencies: content.devDependencies || {},
68
+ peerDependencies: content.peerDependencies || {},
69
+ description: content.description,
70
+ license: content.license,
71
+ author,
72
+ repository,
73
+ homepage: content.homepage,
74
+ keywords: content.keywords,
75
+ isMonorepoRoot: !!content.workspaces,
76
+ isWorkspace: false, // Will be determined by context
77
+ parentPackage: undefined,
78
+ availableCommands,
79
+ };
80
+ }
81
+ extractCommands(content, packagePath) {
82
+ const commands = [];
83
+ // Extract npm scripts
84
+ if (content.scripts) {
85
+ Object.entries(content.scripts).forEach(([name, script]) => {
86
+ // Use npm as default, will be updated with correct package manager later
87
+ commands.push({
88
+ name,
89
+ command: `npm run ${name}`,
90
+ description: script.length > 50 ? script.substring(0, 47) + '...' : script,
91
+ type: 'script',
92
+ workingDirectory: packagePath,
93
+ });
94
+ });
95
+ }
96
+ // Add standard npm commands - these will also be updated with correct package manager
97
+ const standardCommands = [
98
+ { name: 'install', command: 'npm install', description: 'Install dependencies' },
99
+ { name: 'ci', command: 'npm ci', description: 'Clean install dependencies' },
100
+ { name: 'update', command: 'npm update', description: 'Update dependencies' },
101
+ ];
102
+ standardCommands.forEach(cmd => {
103
+ commands.push({
104
+ ...cmd,
105
+ type: 'standard',
106
+ workingDirectory: packagePath,
107
+ });
108
+ });
109
+ return commands;
110
+ }
111
+ detectWorkspaces(content) {
112
+ if (Array.isArray(content.workspaces)) {
113
+ return content.workspaces;
114
+ }
115
+ if (content.workspaces &&
116
+ typeof content.workspaces === 'object' &&
117
+ 'packages' in content.workspaces) {
118
+ return content.workspaces.packages || null;
119
+ }
120
+ return null;
121
+ }
122
+ detectPackageManager(content, lockFiles) {
123
+ // Check for packageManager field (corepack)
124
+ if (content.packageManager) {
125
+ const pm = content.packageManager.split('@')[0];
126
+ if (['npm', 'yarn', 'pnpm'].includes(pm)) {
127
+ return pm;
128
+ }
129
+ }
130
+ // Check lock files
131
+ if (lockFiles.some((f) => f.endsWith('yarn.lock')))
132
+ return 'yarn';
133
+ if (lockFiles.some((f) => f.endsWith('pnpm-lock.yaml')))
134
+ return 'pnpm';
135
+ if (lockFiles.some((f) => f.endsWith('package-lock.json')))
136
+ return 'npm';
137
+ // Default to npm instead of unknown for Node packages
138
+ return 'npm';
139
+ }
140
+ detectConfigs(packagePath, fileTree, manifestContent) {
141
+ const configs = {};
142
+ // Define config patterns for Node/JavaScript projects
143
+ const configPatterns = {
144
+ knip: [
145
+ 'knip.json',
146
+ 'knip.jsonc',
147
+ '.knip.json',
148
+ 'knip.config.js',
149
+ 'knip.config.ts',
150
+ 'knip.config.mjs',
151
+ ],
152
+ eslint: [
153
+ '.eslintrc',
154
+ '.eslintrc.js',
155
+ '.eslintrc.json',
156
+ '.eslintrc.yml',
157
+ '.eslintrc.yaml',
158
+ 'eslint.config.js',
159
+ 'eslint.config.mjs',
160
+ '.eslintrc.cjs',
161
+ ],
162
+ prettier: [
163
+ '.prettierrc',
164
+ '.prettierrc.js',
165
+ '.prettierrc.json',
166
+ '.prettierrc.yml',
167
+ '.prettierrc.yaml',
168
+ 'prettier.config.js',
169
+ '.prettierrc.toml',
170
+ ],
171
+ typescript: ['tsconfig.json', 'tsconfig.base.json'],
172
+ jest: [
173
+ 'jest.config.js',
174
+ 'jest.config.ts',
175
+ 'jest.config.mjs',
176
+ 'jest.config.cjs',
177
+ 'jest.config.json',
178
+ ],
179
+ vitest: ['vitest.config.js', 'vitest.config.ts', 'vitest.config.mjs'],
180
+ webpack: ['webpack.config.js', 'webpack.config.ts', 'webpack.config.mjs'],
181
+ vite: ['vite.config.js', 'vite.config.ts', 'vite.config.mjs'],
182
+ rollup: ['rollup.config.js', 'rollup.config.ts', 'rollup.config.mjs'],
183
+ babel: ['.babelrc', '.babelrc.js', '.babelrc.json', 'babel.config.js', 'babel.config.json'],
184
+ dockerfile: ['Dockerfile', 'dockerfile', 'Dockerfile.dev', 'Dockerfile.prod'],
185
+ gitignore: ['.gitignore'],
186
+ editorconfig: ['.editorconfig'],
187
+ };
188
+ // Normalize package path
189
+ const normalizedPath = packagePath === '.' || packagePath === ''
190
+ ? ''
191
+ : packagePath.endsWith('/')
192
+ ? packagePath
193
+ : packagePath + '/';
194
+ // Check files in tree
195
+ if (fileTree.allFiles) {
196
+ for (const [tool, patterns] of Object.entries(configPatterns)) {
197
+ for (const pattern of patterns) {
198
+ const fullPath = normalizedPath + pattern;
199
+ const fileExists = fileTree.allFiles.some(f => f.path === fullPath || f.relativePath === fullPath);
200
+ if (fileExists) {
201
+ configs[tool] = {
202
+ path: fullPath,
203
+ exists: true,
204
+ type: this.getConfigFileType(pattern),
205
+ };
206
+ break; // Found config for this tool
207
+ }
208
+ }
209
+ }
210
+ }
211
+ // Check for inline configs in package.json
212
+ if (manifestContent) {
213
+ if (manifestContent.eslintConfig) {
214
+ configs.eslint = {
215
+ path: normalizedPath + 'package.json',
216
+ exists: true,
217
+ type: 'json',
218
+ isInline: true,
219
+ inlineField: 'eslintConfig',
220
+ };
221
+ }
222
+ if (manifestContent.prettier) {
223
+ configs.prettier = {
224
+ path: normalizedPath + 'package.json',
225
+ exists: true,
226
+ type: 'json',
227
+ isInline: true,
228
+ inlineField: 'prettier',
229
+ };
230
+ }
231
+ if (manifestContent.jest) {
232
+ configs.jest = {
233
+ path: normalizedPath + 'package.json',
234
+ exists: true,
235
+ type: 'json',
236
+ isInline: true,
237
+ inlineField: 'jest',
238
+ };
239
+ }
240
+ if (manifestContent.babel) {
241
+ configs.babel = {
242
+ path: normalizedPath + 'package.json',
243
+ exists: true,
244
+ type: 'json',
245
+ isInline: true,
246
+ inlineField: 'babel',
247
+ };
248
+ }
249
+ }
250
+ return configs;
251
+ }
252
+ getConfigFileType(filename) {
253
+ if (filename.endsWith('.json') || filename.endsWith('.jsonc'))
254
+ return 'json';
255
+ if (filename.endsWith('.yml') || filename.endsWith('.yaml'))
256
+ return 'yaml';
257
+ if (filename.endsWith('.toml'))
258
+ return 'toml';
259
+ if (filename.endsWith('.js') || filename.endsWith('.mjs') || filename.endsWith('.cjs'))
260
+ return 'js';
261
+ if (filename.endsWith('.ts') || filename.endsWith('.mts') || filename.endsWith('.cts'))
262
+ return 'ts';
263
+ if (filename.endsWith('.ini'))
264
+ return 'ini';
265
+ return 'custom';
266
+ }
267
+ detectDocsFolder(packagePath, fileTree, _manifestContent) {
268
+ // Look for common documentation folder names
269
+ const docsFolderNames = ['docs', 'documentation', 'doc'];
270
+ // Normalize package path
271
+ let packagePrefix = packagePath === '.' || packagePath === '' ? '' : packagePath;
272
+ if (packagePrefix && !packagePrefix.endsWith('/')) {
273
+ packagePrefix += '/';
274
+ }
275
+ if (!fileTree.allFiles)
276
+ return undefined;
277
+ // Look for root-level docs folders first (preferred)
278
+ for (const folderName of docsFolderNames) {
279
+ const expectedPath = packagePrefix + folderName;
280
+ const hasFiles = fileTree.allFiles.some(file => file.path?.toLowerCase().startsWith(expectedPath.toLowerCase() + '/'));
281
+ if (hasFiles) {
282
+ // Find the actual folder name with correct case
283
+ const actualPath = fileTree.allFiles.find(file => file.path?.toLowerCase().startsWith(expectedPath.toLowerCase() + '/'))?.path;
284
+ if (actualPath) {
285
+ const actualFolderPath = actualPath.substring(0, actualPath.indexOf('/', packagePrefix.length));
286
+ return actualFolderPath.substring(packagePrefix.length);
287
+ }
288
+ return folderName; // Fallback to lowercase version
289
+ }
290
+ }
291
+ // If no explicit docs folder found, look for directories with high concentration of docs
292
+ const potentialFolders = new Map();
293
+ for (const file of fileTree.allFiles) {
294
+ if (!file.path)
295
+ continue;
296
+ // Only consider files within this package
297
+ if (packagePrefix !== '' && !file.path.startsWith(packagePrefix)) {
298
+ continue;
299
+ }
300
+ const relativePath = packagePrefix === '' ? file.path : file.path.substring(packagePrefix.length);
301
+ const pathParts = relativePath.split('/');
302
+ // Only look at direct subdirectories (depth 1)
303
+ if (pathParts.length === 2) {
304
+ const dirName = pathParts[0];
305
+ const fileName = pathParts[1];
306
+ if (!potentialFolders.has(dirName)) {
307
+ potentialFolders.set(dirName, { docFiles: 0, totalFiles: 0 });
308
+ }
309
+ const stats = potentialFolders.get(dirName);
310
+ stats.totalFiles++;
311
+ if (this.isDocumentationFile(fileName)) {
312
+ stats.docFiles++;
313
+ }
314
+ }
315
+ }
316
+ // Find the best candidate folder (highest percentage of docs, minimum 2 doc files)
317
+ let bestFolder;
318
+ let bestScore = 0.5; // Must be at least 50% docs
319
+ for (const [folderName, stats] of Array.from(potentialFolders.entries())) {
320
+ if (stats.docFiles >= 2) {
321
+ const score = stats.docFiles / stats.totalFiles;
322
+ if (score > bestScore) {
323
+ bestScore = score;
324
+ bestFolder = folderName;
325
+ }
326
+ }
327
+ }
328
+ return bestFolder;
329
+ }
330
+ isDocumentationFile(fileName) {
331
+ const lowerName = fileName.toLowerCase();
332
+ // Documentation file extensions
333
+ const docExtensions = ['.md', '.mdx', '.rst', '.txt', '.adoc', '.asciidoc'];
334
+ const hasDocExtension = docExtensions.some(ext => lowerName.endsWith(ext));
335
+ if (hasDocExtension)
336
+ return true;
337
+ // Documentation file name patterns
338
+ const docPatterns = [
339
+ /^readme/i,
340
+ /^changelog/i,
341
+ /^changes/i,
342
+ /^history/i,
343
+ /^license/i,
344
+ /^copying/i,
345
+ /^install/i,
346
+ /^usage/i,
347
+ /^guide/i,
348
+ /^tutorial/i,
349
+ /^manual/i,
350
+ /^faq/i,
351
+ /^api/i,
352
+ /^reference/i,
353
+ /^spec/i,
354
+ /^specification/i,
355
+ ];
356
+ return docPatterns.some(pattern => pattern.test(fileName));
357
+ }
358
+ }
359
+ // Python pyproject.toml parser - simplified version
360
+ class PythonPackageParser {
361
+ manifestFileName = 'pyproject.toml';
362
+ packageType = 'python';
363
+ canParse(filename) {
364
+ return (filename.endsWith('pyproject.toml') ||
365
+ filename.endsWith('setup.py') ||
366
+ filename.endsWith('requirements.txt'));
367
+ }
368
+ parseContent(rawContent) {
369
+ try {
370
+ return TOML.load(rawContent);
371
+ }
372
+ catch {
373
+ return null;
374
+ }
375
+ }
376
+ extractPackageData(content, path) {
377
+ // Extract package directory path from manifest path
378
+ let packagePath;
379
+ const manifestName = path.split('/').pop() || '';
380
+ if (path === manifestName) {
381
+ // Just the filename, package is at root
382
+ packagePath = ''; // Empty string for root
383
+ }
384
+ else {
385
+ // Remove the manifest filename from path
386
+ packagePath = path.substring(0, path.lastIndexOf('/'));
387
+ }
388
+ // Handle pyproject.toml structure
389
+ const poetryData = content.tool?.poetry;
390
+ const projectData = content.project;
391
+ const name = poetryData?.name || projectData?.name || 'unnamed';
392
+ const version = poetryData?.version || projectData?.version;
393
+ // Extract dependencies
394
+ const dependencies = {};
395
+ const devDependencies = {};
396
+ // Poetry format
397
+ if (poetryData?.dependencies) {
398
+ Object.entries(poetryData.dependencies).forEach(([key, value]) => {
399
+ if (key !== 'python') {
400
+ dependencies[key] = typeof value === 'string' ? value : JSON.stringify(value);
401
+ }
402
+ });
403
+ }
404
+ if (poetryData?.['dev-dependencies']) {
405
+ Object.entries(poetryData['dev-dependencies']).forEach(([key, value]) => {
406
+ devDependencies[key] = typeof value === 'string' ? value : JSON.stringify(value);
407
+ });
408
+ }
409
+ // PEP 621 format
410
+ if (projectData?.dependencies && Array.isArray(projectData.dependencies)) {
411
+ try {
412
+ // Use pip-requirements-js to properly parse PEP 508 dependency specifications
413
+ const depString = projectData.dependencies.join('\n');
414
+ const requirements = parsePipRequirementsFile(depString);
415
+ requirements.forEach((req) => {
416
+ if (req.type === 'ProjectName' && req.name) {
417
+ let version = '*';
418
+ if (req.versionSpec && req.versionSpec.length > 0) {
419
+ // Use the first version spec for simplicity
420
+ const firstSpec = req.versionSpec[0];
421
+ version = firstSpec.version || '*';
422
+ }
423
+ dependencies[req.name] = version;
424
+ }
425
+ });
426
+ }
427
+ catch (error) {
428
+ console.warn('Failed to parse PEP 621 dependencies with pip-requirements-js, falling back to simple parsing:', error);
429
+ // Fallback to simple parsing if pip-requirements-js fails
430
+ projectData.dependencies.forEach((dep) => {
431
+ const cleanDep = dep.trim();
432
+ const spaceIndex = cleanDep.indexOf(' ');
433
+ if (spaceIndex > 0) {
434
+ dependencies[cleanDep.substring(0, spaceIndex)] = '*';
435
+ }
436
+ else {
437
+ dependencies[cleanDep] = '*';
438
+ }
439
+ });
440
+ }
441
+ }
442
+ const packageManager = this.detectPythonPackageManager(content);
443
+ // Extract metadata - prefer Poetry format, fall back to PEP 621
444
+ const description = poetryData?.description || projectData?.description;
445
+ // License can be string or object in PEP 621
446
+ let license;
447
+ if (poetryData?.license) {
448
+ license = poetryData.license;
449
+ }
450
+ else if (projectData?.license) {
451
+ license = typeof projectData.license === 'string'
452
+ ? projectData.license
453
+ : projectData.license.text;
454
+ }
455
+ // Authors - Poetry uses strings like "Name <email>", PEP 621 uses objects
456
+ let authors;
457
+ let author;
458
+ if (poetryData?.authors && poetryData.authors.length > 0) {
459
+ authors = poetryData.authors;
460
+ author = poetryData.authors[0];
461
+ }
462
+ else if (projectData?.authors && projectData.authors.length > 0) {
463
+ authors = projectData.authors.map(a => {
464
+ if (a.name && a.email)
465
+ return `${a.name} <${a.email}>`;
466
+ return a.name || a.email || '';
467
+ }).filter(Boolean);
468
+ author = authors[0];
469
+ }
470
+ // Repository/Homepage - Poetry has direct fields, PEP 621 uses urls table
471
+ let repository;
472
+ let homepage;
473
+ if (poetryData) {
474
+ repository = poetryData.repository;
475
+ homepage = poetryData.homepage;
476
+ }
477
+ else if (projectData?.urls) {
478
+ repository = projectData.urls.repository || projectData.urls.Repository ||
479
+ projectData.urls.source || projectData.urls.Source;
480
+ homepage = projectData.urls.homepage || projectData.urls.Homepage ||
481
+ projectData.urls.home || projectData.urls.Home;
482
+ }
483
+ const keywords = poetryData?.keywords || projectData?.keywords;
484
+ return {
485
+ name,
486
+ version,
487
+ path: packagePath,
488
+ manifestPath: path, // The full relative path to pyproject.toml/requirements.txt/setup.py
489
+ packageManager,
490
+ dependencies,
491
+ devDependencies,
492
+ peerDependencies: {},
493
+ description,
494
+ license,
495
+ author,
496
+ authors,
497
+ repository,
498
+ homepage,
499
+ keywords,
500
+ isMonorepoRoot: false,
501
+ isWorkspace: false,
502
+ parentPackage: undefined,
503
+ availableCommands: [],
504
+ };
505
+ }
506
+ detectWorkspaces(_content) {
507
+ // Python doesn't have built-in workspace support like npm
508
+ return null;
509
+ }
510
+ detectPackageManager(content, _lockFiles) {
511
+ return this.detectPythonPackageManager(content);
512
+ }
513
+ detectPythonPackageManager(content) {
514
+ // Check for Poetry
515
+ if (content.tool?.poetry)
516
+ return 'poetry';
517
+ // Default to pip
518
+ return 'pip';
519
+ }
520
+ detectDocsFolder(packagePath, fileTree, _manifestContent) {
521
+ // Reuse the same logic as NodePackageParser
522
+ const nodeParser = new NodePackageParser();
523
+ return nodeParser.detectDocsFolder(packagePath, fileTree, {});
524
+ }
525
+ }
526
+ // Rust Cargo.toml parser - simplified version
527
+ class CargoPackageParser {
528
+ manifestFileName = 'Cargo.toml';
529
+ packageType = 'cargo';
530
+ canParse(filename) {
531
+ return filename.endsWith('Cargo.toml');
532
+ }
533
+ parseContent(rawContent) {
534
+ try {
535
+ return TOML.load(rawContent);
536
+ }
537
+ catch {
538
+ return null;
539
+ }
540
+ }
541
+ extractPackageData(content, path) {
542
+ const packageData = content.package;
543
+ if (!packageData)
544
+ return null;
545
+ // Extract package directory path from manifest path
546
+ let packagePath;
547
+ if (path === 'Cargo.toml') {
548
+ packagePath = ''; // Root directory - empty string for root
549
+ }
550
+ else if (path.endsWith('/Cargo.toml')) {
551
+ packagePath = path.slice(0, -11); // Remove '/Cargo.toml'
552
+ }
553
+ else {
554
+ // Shouldn't happen, but handle gracefully
555
+ packagePath = path;
556
+ }
557
+ // Extract dependencies
558
+ const dependencies = {};
559
+ const devDependencies = {};
560
+ if (content.dependencies) {
561
+ Object.entries(content.dependencies).forEach(([key, value]) => {
562
+ dependencies[key] = typeof value === 'string' ? value : JSON.stringify(value);
563
+ });
564
+ }
565
+ if (content['dev-dependencies']) {
566
+ Object.entries(content['dev-dependencies']).forEach(([key, value]) => {
567
+ devDependencies[key] = typeof value === 'string' ? value : JSON.stringify(value);
568
+ });
569
+ }
570
+ // Extract metadata from [package] section
571
+ const author = packageData.authors?.[0];
572
+ // Prefer homepage over documentation URL for the homepage field
573
+ const homepage = packageData.homepage || packageData.documentation;
574
+ return {
575
+ name: packageData.name || 'unnamed',
576
+ version: packageData.version,
577
+ path: packagePath,
578
+ manifestPath: path, // The full relative path to Cargo.toml
579
+ packageManager: 'cargo',
580
+ dependencies,
581
+ devDependencies,
582
+ peerDependencies: {},
583
+ description: packageData.description,
584
+ license: packageData.license,
585
+ author,
586
+ authors: packageData.authors,
587
+ repository: packageData.repository,
588
+ homepage,
589
+ keywords: packageData.keywords,
590
+ isMonorepoRoot: !!content.workspace,
591
+ isWorkspace: false,
592
+ parentPackage: undefined,
593
+ availableCommands: [],
594
+ };
595
+ }
596
+ detectWorkspaces(content) {
597
+ return content.workspace?.members || null;
598
+ }
599
+ detectPackageManager() {
600
+ return 'cargo';
601
+ }
602
+ detectDocsFolder(packagePath, fileTree, _manifestContent) {
603
+ // Reuse the same logic as NodePackageParser
604
+ const nodeParser = new NodePackageParser();
605
+ return nodeParser.detectDocsFolder(packagePath, fileTree, {});
606
+ }
607
+ }
608
+ export class PackageLayerModule {
609
+ parsers = [
610
+ new NodePackageParser(),
611
+ new PythonPackageParser(),
612
+ new CargoPackageParser(),
613
+ ];
614
+ /**
615
+ * Extract package information from the file tree
616
+ * @param fileTree - The file tree to analyze
617
+ * @param fileReader - Optional function to read file contents. If not provided, manifest detection only.
618
+ */
619
+ async discoverPackages(fileTree, fileReader) {
620
+ const packages = [];
621
+ if (!fileTree.allFiles)
622
+ return packages;
623
+ // Find all package manifest files
624
+ const manifestFiles = fileTree.allFiles.filter(file => {
625
+ if (!file.path)
626
+ return false;
627
+ return this.parsers.some(parser => parser.canParse(file.path));
628
+ });
629
+ // Get all files for lock file detection
630
+ const allFilePaths = fileTree.allFiles.map(f => f.path).filter(Boolean);
631
+ for (const manifestFile of manifestFiles) {
632
+ if (!manifestFile.path)
633
+ continue;
634
+ // Find appropriate parser
635
+ const parser = this.parsers.find(p => p.canParse(manifestFile.path));
636
+ if (!parser)
637
+ continue;
638
+ // Get manifest content
639
+ let content = null;
640
+ if (fileReader) {
641
+ try {
642
+ const fileContent = await fileReader(manifestFile.path);
643
+ if (fileContent) {
644
+ // Use the parser's parseContent method for type-safe parsing
645
+ content = parser.parseContent(fileContent);
646
+ }
647
+ }
648
+ catch (error) {
649
+ console.warn(`Could not read or parse ${manifestFile.path}:`, error);
650
+ continue;
651
+ }
652
+ }
653
+ else {
654
+ // If no file reader provided, create minimal package info from path
655
+ const packageName = manifestFile.path.split('/').slice(-2, -1)[0] || 'unnamed';
656
+ content = this.createMinimalManifest(parser.packageType, packageName);
657
+ }
658
+ if (!content)
659
+ continue;
660
+ // Extract package data
661
+ const packageData = parser.extractPackageData(content, manifestFile.path);
662
+ if (!packageData) {
663
+ console.warn(`[PackageLayerModule] extractPackageData returned null for ${manifestFile.path}`, { content, parser: parser.packageType });
664
+ continue;
665
+ }
666
+ // Detect package manager
667
+ const dirPath = manifestFile.path.substring(0, manifestFile.path.lastIndexOf('/'));
668
+ const lockFilesInDir = allFilePaths.filter(f => f.startsWith(dirPath) && this.isLockFile(f));
669
+ packageData.packageManager = parser.detectPackageManager(content, lockFilesInDir);
670
+ // Update commands with correct package manager for Node packages
671
+ if (parser.packageType === 'node' && packageData.availableCommands) {
672
+ // Only replace if we detected a valid package manager
673
+ if (packageData.packageManager &&
674
+ packageData.packageManager !== 'unknown' &&
675
+ packageData.packageManager !== 'npm') {
676
+ packageData.availableCommands = packageData.availableCommands.map(cmd => {
677
+ // Update both script and standard commands
678
+ if (cmd.command.includes('npm ')) {
679
+ let pmCommand = cmd.command;
680
+ // Handle different package manager syntaxes
681
+ if (packageData.packageManager === 'yarn') {
682
+ pmCommand = pmCommand
683
+ .replace('npm run', 'yarn')
684
+ .replace('npm install', 'yarn install')
685
+ .replace('npm ci', 'yarn install --frozen-lockfile')
686
+ .replace('npm update', 'yarn upgrade');
687
+ }
688
+ else if (packageData.packageManager === 'pnpm') {
689
+ pmCommand = pmCommand.replace('npm', 'pnpm');
690
+ }
691
+ return { ...cmd, command: pmCommand };
692
+ }
693
+ return cmd;
694
+ });
695
+ }
696
+ }
697
+ // Create file set for the manifest
698
+ const fileSet = {
699
+ id: `package-manifest-${packageData.path}`,
700
+ name: parser.manifestFileName,
701
+ patterns: [
702
+ {
703
+ type: 'exact',
704
+ pattern: manifestFile.path,
705
+ description: `${parser.packageType} package manifest`,
706
+ },
707
+ ],
708
+ matchedFiles: [manifestFile.path],
709
+ fileCount: 1,
710
+ };
711
+ // Detect configuration files if the parser supports it
712
+ let configFiles;
713
+ if (parser.detectConfigs) {
714
+ configFiles = parser.detectConfigs(packageData.path, fileTree, content);
715
+ }
716
+ // Detect documentation folder if the parser supports it
717
+ let docsFolder;
718
+ if (parser.detectDocsFolder) {
719
+ docsFolder = parser.detectDocsFolder(packageData.path, fileTree, content);
720
+ }
721
+ // Calculate quality metrics from lens commands
722
+ const qualityMetrics = packageData.availableCommands
723
+ ? QualityMetricsCalculator.calculateQualityProfile(packageData.availableCommands)
724
+ : undefined;
725
+ // Create package layer
726
+ const layer = {
727
+ id: `package-${parser.packageType}-${packageData.path.replace(/[^a-zA-Z0-9-]/g, '-')}`,
728
+ name: packageData.name,
729
+ type: parser.packageType,
730
+ enabled: true,
731
+ derivedFrom: {
732
+ fileSets: [fileSet],
733
+ derivationType: 'content',
734
+ description: `${parser.packageType} package defined in ${parser.manifestFileName}`,
735
+ contentExtraction: {
736
+ method: 'parse',
737
+ parser: {
738
+ format: parser.manifestFileName.endsWith('.json') ? 'json' : 'toml',
739
+ paths: parser.packageType === 'node'
740
+ ? ['']
741
+ : parser.packageType === 'python'
742
+ ? ['tool.poetry', 'project']
743
+ : ['package'],
744
+ },
745
+ },
746
+ },
747
+ packageData,
748
+ configFiles,
749
+ docsFolder,
750
+ qualityMetrics,
751
+ pillar: 'foundationHealth',
752
+ };
753
+ packages.push(layer);
754
+ }
755
+ // Determine workspace relationships
756
+ this.resolveWorkspaceRelationships(packages);
757
+ return packages;
758
+ }
759
+ /**
760
+ * Create package layers from workspace boundaries (backwards compatibility)
761
+ */
762
+ createPackageLayersFromBoundaries(boundaries) {
763
+ return boundaries.map(boundary => {
764
+ const fileSet = {
765
+ id: `package-manifest-${boundary.rootPath || 'root'}`,
766
+ name: 'package.json',
767
+ patterns: [
768
+ {
769
+ type: 'exact',
770
+ pattern: boundary.packageJsonPath,
771
+ description: 'Node.js package manifest',
772
+ },
773
+ ],
774
+ matchedFiles: [boundary.packageJsonPath],
775
+ fileCount: 1,
776
+ };
777
+ const packageData = {
778
+ name: boundary.packageData?.name || boundary.name,
779
+ version: boundary.packageData?.version,
780
+ path: boundary.rootPath || '.',
781
+ manifestPath: boundary.packageJsonPath,
782
+ packageManager: 'unknown',
783
+ dependencies: {},
784
+ devDependencies: {},
785
+ peerDependencies: {},
786
+ isMonorepoRoot: !!boundary.packageData?.workspaces,
787
+ isWorkspace: !boundary.isRoot && boundaries.length > 1,
788
+ parentPackage: boundary.isRoot ? undefined : 'root',
789
+ };
790
+ const layer = {
791
+ id: `package-node-${boundary.id}`,
792
+ name: packageData.name,
793
+ type: 'node',
794
+ enabled: true,
795
+ derivedFrom: {
796
+ fileSets: [fileSet],
797
+ derivationType: 'content',
798
+ description: 'Node.js package defined in package.json',
799
+ },
800
+ packageData,
801
+ pillar: 'foundationHealth',
802
+ };
803
+ return layer;
804
+ });
805
+ }
806
+ isLockFile(path) {
807
+ const lockFiles = [
808
+ 'package-lock.json',
809
+ 'yarn.lock',
810
+ 'pnpm-lock.yaml',
811
+ 'poetry.lock',
812
+ 'Pipfile.lock',
813
+ 'Cargo.lock',
814
+ ];
815
+ return lockFiles.some(lock => path.endsWith(lock));
816
+ }
817
+ resolveWorkspaceRelationships(packages) {
818
+ // Find root packages (typically at the repository root)
819
+ const _rootPackages = packages.filter(p => p.packageData.path === '.' || p.packageData.path === '');
820
+ // Mark workspace packages
821
+ packages.forEach(pkg => {
822
+ if (pkg.packageData.path !== '.' && pkg.packageData.path !== '') {
823
+ pkg.packageData.isWorkspace = true;
824
+ // Find parent package
825
+ const parentPath = pkg.packageData.path.includes('/')
826
+ ? pkg.packageData.path.substring(0, pkg.packageData.path.lastIndexOf('/'))
827
+ : '.';
828
+ const parent = packages.find(p => p.packageData.path === parentPath);
829
+ if (parent) {
830
+ pkg.packageData.parentPackage = parent.packageData.name;
831
+ }
832
+ }
833
+ });
834
+ }
835
+ /**
836
+ * Create a minimal manifest for when file content isn't available
837
+ */
838
+ createMinimalManifest(packageType, packageName) {
839
+ switch (packageType) {
840
+ case 'node':
841
+ return {
842
+ name: packageName,
843
+ version: '0.0.0',
844
+ dependencies: {},
845
+ devDependencies: {},
846
+ };
847
+ case 'python':
848
+ return {
849
+ project: {
850
+ name: packageName,
851
+ version: '0.0.0',
852
+ },
853
+ };
854
+ case 'cargo':
855
+ return {
856
+ package: {
857
+ name: packageName,
858
+ version: '0.0.0',
859
+ },
860
+ };
861
+ default:
862
+ return {};
863
+ }
864
+ }
865
+ }
866
+ //# sourceMappingURL=PackageLayerModule.js.map