@massu/core 1.10.8 → 1.12.0

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 (178) hide show
  1. package/CHANGELOG.md +1303 -0
  2. package/dist/adapter.js +6 -4
  3. package/dist/cli.js +517 -397
  4. package/dist/detect/adapters/.bundle-shasums.json +4 -7
  5. package/dist/detect/adapters/rails.js +6 -4
  6. package/dist/detect/adapters/spring.js +6 -4
  7. package/dist/detect/adapters/tree-sitter-loader.js +6 -4
  8. package/dist/hooks/auto-learning-pipeline.js +24 -1
  9. package/dist/hooks/classify-failure.js +32 -3
  10. package/dist/hooks/cost-tracker.js +24 -0
  11. package/dist/hooks/fix-detector.js +97 -1
  12. package/dist/hooks/incident-pipeline.js +30 -1
  13. package/dist/hooks/intent-suggester.js +6 -3
  14. package/dist/hooks/post-edit-context.js +24 -1
  15. package/dist/hooks/post-tool-use.js +66 -18
  16. package/dist/hooks/pre-compact.js +27 -2
  17. package/dist/hooks/pre-delete-check.js +48 -22
  18. package/dist/hooks/pre-tool-use-gate.js +962 -0
  19. package/dist/hooks/quality-event.js +24 -0
  20. package/dist/hooks/rule-enforcement-pipeline.js +24 -1
  21. package/dist/hooks/security-gate.js +29 -16
  22. package/dist/hooks/session-end.js +24 -0
  23. package/dist/hooks/session-start.js +8710 -8460
  24. package/dist/hooks/user-prompt.js +24 -0
  25. package/package.json +15 -18
  26. package/commands/README.md +0 -134
  27. package/src/adapter.ts +0 -121
  28. package/src/adr-generator.ts +0 -292
  29. package/src/analytics.ts +0 -373
  30. package/src/audit-trail.ts +0 -450
  31. package/src/backfill-sessions.ts +0 -182
  32. package/src/changelog-generator.ts +0 -178
  33. package/src/cli.ts +0 -246
  34. package/src/cloud-sync.ts +0 -234
  35. package/src/commands/adapters.ts +0 -824
  36. package/src/commands/changelog.ts +0 -165
  37. package/src/commands/config-check-drift.ts +0 -133
  38. package/src/commands/config-refresh.ts +0 -396
  39. package/src/commands/config-upgrade.ts +0 -127
  40. package/src/commands/doctor.ts +0 -485
  41. package/src/commands/hook-runner.ts +0 -145
  42. package/src/commands/init.ts +0 -1823
  43. package/src/commands/install-commands.ts +0 -692
  44. package/src/commands/install-hooks.ts +0 -27
  45. package/src/commands/permissions.ts +0 -150
  46. package/src/commands/refresh-log.ts +0 -37
  47. package/src/commands/show-template.ts +0 -65
  48. package/src/commands/template-engine.ts +0 -301
  49. package/src/commands/watch.ts +0 -430
  50. package/src/config.ts +0 -765
  51. package/src/cost-tracker.ts +0 -355
  52. package/src/db.ts +0 -370
  53. package/src/dependency-scorer.ts +0 -337
  54. package/src/detect/adapters/aspnet.ts +0 -4
  55. package/src/detect/adapters/discover.ts +0 -469
  56. package/src/detect/adapters/file-sampler.ts +0 -225
  57. package/src/detect/adapters/go-chi.ts +0 -4
  58. package/src/detect/adapters/index.ts +0 -51
  59. package/src/detect/adapters/nextjs-trpc.ts +0 -166
  60. package/src/detect/adapters/parse-guard.ts +0 -133
  61. package/src/detect/adapters/phoenix.ts +0 -4
  62. package/src/detect/adapters/python-django.ts +0 -208
  63. package/src/detect/adapters/python-fastapi.ts +0 -223
  64. package/src/detect/adapters/python-flask.ts +0 -235
  65. package/src/detect/adapters/query-helpers.ts +0 -170
  66. package/src/detect/adapters/rails.ts +0 -4
  67. package/src/detect/adapters/runner.ts +0 -284
  68. package/src/detect/adapters/spring.ts +0 -4
  69. package/src/detect/adapters/swift-swiftui.ts +0 -171
  70. package/src/detect/adapters/tree-sitter-loader.ts +0 -517
  71. package/src/detect/adapters/types.ts +0 -191
  72. package/src/detect/codebase-introspector.ts +0 -210
  73. package/src/detect/domain-inferrer.ts +0 -197
  74. package/src/detect/drift.ts +0 -199
  75. package/src/detect/framework-detector.ts +0 -307
  76. package/src/detect/index.ts +0 -209
  77. package/src/detect/manifest-registry.ts +0 -261
  78. package/src/detect/migrate.ts +0 -362
  79. package/src/detect/monorepo-detector.ts +0 -348
  80. package/src/detect/package-detector.ts +0 -828
  81. package/src/detect/passthrough.ts +0 -108
  82. package/src/detect/regex-fallback.ts +0 -449
  83. package/src/detect/source-dir-detector.ts +0 -297
  84. package/src/detect/vr-command-map.ts +0 -167
  85. package/src/docs-map.json +0 -100
  86. package/src/docs-tools.ts +0 -519
  87. package/src/domains.ts +0 -182
  88. package/src/hooks/auto-learning-pipeline.ts +0 -228
  89. package/src/hooks/classify-failure.ts +0 -259
  90. package/src/hooks/cost-tracker.ts +0 -66
  91. package/src/hooks/fix-detector.ts +0 -186
  92. package/src/hooks/incident-pipeline.ts +0 -190
  93. package/src/hooks/intent-suggester.ts +0 -131
  94. package/src/hooks/post-edit-context.ts +0 -91
  95. package/src/hooks/post-tool-use.ts +0 -413
  96. package/src/hooks/pre-compact.ts +0 -168
  97. package/src/hooks/pre-delete-check.ts +0 -243
  98. package/src/hooks/quality-event.ts +0 -127
  99. package/src/hooks/rule-enforcement-pipeline.ts +0 -159
  100. package/src/hooks/security-gate.ts +0 -153
  101. package/src/hooks/session-end.ts +0 -467
  102. package/src/hooks/session-start.ts +0 -436
  103. package/src/hooks/user-prompt.ts +0 -157
  104. package/src/import-resolver.ts +0 -226
  105. package/src/knowledge-db.ts +0 -176
  106. package/src/knowledge-indexer.ts +0 -708
  107. package/src/knowledge-tools.ts +0 -1414
  108. package/src/lib/fileLock.ts +0 -203
  109. package/src/lib/framework-supports.ts +0 -85
  110. package/src/lib/gitToplevel.ts +0 -22
  111. package/src/lib/hook-registry.ts +0 -43
  112. package/src/lib/installLock.ts +0 -66
  113. package/src/lib/memory-path.ts +0 -49
  114. package/src/lib/pidLiveness.ts +0 -67
  115. package/src/lib/settings-local.ts +0 -110
  116. package/src/lib/sql-table-names.ts +0 -71
  117. package/src/license.ts +0 -547
  118. package/src/lsp/auto-detect.ts +0 -98
  119. package/src/lsp/client.ts +0 -776
  120. package/src/lsp/enrich.ts +0 -127
  121. package/src/lsp/types.ts +0 -221
  122. package/src/memory-db.ts +0 -1614
  123. package/src/memory-file-ingest.ts +0 -128
  124. package/src/memory-tools.ts +0 -424
  125. package/src/middleware-tree.ts +0 -71
  126. package/src/observability-tools.ts +0 -343
  127. package/src/observation-extractor.ts +0 -418
  128. package/src/page-deps.ts +0 -285
  129. package/src/permissions.ts +0 -422
  130. package/src/prompt-analyzer.ts +0 -332
  131. package/src/python/coupling-detector.ts +0 -125
  132. package/src/python/domain-enforcer.ts +0 -84
  133. package/src/python/impact-analyzer.ts +0 -96
  134. package/src/python/import-parser.ts +0 -244
  135. package/src/python/import-resolver.ts +0 -136
  136. package/src/python/migration-indexer.ts +0 -116
  137. package/src/python/migration-parser.ts +0 -332
  138. package/src/python/model-indexer.ts +0 -71
  139. package/src/python/model-parser.ts +0 -279
  140. package/src/python/route-indexer.ts +0 -59
  141. package/src/python/route-parser.ts +0 -317
  142. package/src/python-tools.ts +0 -630
  143. package/src/regression-detector.ts +0 -319
  144. package/src/rules.ts +0 -57
  145. package/src/schema-mapper.ts +0 -232
  146. package/src/security/adapter-origin.ts +0 -130
  147. package/src/security/adapter-verifier.ts +0 -319
  148. package/src/security/atomic-write.ts +0 -164
  149. package/src/security/fetcher.ts +0 -200
  150. package/src/security/install-tracking.ts +0 -319
  151. package/src/security/license-pubkey.generated.ts +0 -30
  152. package/src/security/license-response-verifier.ts +0 -149
  153. package/src/security/local-fingerprint.ts +0 -225
  154. package/src/security/manifest-cache.ts +0 -333
  155. package/src/security/manifest-schema.ts +0 -129
  156. package/src/security/registry-pubkey.generated.ts +0 -35
  157. package/src/security/telemetry.ts +0 -320
  158. package/src/security-scorer.ts +0 -405
  159. package/src/security-utils.ts +0 -133
  160. package/src/sentinel-db.ts +0 -580
  161. package/src/sentinel-scanner.ts +0 -406
  162. package/src/sentinel-tools.ts +0 -513
  163. package/src/sentinel-types.ts +0 -140
  164. package/src/server-dispatch.ts +0 -225
  165. package/src/server.ts +0 -107
  166. package/src/session-archiver.ts +0 -111
  167. package/src/session-state-generator.ts +0 -174
  168. package/src/team-knowledge.ts +0 -407
  169. package/src/tool-db-needs.ts +0 -232
  170. package/src/tools.ts +0 -1206
  171. package/src/transcript-parser.ts +0 -458
  172. package/src/trpc-index.ts +0 -215
  173. package/src/validate-features-runner.ts +0 -107
  174. package/src/validation-engine.ts +0 -358
  175. package/src/watch/daemon.ts +0 -385
  176. package/src/watch/lockfile-detector.ts +0 -65
  177. package/src/watch/paths.ts +0 -279
  178. 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
- }