@massu/core 1.10.8 → 1.12.1
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/CHANGELOG.md +1334 -0
- package/dist/adapter.js +6 -4
- package/dist/cli.js +581 -448
- package/dist/detect/adapters/.bundle-shasums.json +4 -7
- package/dist/detect/adapters/rails.js +6 -4
- package/dist/detect/adapters/spring.js +6 -4
- package/dist/detect/adapters/tree-sitter-loader.js +6 -4
- package/dist/hooks/auto-learning-pipeline.js +24 -1
- package/dist/hooks/classify-failure.js +32 -3
- package/dist/hooks/cost-tracker.js +24 -0
- package/dist/hooks/fix-detector.js +97 -1
- package/dist/hooks/incident-pipeline.js +30 -1
- package/dist/hooks/intent-suggester.js +6 -3
- package/dist/hooks/post-edit-context.js +24 -1
- package/dist/hooks/post-tool-use.js +67 -19
- package/dist/hooks/pre-compact.js +28 -3
- package/dist/hooks/pre-delete-check.js +53 -27
- package/dist/hooks/pre-tool-use-gate.js +962 -0
- package/dist/hooks/quality-event.js +24 -0
- package/dist/hooks/rule-enforcement-pipeline.js +24 -1
- package/dist/hooks/security-gate.js +29 -16
- package/dist/hooks/session-end.js +31 -6
- package/dist/hooks/session-start.js +8709 -8459
- package/dist/hooks/user-prompt.js +25 -1
- package/package.json +15 -18
- package/commands/README.md +0 -134
- package/src/adapter.ts +0 -121
- package/src/adr-generator.ts +0 -292
- package/src/analytics.ts +0 -373
- package/src/audit-trail.ts +0 -450
- package/src/backfill-sessions.ts +0 -182
- package/src/changelog-generator.ts +0 -178
- package/src/cli.ts +0 -246
- package/src/cloud-sync.ts +0 -234
- package/src/commands/adapters.ts +0 -824
- package/src/commands/changelog.ts +0 -165
- package/src/commands/config-check-drift.ts +0 -133
- package/src/commands/config-refresh.ts +0 -396
- package/src/commands/config-upgrade.ts +0 -127
- package/src/commands/doctor.ts +0 -485
- package/src/commands/hook-runner.ts +0 -145
- package/src/commands/init.ts +0 -1823
- package/src/commands/install-commands.ts +0 -692
- package/src/commands/install-hooks.ts +0 -27
- package/src/commands/permissions.ts +0 -150
- package/src/commands/refresh-log.ts +0 -37
- package/src/commands/show-template.ts +0 -65
- package/src/commands/template-engine.ts +0 -301
- package/src/commands/watch.ts +0 -430
- package/src/config.ts +0 -765
- package/src/cost-tracker.ts +0 -355
- package/src/db.ts +0 -370
- package/src/dependency-scorer.ts +0 -337
- package/src/detect/adapters/aspnet.ts +0 -4
- package/src/detect/adapters/discover.ts +0 -469
- package/src/detect/adapters/file-sampler.ts +0 -225
- package/src/detect/adapters/go-chi.ts +0 -4
- package/src/detect/adapters/index.ts +0 -51
- package/src/detect/adapters/nextjs-trpc.ts +0 -166
- package/src/detect/adapters/parse-guard.ts +0 -133
- package/src/detect/adapters/phoenix.ts +0 -4
- package/src/detect/adapters/python-django.ts +0 -208
- package/src/detect/adapters/python-fastapi.ts +0 -223
- package/src/detect/adapters/python-flask.ts +0 -235
- package/src/detect/adapters/query-helpers.ts +0 -170
- package/src/detect/adapters/rails.ts +0 -4
- package/src/detect/adapters/runner.ts +0 -284
- package/src/detect/adapters/spring.ts +0 -4
- package/src/detect/adapters/swift-swiftui.ts +0 -171
- package/src/detect/adapters/tree-sitter-loader.ts +0 -517
- package/src/detect/adapters/types.ts +0 -191
- package/src/detect/codebase-introspector.ts +0 -210
- package/src/detect/domain-inferrer.ts +0 -197
- package/src/detect/drift.ts +0 -199
- package/src/detect/framework-detector.ts +0 -307
- package/src/detect/index.ts +0 -209
- package/src/detect/manifest-registry.ts +0 -261
- package/src/detect/migrate.ts +0 -362
- package/src/detect/monorepo-detector.ts +0 -348
- package/src/detect/package-detector.ts +0 -828
- package/src/detect/passthrough.ts +0 -108
- package/src/detect/regex-fallback.ts +0 -449
- package/src/detect/source-dir-detector.ts +0 -297
- package/src/detect/vr-command-map.ts +0 -167
- package/src/docs-map.json +0 -100
- package/src/docs-tools.ts +0 -519
- package/src/domains.ts +0 -182
- package/src/hooks/auto-learning-pipeline.ts +0 -228
- package/src/hooks/classify-failure.ts +0 -259
- package/src/hooks/cost-tracker.ts +0 -66
- package/src/hooks/fix-detector.ts +0 -186
- package/src/hooks/incident-pipeline.ts +0 -190
- package/src/hooks/intent-suggester.ts +0 -131
- package/src/hooks/post-edit-context.ts +0 -91
- package/src/hooks/post-tool-use.ts +0 -413
- package/src/hooks/pre-compact.ts +0 -168
- package/src/hooks/pre-delete-check.ts +0 -243
- package/src/hooks/quality-event.ts +0 -127
- package/src/hooks/rule-enforcement-pipeline.ts +0 -159
- package/src/hooks/security-gate.ts +0 -153
- package/src/hooks/session-end.ts +0 -467
- package/src/hooks/session-start.ts +0 -436
- package/src/hooks/user-prompt.ts +0 -157
- package/src/import-resolver.ts +0 -226
- package/src/knowledge-db.ts +0 -176
- package/src/knowledge-indexer.ts +0 -708
- package/src/knowledge-tools.ts +0 -1414
- package/src/lib/fileLock.ts +0 -203
- package/src/lib/framework-supports.ts +0 -85
- package/src/lib/gitToplevel.ts +0 -22
- package/src/lib/hook-registry.ts +0 -43
- package/src/lib/installLock.ts +0 -66
- package/src/lib/memory-path.ts +0 -49
- package/src/lib/pidLiveness.ts +0 -67
- package/src/lib/settings-local.ts +0 -110
- package/src/lib/sql-table-names.ts +0 -71
- package/src/license.ts +0 -547
- package/src/lsp/auto-detect.ts +0 -98
- package/src/lsp/client.ts +0 -776
- package/src/lsp/enrich.ts +0 -127
- package/src/lsp/types.ts +0 -221
- package/src/memory-db.ts +0 -1614
- package/src/memory-file-ingest.ts +0 -128
- package/src/memory-tools.ts +0 -424
- package/src/middleware-tree.ts +0 -71
- package/src/observability-tools.ts +0 -343
- package/src/observation-extractor.ts +0 -418
- package/src/page-deps.ts +0 -285
- package/src/permissions.ts +0 -422
- package/src/prompt-analyzer.ts +0 -332
- package/src/python/coupling-detector.ts +0 -125
- package/src/python/domain-enforcer.ts +0 -84
- package/src/python/impact-analyzer.ts +0 -96
- package/src/python/import-parser.ts +0 -244
- package/src/python/import-resolver.ts +0 -136
- package/src/python/migration-indexer.ts +0 -116
- package/src/python/migration-parser.ts +0 -332
- package/src/python/model-indexer.ts +0 -71
- package/src/python/model-parser.ts +0 -279
- package/src/python/route-indexer.ts +0 -59
- package/src/python/route-parser.ts +0 -317
- package/src/python-tools.ts +0 -630
- package/src/regression-detector.ts +0 -319
- package/src/rules.ts +0 -57
- package/src/schema-mapper.ts +0 -232
- package/src/security/adapter-origin.ts +0 -130
- package/src/security/adapter-verifier.ts +0 -319
- package/src/security/atomic-write.ts +0 -164
- package/src/security/fetcher.ts +0 -200
- package/src/security/install-tracking.ts +0 -319
- package/src/security/license-pubkey.generated.ts +0 -30
- package/src/security/license-response-verifier.ts +0 -149
- package/src/security/local-fingerprint.ts +0 -225
- package/src/security/manifest-cache.ts +0 -333
- package/src/security/manifest-schema.ts +0 -129
- package/src/security/registry-pubkey.generated.ts +0 -35
- package/src/security/telemetry.ts +0 -320
- package/src/security-scorer.ts +0 -405
- package/src/security-utils.ts +0 -133
- package/src/sentinel-db.ts +0 -580
- package/src/sentinel-scanner.ts +0 -406
- package/src/sentinel-tools.ts +0 -513
- package/src/sentinel-types.ts +0 -140
- package/src/server-dispatch.ts +0 -225
- package/src/server.ts +0 -107
- package/src/session-archiver.ts +0 -111
- package/src/session-state-generator.ts +0 -174
- package/src/team-knowledge.ts +0 -407
- package/src/tool-db-needs.ts +0 -232
- package/src/tools.ts +0 -1206
- package/src/transcript-parser.ts +0 -458
- package/src/trpc-index.ts +0 -215
- package/src/validate-features-runner.ts +0 -107
- package/src/validation-engine.ts +0 -358
- package/src/watch/daemon.ts +0 -385
- package/src/watch/lockfile-detector.ts +0 -65
- package/src/watch/paths.ts +0 -279
- package/src/watch/state.ts +0 -178
|
@@ -1,828 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Package Manifest Detector (P1-001)
|
|
6
|
-
* ==================================
|
|
7
|
-
*
|
|
8
|
-
* Scans a project root for dependency-manifest files across 9 ecosystems and
|
|
9
|
-
* returns a structured `PackageManifest[]`. Pure filesystem, pure function —
|
|
10
|
-
* no DB handles, no network, no child processes.
|
|
11
|
-
*
|
|
12
|
-
* Supported manifests:
|
|
13
|
-
* - `package.json` (Node.js/TypeScript)
|
|
14
|
-
* - `pyproject.toml` (Python — poetry, pep621, setuptools)
|
|
15
|
-
* - `requirements.txt` (Python — plain)
|
|
16
|
-
* - `Pipfile` (Python — pipenv)
|
|
17
|
-
* - `Cargo.toml` (Rust)
|
|
18
|
-
* - `Package.swift` (Swift)
|
|
19
|
-
* - `go.mod` (Go)
|
|
20
|
-
* - `pom.xml` (Java — Maven)
|
|
21
|
-
* - `build.gradle` / `build.gradle.kts` (Java/Kotlin — Gradle)
|
|
22
|
-
* - `Gemfile` (Ruby)
|
|
23
|
-
*
|
|
24
|
-
* Monorepos: walks up to 2 levels deep into conventional workspace roots
|
|
25
|
-
* (`apps/*`, `packages/*`, `services/*`, `libs/*`, `modules/*`) and returns
|
|
26
|
-
* a manifest per workspace.
|
|
27
|
-
*
|
|
28
|
-
* Malformed files log a structured warning to the returned `warnings[]` and do
|
|
29
|
-
* NOT throw, per CR-9.
|
|
30
|
-
*
|
|
31
|
-
* Usage:
|
|
32
|
-
* ```ts
|
|
33
|
-
* import { detectPackageManifests } from './detect/package-detector.ts';
|
|
34
|
-
* const { manifests, warnings } = detectPackageManifests('/repo');
|
|
35
|
-
* ```
|
|
36
|
-
*/
|
|
37
|
-
|
|
38
|
-
import { readFileSync, existsSync, statSync, lstatSync, readdirSync } from 'fs';
|
|
39
|
-
import { join, relative } from 'path';
|
|
40
|
-
import { parse as parseToml } from 'smol-toml';
|
|
41
|
-
|
|
42
|
-
export type SupportedLanguage =
|
|
43
|
-
| 'typescript'
|
|
44
|
-
| 'javascript'
|
|
45
|
-
| 'python'
|
|
46
|
-
| 'rust'
|
|
47
|
-
| 'swift'
|
|
48
|
-
| 'go'
|
|
49
|
-
| 'java'
|
|
50
|
-
| 'ruby'
|
|
51
|
-
| 'elixir'
|
|
52
|
-
| 'csharp';
|
|
53
|
-
|
|
54
|
-
export interface PackageManifest {
|
|
55
|
-
/** Absolute path to the manifest file. */
|
|
56
|
-
path: string;
|
|
57
|
-
/** Path relative to projectRoot, forward-slash normalized. */
|
|
58
|
-
relativePath: string;
|
|
59
|
-
/** Workspace/package root directory (parent of manifest). */
|
|
60
|
-
directory: string;
|
|
61
|
-
/** Language this manifest belongs to. */
|
|
62
|
-
language: SupportedLanguage;
|
|
63
|
-
/** Runtime family (e.g., 'node', 'python3', 'cargo', 'xcode'). */
|
|
64
|
-
runtime: string;
|
|
65
|
-
/** Manifest-declared package name (best-effort; null when not present). */
|
|
66
|
-
name: string | null;
|
|
67
|
-
/** Declared version when available. */
|
|
68
|
-
version: string | null;
|
|
69
|
-
/** Runtime dependencies. */
|
|
70
|
-
dependencies: string[];
|
|
71
|
-
/** Dev / test / build dependencies. */
|
|
72
|
-
devDependencies: string[];
|
|
73
|
-
/** Script / task names declared (e.g., npm scripts, poetry scripts). */
|
|
74
|
-
scripts: string[];
|
|
75
|
-
/** Raw manifest type key. */
|
|
76
|
-
manifestType:
|
|
77
|
-
| 'package.json'
|
|
78
|
-
| 'pyproject.toml'
|
|
79
|
-
| 'requirements.txt'
|
|
80
|
-
| 'Pipfile'
|
|
81
|
-
| 'Cargo.toml'
|
|
82
|
-
| 'Package.swift'
|
|
83
|
-
| 'go.mod'
|
|
84
|
-
| 'pom.xml'
|
|
85
|
-
| 'build.gradle'
|
|
86
|
-
| 'Gemfile'
|
|
87
|
-
| 'mix.exs'
|
|
88
|
-
| '*.csproj';
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export interface DetectionWarning {
|
|
92
|
-
path: string;
|
|
93
|
-
reason: string;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export interface PackageDetectionResult {
|
|
97
|
-
manifests: PackageManifest[];
|
|
98
|
-
warnings: DetectionWarning[];
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const WORKSPACE_DIRS = ['apps', 'packages', 'services', 'libs', 'modules'];
|
|
102
|
-
|
|
103
|
-
const IGNORED_DIRS = new Set([
|
|
104
|
-
'node_modules',
|
|
105
|
-
'.venv',
|
|
106
|
-
'venv',
|
|
107
|
-
'__pycache__',
|
|
108
|
-
'dist',
|
|
109
|
-
'build',
|
|
110
|
-
'.build',
|
|
111
|
-
'target',
|
|
112
|
-
'.next',
|
|
113
|
-
'.nuxt',
|
|
114
|
-
'coverage',
|
|
115
|
-
'.git',
|
|
116
|
-
'.massu',
|
|
117
|
-
'.turbo',
|
|
118
|
-
'.cache',
|
|
119
|
-
'.pytest_cache',
|
|
120
|
-
'.mypy_cache',
|
|
121
|
-
'DerivedData',
|
|
122
|
-
'Pods',
|
|
123
|
-
]);
|
|
124
|
-
|
|
125
|
-
// MANIFEST_FILES removed Plan 1.5.1. The canonical list lives at
|
|
126
|
-
// `manifest-registry.ts:getManifestPatterns()`. `detectManifestsInDir`
|
|
127
|
-
// (below) iterates the registry directly so adding a new manifest type
|
|
128
|
-
// requires only a single registry entry — no second list to keep in sync.
|
|
129
|
-
|
|
130
|
-
function safeRead(path: string): string | null {
|
|
131
|
-
try {
|
|
132
|
-
if (!existsSync(path)) return null;
|
|
133
|
-
// lstatSync does NOT follow symlinks — required for accurate symlink detection.
|
|
134
|
-
// statSync returns the target's stat, so .isSymbolicLink() would be false on
|
|
135
|
-
// symlink-to-regular-file, bypassing the intended rejection.
|
|
136
|
-
const ls = lstatSync(path);
|
|
137
|
-
if (ls.isSymbolicLink()) return null;
|
|
138
|
-
const st = statSync(path);
|
|
139
|
-
if (!st.isFile()) return null;
|
|
140
|
-
return readFileSync(path, 'utf-8');
|
|
141
|
-
} catch {
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function normalizeRelative(root: string, path: string): string {
|
|
147
|
-
const rel = relative(root, path);
|
|
148
|
-
return rel.split(/[/\\]/).join('/');
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export function parsePackageJson(
|
|
152
|
-
path: string,
|
|
153
|
-
directory: string,
|
|
154
|
-
root: string,
|
|
155
|
-
warnings: DetectionWarning[]
|
|
156
|
-
): PackageManifest | null {
|
|
157
|
-
const raw = safeRead(path);
|
|
158
|
-
if (raw === null) return null;
|
|
159
|
-
let pkg: Record<string, unknown>;
|
|
160
|
-
try {
|
|
161
|
-
pkg = JSON.parse(raw) as Record<string, unknown>;
|
|
162
|
-
} catch (err) {
|
|
163
|
-
warnings.push({
|
|
164
|
-
path,
|
|
165
|
-
reason: `package.json JSON parse failed: ${(err as Error).message}`,
|
|
166
|
-
});
|
|
167
|
-
return null;
|
|
168
|
-
}
|
|
169
|
-
const deps = Object.keys(
|
|
170
|
-
(pkg.dependencies as Record<string, string>) ?? {}
|
|
171
|
-
);
|
|
172
|
-
const devDeps = Object.keys(
|
|
173
|
-
(pkg.devDependencies as Record<string, string>) ?? {}
|
|
174
|
-
);
|
|
175
|
-
const peer = Object.keys(
|
|
176
|
-
(pkg.peerDependencies as Record<string, string>) ?? {}
|
|
177
|
-
);
|
|
178
|
-
// Classify TypeScript vs JavaScript based on typescript dep presence or tsconfig.
|
|
179
|
-
const hasTs =
|
|
180
|
-
deps.includes('typescript') ||
|
|
181
|
-
devDeps.includes('typescript') ||
|
|
182
|
-
existsSync(join(directory, 'tsconfig.json'));
|
|
183
|
-
const language: SupportedLanguage = hasTs ? 'typescript' : 'javascript';
|
|
184
|
-
const scripts = Object.keys(
|
|
185
|
-
(pkg.scripts as Record<string, string>) ?? {}
|
|
186
|
-
);
|
|
187
|
-
return {
|
|
188
|
-
path,
|
|
189
|
-
relativePath: normalizeRelative(root, path),
|
|
190
|
-
directory,
|
|
191
|
-
language,
|
|
192
|
-
runtime: 'node',
|
|
193
|
-
name: typeof pkg.name === 'string' ? pkg.name : null,
|
|
194
|
-
version: typeof pkg.version === 'string' ? pkg.version : null,
|
|
195
|
-
dependencies: [...deps, ...peer],
|
|
196
|
-
devDependencies: devDeps,
|
|
197
|
-
scripts,
|
|
198
|
-
manifestType: 'package.json',
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
export function parsePyproject(
|
|
203
|
-
path: string,
|
|
204
|
-
directory: string,
|
|
205
|
-
root: string,
|
|
206
|
-
warnings: DetectionWarning[]
|
|
207
|
-
): PackageManifest | null {
|
|
208
|
-
const raw = safeRead(path);
|
|
209
|
-
if (raw === null) return null;
|
|
210
|
-
let toml: Record<string, unknown>;
|
|
211
|
-
try {
|
|
212
|
-
toml = parseToml(raw) as Record<string, unknown>;
|
|
213
|
-
} catch (err) {
|
|
214
|
-
warnings.push({
|
|
215
|
-
path,
|
|
216
|
-
reason: `pyproject.toml TOML parse failed: ${(err as Error).message}`,
|
|
217
|
-
});
|
|
218
|
-
return null;
|
|
219
|
-
}
|
|
220
|
-
const deps: string[] = [];
|
|
221
|
-
const devDeps: string[] = [];
|
|
222
|
-
const scripts: string[] = [];
|
|
223
|
-
let name: string | null = null;
|
|
224
|
-
let version: string | null = null;
|
|
225
|
-
|
|
226
|
-
// PEP 621 [project] table
|
|
227
|
-
const project = toml.project as Record<string, unknown> | undefined;
|
|
228
|
-
if (project && typeof project === 'object') {
|
|
229
|
-
if (typeof project.name === 'string') name = project.name;
|
|
230
|
-
if (typeof project.version === 'string') version = project.version;
|
|
231
|
-
const pd = project.dependencies;
|
|
232
|
-
if (Array.isArray(pd)) {
|
|
233
|
-
for (const d of pd) {
|
|
234
|
-
if (typeof d === 'string') deps.push(normalizePyDep(d));
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
const optDeps = project['optional-dependencies'] as
|
|
238
|
-
| Record<string, unknown>
|
|
239
|
-
| undefined;
|
|
240
|
-
if (optDeps && typeof optDeps === 'object') {
|
|
241
|
-
for (const grp of Object.values(optDeps)) {
|
|
242
|
-
if (Array.isArray(grp)) {
|
|
243
|
-
for (const d of grp) {
|
|
244
|
-
if (typeof d === 'string') devDeps.push(normalizePyDep(d));
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
const psScripts = project.scripts as Record<string, unknown> | undefined;
|
|
250
|
-
if (psScripts && typeof psScripts === 'object') {
|
|
251
|
-
scripts.push(...Object.keys(psScripts));
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Poetry [tool.poetry]
|
|
256
|
-
const tool = toml.tool as Record<string, unknown> | undefined;
|
|
257
|
-
const poetry = tool?.poetry as Record<string, unknown> | undefined;
|
|
258
|
-
if (poetry && typeof poetry === 'object') {
|
|
259
|
-
if (!name && typeof poetry.name === 'string') name = poetry.name;
|
|
260
|
-
if (!version && typeof poetry.version === 'string') version = poetry.version;
|
|
261
|
-
const pdeps = poetry.dependencies as Record<string, unknown> | undefined;
|
|
262
|
-
if (pdeps && typeof pdeps === 'object') {
|
|
263
|
-
for (const k of Object.keys(pdeps)) {
|
|
264
|
-
if (k !== 'python') deps.push(k);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
const groups = poetry.group as Record<string, unknown> | undefined;
|
|
268
|
-
if (groups && typeof groups === 'object') {
|
|
269
|
-
for (const grp of Object.values(groups)) {
|
|
270
|
-
const grpObj = grp as Record<string, unknown> | undefined;
|
|
271
|
-
const grpDeps = grpObj?.dependencies as
|
|
272
|
-
| Record<string, unknown>
|
|
273
|
-
| undefined;
|
|
274
|
-
if (grpDeps && typeof grpDeps === 'object') {
|
|
275
|
-
for (const k of Object.keys(grpDeps)) {
|
|
276
|
-
if (k !== 'python') devDeps.push(k);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
// Legacy poetry dev-dependencies
|
|
282
|
-
const legacyDev = poetry['dev-dependencies'] as
|
|
283
|
-
| Record<string, unknown>
|
|
284
|
-
| undefined;
|
|
285
|
-
if (legacyDev && typeof legacyDev === 'object') {
|
|
286
|
-
for (const k of Object.keys(legacyDev)) {
|
|
287
|
-
if (k !== 'python') devDeps.push(k);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
const pScripts = poetry.scripts as Record<string, unknown> | undefined;
|
|
291
|
-
if (pScripts && typeof pScripts === 'object') {
|
|
292
|
-
scripts.push(...Object.keys(pScripts));
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
return {
|
|
297
|
-
path,
|
|
298
|
-
relativePath: normalizeRelative(root, path),
|
|
299
|
-
directory,
|
|
300
|
-
language: 'python',
|
|
301
|
-
runtime: 'python3',
|
|
302
|
-
name,
|
|
303
|
-
version,
|
|
304
|
-
dependencies: deps,
|
|
305
|
-
devDependencies: devDeps,
|
|
306
|
-
scripts,
|
|
307
|
-
manifestType: 'pyproject.toml',
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function normalizePyDep(spec: string): string {
|
|
312
|
-
// Strip version specifiers, extras, markers.
|
|
313
|
-
// Example: "fastapi[all]>=0.110,<0.120 ; python_version>='3.10'"
|
|
314
|
-
// → "fastapi"
|
|
315
|
-
const semi = spec.split(';')[0];
|
|
316
|
-
const extras = semi.split('[')[0];
|
|
317
|
-
const name = extras.split(/[=<>!~ ]/)[0];
|
|
318
|
-
return name.trim();
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
export function parseRequirementsTxt(
|
|
322
|
-
path: string,
|
|
323
|
-
directory: string,
|
|
324
|
-
root: string,
|
|
325
|
-
_warnings: DetectionWarning[]
|
|
326
|
-
): PackageManifest | null {
|
|
327
|
-
const raw = safeRead(path);
|
|
328
|
-
if (raw === null) return null;
|
|
329
|
-
const deps: string[] = [];
|
|
330
|
-
for (const line of raw.split(/\r?\n/)) {
|
|
331
|
-
const trimmed = line.trim();
|
|
332
|
-
if (!trimmed) continue;
|
|
333
|
-
if (trimmed.startsWith('#')) continue;
|
|
334
|
-
if (trimmed.startsWith('-')) continue; // -r, -e, --index-url, etc.
|
|
335
|
-
const name = normalizePyDep(trimmed);
|
|
336
|
-
if (name) deps.push(name);
|
|
337
|
-
}
|
|
338
|
-
return {
|
|
339
|
-
path,
|
|
340
|
-
relativePath: normalizeRelative(root, path),
|
|
341
|
-
directory,
|
|
342
|
-
language: 'python',
|
|
343
|
-
runtime: 'python3',
|
|
344
|
-
name: null,
|
|
345
|
-
version: null,
|
|
346
|
-
dependencies: deps,
|
|
347
|
-
devDependencies: [],
|
|
348
|
-
scripts: [],
|
|
349
|
-
manifestType: 'requirements.txt',
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
export function parsePipfile(
|
|
354
|
-
path: string,
|
|
355
|
-
directory: string,
|
|
356
|
-
root: string,
|
|
357
|
-
warnings: DetectionWarning[]
|
|
358
|
-
): PackageManifest | null {
|
|
359
|
-
const raw = safeRead(path);
|
|
360
|
-
if (raw === null) return null;
|
|
361
|
-
let toml: Record<string, unknown>;
|
|
362
|
-
try {
|
|
363
|
-
toml = parseToml(raw) as Record<string, unknown>;
|
|
364
|
-
} catch (err) {
|
|
365
|
-
warnings.push({
|
|
366
|
-
path,
|
|
367
|
-
reason: `Pipfile TOML parse failed: ${(err as Error).message}`,
|
|
368
|
-
});
|
|
369
|
-
return null;
|
|
370
|
-
}
|
|
371
|
-
const packages =
|
|
372
|
-
(toml.packages as Record<string, unknown> | undefined) ?? {};
|
|
373
|
-
const devPackages =
|
|
374
|
-
(toml['dev-packages'] as Record<string, unknown> | undefined) ?? {};
|
|
375
|
-
return {
|
|
376
|
-
path,
|
|
377
|
-
relativePath: normalizeRelative(root, path),
|
|
378
|
-
directory,
|
|
379
|
-
language: 'python',
|
|
380
|
-
runtime: 'python3',
|
|
381
|
-
name: null,
|
|
382
|
-
version: null,
|
|
383
|
-
dependencies: Object.keys(packages),
|
|
384
|
-
devDependencies: Object.keys(devPackages),
|
|
385
|
-
scripts: [],
|
|
386
|
-
manifestType: 'Pipfile',
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
export function parseCargoToml(
|
|
391
|
-
path: string,
|
|
392
|
-
directory: string,
|
|
393
|
-
root: string,
|
|
394
|
-
warnings: DetectionWarning[]
|
|
395
|
-
): PackageManifest | null {
|
|
396
|
-
const raw = safeRead(path);
|
|
397
|
-
if (raw === null) return null;
|
|
398
|
-
let toml: Record<string, unknown>;
|
|
399
|
-
try {
|
|
400
|
-
toml = parseToml(raw) as Record<string, unknown>;
|
|
401
|
-
} catch (err) {
|
|
402
|
-
warnings.push({
|
|
403
|
-
path,
|
|
404
|
-
reason: `Cargo.toml TOML parse failed: ${(err as Error).message}`,
|
|
405
|
-
});
|
|
406
|
-
return null;
|
|
407
|
-
}
|
|
408
|
-
const pkg = toml.package as Record<string, unknown> | undefined;
|
|
409
|
-
const deps = toml.dependencies as Record<string, unknown> | undefined;
|
|
410
|
-
const devDeps = toml['dev-dependencies'] as
|
|
411
|
-
| Record<string, unknown>
|
|
412
|
-
| undefined;
|
|
413
|
-
return {
|
|
414
|
-
path,
|
|
415
|
-
relativePath: normalizeRelative(root, path),
|
|
416
|
-
directory,
|
|
417
|
-
language: 'rust',
|
|
418
|
-
runtime: 'cargo',
|
|
419
|
-
name: typeof pkg?.name === 'string' ? (pkg.name as string) : null,
|
|
420
|
-
version: typeof pkg?.version === 'string' ? (pkg.version as string) : null,
|
|
421
|
-
dependencies: deps ? Object.keys(deps) : [],
|
|
422
|
-
devDependencies: devDeps ? Object.keys(devDeps) : [],
|
|
423
|
-
scripts: [],
|
|
424
|
-
manifestType: 'Cargo.toml',
|
|
425
|
-
};
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
export function parsePackageSwift(
|
|
429
|
-
path: string,
|
|
430
|
-
directory: string,
|
|
431
|
-
root: string,
|
|
432
|
-
_warnings: DetectionWarning[]
|
|
433
|
-
): PackageManifest | null {
|
|
434
|
-
const raw = safeRead(path);
|
|
435
|
-
if (raw === null) return null;
|
|
436
|
-
const deps: string[] = [];
|
|
437
|
-
// .package(url: "https://github.com/foo/bar.git", ...) → extract "bar"
|
|
438
|
-
const urlRe = /\.package\s*\(\s*(?:name\s*:\s*"([^"]+)"\s*,\s*)?url\s*:\s*"([^"]+)"/g;
|
|
439
|
-
let m: RegExpExecArray | null;
|
|
440
|
-
while ((m = urlRe.exec(raw)) !== null) {
|
|
441
|
-
const explicitName = m[1];
|
|
442
|
-
if (explicitName) {
|
|
443
|
-
deps.push(explicitName);
|
|
444
|
-
continue;
|
|
445
|
-
}
|
|
446
|
-
const url = m[2];
|
|
447
|
-
const last = url.split('/').pop() ?? '';
|
|
448
|
-
const clean = last.replace(/\.git$/, '').trim();
|
|
449
|
-
if (clean) deps.push(clean);
|
|
450
|
-
}
|
|
451
|
-
// name: "MyLibrary"
|
|
452
|
-
const nameMatch = /let\s+package\s*=\s*Package\s*\(\s*name\s*:\s*"([^"]+)"/.exec(
|
|
453
|
-
raw
|
|
454
|
-
);
|
|
455
|
-
return {
|
|
456
|
-
path,
|
|
457
|
-
relativePath: normalizeRelative(root, path),
|
|
458
|
-
directory,
|
|
459
|
-
language: 'swift',
|
|
460
|
-
runtime: 'xcode',
|
|
461
|
-
name: nameMatch ? nameMatch[1] : null,
|
|
462
|
-
version: null,
|
|
463
|
-
dependencies: deps,
|
|
464
|
-
devDependencies: [],
|
|
465
|
-
scripts: [],
|
|
466
|
-
manifestType: 'Package.swift',
|
|
467
|
-
};
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
export function parseGoMod(
|
|
471
|
-
path: string,
|
|
472
|
-
directory: string,
|
|
473
|
-
root: string,
|
|
474
|
-
_warnings: DetectionWarning[]
|
|
475
|
-
): PackageManifest | null {
|
|
476
|
-
const raw = safeRead(path);
|
|
477
|
-
if (raw === null) return null;
|
|
478
|
-
const deps: string[] = [];
|
|
479
|
-
let name: string | null = null;
|
|
480
|
-
let inRequire = false;
|
|
481
|
-
for (const rawLine of raw.split(/\r?\n/)) {
|
|
482
|
-
const line = rawLine.trim();
|
|
483
|
-
if (!line || line.startsWith('//')) continue;
|
|
484
|
-
if (line.startsWith('module ')) {
|
|
485
|
-
name = line.slice('module '.length).trim();
|
|
486
|
-
continue;
|
|
487
|
-
}
|
|
488
|
-
if (line === 'require (') {
|
|
489
|
-
inRequire = true;
|
|
490
|
-
continue;
|
|
491
|
-
}
|
|
492
|
-
if (inRequire) {
|
|
493
|
-
if (line === ')') {
|
|
494
|
-
inRequire = false;
|
|
495
|
-
continue;
|
|
496
|
-
}
|
|
497
|
-
const parts = line.split(/\s+/);
|
|
498
|
-
if (parts.length >= 2 && !parts[0].startsWith('//')) deps.push(parts[0]);
|
|
499
|
-
continue;
|
|
500
|
-
}
|
|
501
|
-
if (line.startsWith('require ')) {
|
|
502
|
-
const parts = line.slice('require '.length).trim().split(/\s+/);
|
|
503
|
-
if (parts[0]) deps.push(parts[0]);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
return {
|
|
507
|
-
path,
|
|
508
|
-
relativePath: normalizeRelative(root, path),
|
|
509
|
-
directory,
|
|
510
|
-
language: 'go',
|
|
511
|
-
runtime: 'go',
|
|
512
|
-
name,
|
|
513
|
-
version: null,
|
|
514
|
-
dependencies: deps,
|
|
515
|
-
devDependencies: [],
|
|
516
|
-
scripts: [],
|
|
517
|
-
manifestType: 'go.mod',
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
export function parsePomXml(
|
|
522
|
-
path: string,
|
|
523
|
-
directory: string,
|
|
524
|
-
root: string,
|
|
525
|
-
_warnings: DetectionWarning[]
|
|
526
|
-
): PackageManifest | null {
|
|
527
|
-
const raw = safeRead(path);
|
|
528
|
-
if (raw === null) return null;
|
|
529
|
-
const deps: string[] = [];
|
|
530
|
-
const depRe = /<dependency>[\s\S]*?<artifactId>([^<]+)<\/artifactId>[\s\S]*?<\/dependency>/g;
|
|
531
|
-
let m: RegExpExecArray | null;
|
|
532
|
-
while ((m = depRe.exec(raw)) !== null) deps.push(m[1].trim());
|
|
533
|
-
const nameMatch = /<artifactId>([^<]+)<\/artifactId>/.exec(raw);
|
|
534
|
-
const versionMatch = /<project[^>]*>[\s\S]*?<version>([^<]+)<\/version>/.exec(
|
|
535
|
-
raw
|
|
536
|
-
);
|
|
537
|
-
return {
|
|
538
|
-
path,
|
|
539
|
-
relativePath: normalizeRelative(root, path),
|
|
540
|
-
directory,
|
|
541
|
-
language: 'java',
|
|
542
|
-
runtime: 'jvm',
|
|
543
|
-
name: nameMatch ? nameMatch[1].trim() : null,
|
|
544
|
-
version: versionMatch ? versionMatch[1].trim() : null,
|
|
545
|
-
dependencies: deps,
|
|
546
|
-
devDependencies: [],
|
|
547
|
-
scripts: [],
|
|
548
|
-
manifestType: 'pom.xml',
|
|
549
|
-
};
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
export function parseBuildGradle(
|
|
553
|
-
path: string,
|
|
554
|
-
directory: string,
|
|
555
|
-
root: string,
|
|
556
|
-
_warnings: DetectionWarning[]
|
|
557
|
-
): PackageManifest | null {
|
|
558
|
-
const raw = safeRead(path);
|
|
559
|
-
if (raw === null) return null;
|
|
560
|
-
const deps: string[] = [];
|
|
561
|
-
const devDeps: string[] = [];
|
|
562
|
-
// implementation 'group:artifact:version' | implementation("group:artifact:version")
|
|
563
|
-
const re = /(implementation|api|runtimeOnly|compileOnly|testImplementation|testRuntimeOnly|androidTestImplementation)\s*[\("']+([^"'\)]+)[\)"']+/g;
|
|
564
|
-
let m: RegExpExecArray | null;
|
|
565
|
-
while ((m = re.exec(raw)) !== null) {
|
|
566
|
-
const scope = m[1];
|
|
567
|
-
const coord = m[2];
|
|
568
|
-
const parts = coord.split(':');
|
|
569
|
-
const artifact = parts.length >= 2 ? parts[1] : parts[0];
|
|
570
|
-
if (!artifact) continue;
|
|
571
|
-
if (scope.toLowerCase().startsWith('test')) devDeps.push(artifact);
|
|
572
|
-
else deps.push(artifact);
|
|
573
|
-
}
|
|
574
|
-
return {
|
|
575
|
-
path,
|
|
576
|
-
relativePath: normalizeRelative(root, path),
|
|
577
|
-
directory,
|
|
578
|
-
language: 'java',
|
|
579
|
-
runtime: 'jvm',
|
|
580
|
-
name: null,
|
|
581
|
-
version: null,
|
|
582
|
-
dependencies: deps,
|
|
583
|
-
devDependencies: devDeps,
|
|
584
|
-
scripts: [],
|
|
585
|
-
manifestType: 'build.gradle',
|
|
586
|
-
};
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
export function parseGemfile(
|
|
590
|
-
path: string,
|
|
591
|
-
directory: string,
|
|
592
|
-
root: string,
|
|
593
|
-
_warnings: DetectionWarning[]
|
|
594
|
-
): PackageManifest | null {
|
|
595
|
-
const raw = safeRead(path);
|
|
596
|
-
if (raw === null) return null;
|
|
597
|
-
const deps: string[] = [];
|
|
598
|
-
const devDeps: string[] = [];
|
|
599
|
-
let inDevGroup = false;
|
|
600
|
-
for (const rawLine of raw.split(/\r?\n/)) {
|
|
601
|
-
const line = rawLine.trim();
|
|
602
|
-
if (!line || line.startsWith('#')) continue;
|
|
603
|
-
if (/^group\s*:test|^group\s+:development/.test(line)) inDevGroup = true;
|
|
604
|
-
if (/^end\b/.test(line)) inDevGroup = false;
|
|
605
|
-
const gemMatch = /^gem\s+["']([^"']+)["']/.exec(line);
|
|
606
|
-
if (gemMatch) {
|
|
607
|
-
if (inDevGroup) devDeps.push(gemMatch[1]);
|
|
608
|
-
else deps.push(gemMatch[1]);
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
return {
|
|
612
|
-
path,
|
|
613
|
-
relativePath: normalizeRelative(root, path),
|
|
614
|
-
directory,
|
|
615
|
-
language: 'ruby',
|
|
616
|
-
runtime: 'ruby',
|
|
617
|
-
name: null,
|
|
618
|
-
version: null,
|
|
619
|
-
dependencies: deps,
|
|
620
|
-
devDependencies: devDeps,
|
|
621
|
-
scripts: [],
|
|
622
|
-
manifestType: 'Gemfile',
|
|
623
|
-
};
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
/**
|
|
627
|
-
* Parse a `mix.exs` (Elixir / Mix) file. Plan 1.5.1 — closes CR-39
|
|
628
|
-
* gap where Phoenix projects failed `npx massu init` with "no languages
|
|
629
|
-
* detected" because the package-detector layer didn't recognize the
|
|
630
|
-
* manifest. The AST adapter at `detect/adapters/phoenix.ts` was already
|
|
631
|
-
* shipped in 1.5.0 and works correctly when given a SourceFile[] directly.
|
|
632
|
-
*
|
|
633
|
-
* mix.exs is Elixir source (not a declarative format), but for detection
|
|
634
|
-
* purposes we extract `{:dep, "~> X.Y"}` style declarations via a regex
|
|
635
|
-
* scan. False-positive risk is low (any `{:atom, ...}` pattern that's
|
|
636
|
-
* NOT a dep is rare in mix.exs files outside the deps function).
|
|
637
|
-
*/
|
|
638
|
-
export function parseMixExs(
|
|
639
|
-
path: string,
|
|
640
|
-
directory: string,
|
|
641
|
-
root: string,
|
|
642
|
-
_warnings: DetectionWarning[],
|
|
643
|
-
): PackageManifest | null {
|
|
644
|
-
const raw = safeRead(path);
|
|
645
|
-
if (raw === null) return null;
|
|
646
|
-
const deps: string[] = [];
|
|
647
|
-
// Match `{:dep_name, ...}` — atom name as first tuple element.
|
|
648
|
-
const depPattern = /\{\s*:([a-z][a-z0-9_]*)\s*,/g;
|
|
649
|
-
let m: RegExpExecArray | null;
|
|
650
|
-
while ((m = depPattern.exec(raw)) !== null) {
|
|
651
|
-
if (!deps.includes(m[1])) deps.push(m[1]);
|
|
652
|
-
}
|
|
653
|
-
// Best-effort `app: :name` extraction (the `def project` block usually
|
|
654
|
-
// declares this).
|
|
655
|
-
const appMatch = /\bapp\s*:\s*:([a-z][a-z0-9_]*)/.exec(raw);
|
|
656
|
-
const name = appMatch ? appMatch[1] : null;
|
|
657
|
-
return {
|
|
658
|
-
path,
|
|
659
|
-
relativePath: normalizeRelative(root, path),
|
|
660
|
-
directory,
|
|
661
|
-
language: 'elixir',
|
|
662
|
-
runtime: 'beam',
|
|
663
|
-
name,
|
|
664
|
-
version: null,
|
|
665
|
-
dependencies: deps,
|
|
666
|
-
devDependencies: [],
|
|
667
|
-
scripts: [],
|
|
668
|
-
manifestType: 'mix.exs',
|
|
669
|
-
};
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
/**
|
|
673
|
-
* Parse a `*.csproj` (C# / .NET project) file. Plan 1.5.1 — closes CR-39
|
|
674
|
-
* gap where ASP.NET projects failed `npx massu init`. The AST adapter at
|
|
675
|
-
* `detect/adapters/aspnet.ts` was already shipped in 1.5.0 and works
|
|
676
|
-
* correctly.
|
|
677
|
-
*
|
|
678
|
-
* .csproj is XML; we use a lightweight regex scan rather than pulling
|
|
679
|
-
* an XML parser because the only fields we need are `<PackageReference
|
|
680
|
-
* Include="X" />` (deps) and the `Sdk="..."` attribute (framework hint).
|
|
681
|
-
* Full XML parsing would be over-engineered for this surface and risks
|
|
682
|
-
* adding a dependency for marginal gain.
|
|
683
|
-
*/
|
|
684
|
-
export function parseCsproj(
|
|
685
|
-
path: string,
|
|
686
|
-
directory: string,
|
|
687
|
-
root: string,
|
|
688
|
-
_warnings: DetectionWarning[],
|
|
689
|
-
): PackageManifest | null {
|
|
690
|
-
const raw = safeRead(path);
|
|
691
|
-
if (raw === null) return null;
|
|
692
|
-
const deps: string[] = [];
|
|
693
|
-
// Match `<PackageReference Include="Foo.Bar" ... />` (Include attribute
|
|
694
|
-
// value is the package id; the Version attribute is captured but we
|
|
695
|
-
// discard it since runtime/build distinction isn't expressed via
|
|
696
|
-
// this schema).
|
|
697
|
-
const pkgRefPattern = /<PackageReference\s+[^>]*Include\s*=\s*"([^"]+)"/gi;
|
|
698
|
-
let m: RegExpExecArray | null;
|
|
699
|
-
while ((m = pkgRefPattern.exec(raw)) !== null) {
|
|
700
|
-
if (!deps.includes(m[1])) deps.push(m[1]);
|
|
701
|
-
}
|
|
702
|
-
// The `<Project Sdk="Microsoft.NET.Sdk.Web">` attribute is a strong
|
|
703
|
-
// ASP.NET Core indicator that doesn't appear as a PackageReference.
|
|
704
|
-
// Surface it as a dep so framework-detector rules (which match against
|
|
705
|
-
// the deps set) can fire on it. Same downstream consumer; no new
|
|
706
|
-
// signal channel needed.
|
|
707
|
-
const sdkMatch = /<Project\s+[^>]*Sdk\s*=\s*"([^"]+)"/i.exec(raw);
|
|
708
|
-
if (sdkMatch && !deps.includes(sdkMatch[1])) {
|
|
709
|
-
deps.push(sdkMatch[1]);
|
|
710
|
-
}
|
|
711
|
-
// Best-effort name from filename (Foo.csproj → "Foo").
|
|
712
|
-
const fname = path.split(/[/\\]/).pop() ?? '';
|
|
713
|
-
const name = fname.endsWith('.csproj') ? fname.slice(0, -'.csproj'.length) : null;
|
|
714
|
-
return {
|
|
715
|
-
path,
|
|
716
|
-
relativePath: normalizeRelative(root, path),
|
|
717
|
-
directory,
|
|
718
|
-
language: 'csharp',
|
|
719
|
-
runtime: 'dotnet',
|
|
720
|
-
name,
|
|
721
|
-
version: null,
|
|
722
|
-
dependencies: deps,
|
|
723
|
-
devDependencies: [],
|
|
724
|
-
scripts: [],
|
|
725
|
-
manifestType: '*.csproj',
|
|
726
|
-
};
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
function detectManifestsInDir(
|
|
730
|
-
dir: string,
|
|
731
|
-
root: string,
|
|
732
|
-
warnings: DetectionWarning[]
|
|
733
|
-
): PackageManifest[] {
|
|
734
|
-
// Plan 1.5.1: dispatch via the canonical MANIFEST_REGISTRY (single
|
|
735
|
-
// source-of-truth). The previous hand-rolled MANIFEST_FILES list +
|
|
736
|
-
// switch statement led to drift with runner.ts:buildDetectionSignals
|
|
737
|
-
// (Phoenix + ASP.NET were unreachable). Closed by registry.
|
|
738
|
-
// Lazy import to avoid ESM cycle; getManifestRegistry() is itself
|
|
739
|
-
// lazy-initialized.
|
|
740
|
-
const { getManifestRegistry, matchManifestPattern } = registryModule;
|
|
741
|
-
const out: PackageManifest[] = [];
|
|
742
|
-
let dirEntries: string[] | null = null;
|
|
743
|
-
for (const entry of getManifestRegistry()) {
|
|
744
|
-
if (!entry.pattern.startsWith('*')) {
|
|
745
|
-
// Exact-filename pattern: O(1) existence check.
|
|
746
|
-
const path = join(dir, entry.pattern);
|
|
747
|
-
if (!existsSync(path)) continue;
|
|
748
|
-
const m = entry.parse(path, dir, root, warnings);
|
|
749
|
-
if (m !== null) out.push(m);
|
|
750
|
-
} else {
|
|
751
|
-
// Extension-glob pattern: scan dir for matches. Lazy-readdir so
|
|
752
|
-
// we pay the cost only when at least one glob entry exists.
|
|
753
|
-
if (dirEntries === null) {
|
|
754
|
-
try {
|
|
755
|
-
dirEntries = readdirSync(dir);
|
|
756
|
-
} catch {
|
|
757
|
-
dirEntries = [];
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
for (const fname of dirEntries) {
|
|
761
|
-
if (!matchManifestPattern(fname, entry.pattern)) continue;
|
|
762
|
-
const path = join(dir, fname);
|
|
763
|
-
if (!existsSync(path)) continue;
|
|
764
|
-
const m = entry.parse(path, dir, root, warnings);
|
|
765
|
-
if (m !== null) out.push(m);
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
return out;
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
// Imported lazily-via-namespace to break the ESM cycle (manifest-registry
|
|
773
|
-
// imports parsers from THIS module). The namespace import is hoisted to
|
|
774
|
-
// the top of the module by ESM, but the named members are resolved on
|
|
775
|
-
// access — by the time `detectManifestsInDir` runs, the registry module's
|
|
776
|
-
// top-level evaluation has completed.
|
|
777
|
-
import * as registryModule from './manifest-registry.ts';
|
|
778
|
-
|
|
779
|
-
function listSubdirs(dir: string): string[] {
|
|
780
|
-
try {
|
|
781
|
-
return readdirSync(dir, { withFileTypes: true })
|
|
782
|
-
.filter((e) => e.isDirectory() && !IGNORED_DIRS.has(e.name))
|
|
783
|
-
.map((e) => join(dir, e.name));
|
|
784
|
-
} catch {
|
|
785
|
-
return [];
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
/**
|
|
790
|
-
* Scan a project root for dependency manifests.
|
|
791
|
-
*
|
|
792
|
-
* Walks projectRoot plus conventional workspace subtrees (`apps/*`,
|
|
793
|
-
* `packages/*`, `services/*`, `libs/*`, `modules/*`) up to 2 levels deep.
|
|
794
|
-
*/
|
|
795
|
-
export function detectPackageManifests(
|
|
796
|
-
projectRoot: string
|
|
797
|
-
): PackageDetectionResult {
|
|
798
|
-
const warnings: DetectionWarning[] = [];
|
|
799
|
-
const manifests: PackageManifest[] = [];
|
|
800
|
-
|
|
801
|
-
// Level 0: projectRoot itself
|
|
802
|
-
manifests.push(...detectManifestsInDir(projectRoot, projectRoot, warnings));
|
|
803
|
-
|
|
804
|
-
// Level 1: workspace roots (apps/, packages/, services/, libs/, modules/)
|
|
805
|
-
for (const ws of WORKSPACE_DIRS) {
|
|
806
|
-
const wsRoot = join(projectRoot, ws);
|
|
807
|
-
if (!existsSync(wsRoot)) continue;
|
|
808
|
-
for (const sub of listSubdirs(wsRoot)) {
|
|
809
|
-
manifests.push(...detectManifestsInDir(sub, projectRoot, warnings));
|
|
810
|
-
// Level 2 (one nesting allowed, e.g., apps/ios/<target>)
|
|
811
|
-
for (const sub2 of listSubdirs(sub)) {
|
|
812
|
-
manifests.push(...detectManifestsInDir(sub2, projectRoot, warnings));
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
// Deduplicate by manifest path (rare, but a nested workspace dir equal to its
|
|
818
|
-
// parent by coincidence could double-scan).
|
|
819
|
-
const seen = new Set<string>();
|
|
820
|
-
const dedup: PackageManifest[] = [];
|
|
821
|
-
for (const m of manifests) {
|
|
822
|
-
if (seen.has(m.path)) continue;
|
|
823
|
-
seen.add(m.path);
|
|
824
|
-
dedup.push(m);
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
return { manifests: dedup, warnings };
|
|
828
|
-
}
|