@lakindu_perera/toren 1.0.7 → 1.0.8
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 +19 -0
- package/README.md +120 -31
- package/bin/toren.js +3 -0
- package/package.json +7 -3
- package/src/detectors/health-detector.js +72 -0
- package/src/detectors/important-files-detector.js +515 -0
- package/src/detectors/package-manager-detector.js +112 -0
- package/src/detectors/project-info-detector.js +89 -0
- package/src/detectors/script-detector.js +226 -10
- package/src/focused-output.js +78 -8
- package/src/renderers/console-renderer.js +53 -4
- package/src/renderers/html-renderer.js +128 -6
- package/src/renderers/json-renderer.js +15 -1
- package/src/renderers/markdown-renderer.js +70 -10
- package/src/scanner/scan.js +28 -6
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Toren — Important File Detector
|
|
3
|
+
*
|
|
4
|
+
* Identifies the most strategically significant files in a project, based
|
|
5
|
+
* on the already-scanned flat file list and detected project context.
|
|
6
|
+
*
|
|
7
|
+
* Design contract:
|
|
8
|
+
* - Pure function. Uses only the data already produced by scan().
|
|
9
|
+
* - No filesystem I/O. No directory traversal.
|
|
10
|
+
* - Deterministic: same input always produces the same output.
|
|
11
|
+
* - No duplicates: each path appears at most once (highest priority wins).
|
|
12
|
+
* - Sorted: highest priority first; ties broken alphabetically by path.
|
|
13
|
+
* - Never returns null; importantFiles is always an array.
|
|
14
|
+
* - Framework-specific files are only marked important when they exist.
|
|
15
|
+
*
|
|
16
|
+
* Priority anchors:
|
|
17
|
+
* 100 – Project manifest (package.json, pom.xml, Cargo.toml, …)
|
|
18
|
+
* 95 – Primary documentation (README.md)
|
|
19
|
+
* 90 – Primary detected entry point
|
|
20
|
+
* 85 – Framework configuration (next.config.*, vite.config.*, …)
|
|
21
|
+
* 82 – App-level framework files (root layout, root page, …)
|
|
22
|
+
* 80 – Environment documentation (.env.example)
|
|
23
|
+
* 78 – Secondary framework files (middleware, server entry)
|
|
24
|
+
* 75 – TypeScript / language config
|
|
25
|
+
* 72 – Project-specific config (routes, config/application)
|
|
26
|
+
* 70 – Container (Dockerfile)
|
|
27
|
+
* 68 – Container orchestration (docker-compose)
|
|
28
|
+
*
|
|
29
|
+
* @module detectors/important-files-detector
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Types (JSDoc — no TypeScript dependency required)
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {Object} ImportantFile
|
|
38
|
+
* @property {string} path - POSIX relative path from the project root
|
|
39
|
+
* @property {string} type - Semantic role: 'manifest' | 'documentation' |
|
|
40
|
+
* 'entry-point' | 'configuration' | 'environment' |
|
|
41
|
+
* 'container' | 'utility'
|
|
42
|
+
* @property {string} reason - Human-readable explanation of why this file matters
|
|
43
|
+
* @property {number} priority - Sort weight; higher = more important
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Static candidate tables — files important for every project type
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Files that are generically important regardless of project type.
|
|
52
|
+
* Each entry is included only if the file actually exists in flatFiles.
|
|
53
|
+
*
|
|
54
|
+
* @type {ImportantFile[]}
|
|
55
|
+
*/
|
|
56
|
+
const GENERIC_CANDIDATES = [
|
|
57
|
+
// ── Package manager manifests ─────────────────────────────────────────────
|
|
58
|
+
{
|
|
59
|
+
path: 'package.json',
|
|
60
|
+
type: 'manifest',
|
|
61
|
+
reason: 'Defines dependencies, scripts, and project metadata',
|
|
62
|
+
priority: 100,
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// ── Primary documentation ─────────────────────────────────────────────────
|
|
66
|
+
{
|
|
67
|
+
path: 'README.md',
|
|
68
|
+
type: 'documentation',
|
|
69
|
+
reason: 'Primary project documentation',
|
|
70
|
+
priority: 95,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
path: 'README',
|
|
74
|
+
type: 'documentation',
|
|
75
|
+
reason: 'Primary project documentation',
|
|
76
|
+
priority: 95,
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
// ── Environment documentation ─────────────────────────────────────────────
|
|
80
|
+
{
|
|
81
|
+
path: '.env.example',
|
|
82
|
+
type: 'environment',
|
|
83
|
+
reason: 'Documents required environment variables',
|
|
84
|
+
priority: 80,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
path: '.env.sample',
|
|
88
|
+
type: 'environment',
|
|
89
|
+
reason: 'Documents required environment variables',
|
|
90
|
+
priority: 80,
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
// ── TypeScript ────────────────────────────────────────────────────────────
|
|
94
|
+
{
|
|
95
|
+
path: 'tsconfig.json',
|
|
96
|
+
type: 'configuration',
|
|
97
|
+
reason: 'TypeScript compiler configuration',
|
|
98
|
+
priority: 75,
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
// ── Container ─────────────────────────────────────────────────────────────
|
|
102
|
+
{
|
|
103
|
+
path: 'Dockerfile',
|
|
104
|
+
type: 'container',
|
|
105
|
+
reason: 'Container build configuration',
|
|
106
|
+
priority: 70,
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
path: 'docker-compose.yml',
|
|
110
|
+
type: 'container',
|
|
111
|
+
reason: 'Multi-container Docker configuration',
|
|
112
|
+
priority: 68,
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
path: 'docker-compose.yaml',
|
|
116
|
+
type: 'container',
|
|
117
|
+
reason: 'Multi-container Docker configuration',
|
|
118
|
+
priority: 68,
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Non-JS project manifests — same role as package.json for their ecosystems.
|
|
124
|
+
* Checked unconditionally because they might be present in polyglot repos.
|
|
125
|
+
*
|
|
126
|
+
* @type {ImportantFile[]}
|
|
127
|
+
*/
|
|
128
|
+
const ECOSYSTEM_MANIFESTS = [
|
|
129
|
+
{
|
|
130
|
+
path: 'pom.xml',
|
|
131
|
+
type: 'manifest',
|
|
132
|
+
reason: 'Maven project descriptor and dependency manifest',
|
|
133
|
+
priority: 100,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
path: 'build.gradle',
|
|
137
|
+
type: 'manifest',
|
|
138
|
+
reason: 'Gradle build script and dependency manifest',
|
|
139
|
+
priority: 100,
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
path: 'build.gradle.kts',
|
|
143
|
+
type: 'manifest',
|
|
144
|
+
reason: 'Gradle Kotlin DSL build script',
|
|
145
|
+
priority: 100,
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
path: 'Cargo.toml',
|
|
149
|
+
type: 'manifest',
|
|
150
|
+
reason: 'Rust package manifest and dependency configuration',
|
|
151
|
+
priority: 100,
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
path: 'composer.json',
|
|
155
|
+
type: 'manifest',
|
|
156
|
+
reason: 'PHP Composer dependency manifest',
|
|
157
|
+
priority: 100,
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
path: 'Gemfile',
|
|
161
|
+
type: 'manifest',
|
|
162
|
+
reason: 'Ruby gem dependency manifest',
|
|
163
|
+
priority: 100,
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
path: 'go.mod',
|
|
167
|
+
type: 'manifest',
|
|
168
|
+
reason: 'Go module definition and dependency manifest',
|
|
169
|
+
priority: 100,
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
path: 'pyproject.toml',
|
|
173
|
+
type: 'manifest',
|
|
174
|
+
reason: 'Python project configuration and dependency manifest',
|
|
175
|
+
priority: 100,
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
path: 'requirements.txt',
|
|
179
|
+
type: 'manifest',
|
|
180
|
+
reason: 'Python runtime dependency list',
|
|
181
|
+
priority: 95,
|
|
182
|
+
},
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// Pattern-matching helpers (no filesystem access — operate on flatFiles array)
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Push a candidate entry if the exact path exists in the file set.
|
|
191
|
+
*
|
|
192
|
+
* @param {ImportantFile[]} out
|
|
193
|
+
* @param {Set<string>} fileSet
|
|
194
|
+
* @param {string} filePath
|
|
195
|
+
* @param {string} type
|
|
196
|
+
* @param {string} reason
|
|
197
|
+
* @param {number} priority
|
|
198
|
+
*/
|
|
199
|
+
function addExact(out, fileSet, filePath, type, reason, priority) {
|
|
200
|
+
if (fileSet.has(filePath)) {
|
|
201
|
+
out.push({ path: filePath, type, reason, priority });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Push candidates for all files whose path starts with the given prefix.
|
|
207
|
+
* Used for patterns like 'vite.config.' (matches vite.config.js, .ts, .mjs …).
|
|
208
|
+
*
|
|
209
|
+
* @param {ImportantFile[]} out
|
|
210
|
+
* @param {string[]} flatFiles
|
|
211
|
+
* @param {string} prefix - e.g. 'vite.config.'
|
|
212
|
+
* @param {string} type
|
|
213
|
+
* @param {string} reason
|
|
214
|
+
* @param {number} priority
|
|
215
|
+
*/
|
|
216
|
+
function addPrefix(out, flatFiles, prefix, type, reason, priority) {
|
|
217
|
+
for (const f of flatFiles) {
|
|
218
|
+
if (f.startsWith(prefix)) {
|
|
219
|
+
out.push({ path: f, type, reason, priority });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Push candidates for all files whose path ends with the given suffix.
|
|
226
|
+
* Used for patterns like '*Application.java' (matches any depth).
|
|
227
|
+
*
|
|
228
|
+
* @param {ImportantFile[]} out
|
|
229
|
+
* @param {string[]} flatFiles
|
|
230
|
+
* @param {string} suffix - e.g. 'Application.java'
|
|
231
|
+
* @param {string} type
|
|
232
|
+
* @param {string} reason
|
|
233
|
+
* @param {number} priority
|
|
234
|
+
*/
|
|
235
|
+
function addSuffix(out, flatFiles, suffix, type, reason, priority) {
|
|
236
|
+
for (const f of flatFiles) {
|
|
237
|
+
if (f.endsWith(suffix)) {
|
|
238
|
+
out.push({ path: f, type, reason, priority });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Push candidates for root-level files matching `stem.*`.
|
|
245
|
+
* "Root-level" means the path contains no '/' after the stem.
|
|
246
|
+
* Used for patterns like 'server.*', 'app.*' (root only, not src/server.*).
|
|
247
|
+
*
|
|
248
|
+
* @param {ImportantFile[]} out
|
|
249
|
+
* @param {string[]} flatFiles
|
|
250
|
+
* @param {string} stem - e.g. 'server.' (note trailing dot)
|
|
251
|
+
* @param {string} type
|
|
252
|
+
* @param {string} reason
|
|
253
|
+
* @param {number} priority
|
|
254
|
+
*/
|
|
255
|
+
function addRootPrefix(out, flatFiles, stem, type, reason, priority) {
|
|
256
|
+
for (const f of flatFiles) {
|
|
257
|
+
// Must start with stem AND have no directory separator in the remainder
|
|
258
|
+
if (f.startsWith(stem) && !f.slice(stem.length).includes('/')) {
|
|
259
|
+
out.push({ path: f, type, reason, priority });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
// Project-specific candidate builder
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Return framework-specific candidates based on detected project type.
|
|
270
|
+
* Only files that exist in flatFiles are included (checked by callers via
|
|
271
|
+
* addExact/addPrefix/addSuffix/addRootPrefix).
|
|
272
|
+
*
|
|
273
|
+
* @param {string} projectType - From ScanResult.projectType
|
|
274
|
+
* @param {string[]} flatFiles - All scanned file paths
|
|
275
|
+
* @param {Set<string>} fileSet - flatFiles as Set for O(1) lookup
|
|
276
|
+
* @returns {ImportantFile[]}
|
|
277
|
+
*/
|
|
278
|
+
function getProjectCandidates(projectType, flatFiles, fileSet) {
|
|
279
|
+
const out = [];
|
|
280
|
+
const pt = projectType.toLowerCase();
|
|
281
|
+
|
|
282
|
+
// ── Framework build configs (apply to many ecosystems) ───────────────────
|
|
283
|
+
addPrefix(out, flatFiles, 'vite.config.', 'configuration', 'Vite build and development configuration', 85);
|
|
284
|
+
addPrefix(out, flatFiles, 'webpack.config.','configuration', 'Webpack bundler configuration', 85);
|
|
285
|
+
|
|
286
|
+
// ── Next.js ───────────────────────────────────────────────────────────────
|
|
287
|
+
if (pt.includes('next')) {
|
|
288
|
+
// Framework config
|
|
289
|
+
for (const f of ['next.config.js', 'next.config.mjs', 'next.config.ts']) {
|
|
290
|
+
addExact(out, fileSet, f, 'configuration', 'Next.js framework configuration', 85);
|
|
291
|
+
}
|
|
292
|
+
// App Router
|
|
293
|
+
for (const ext of ['js', 'jsx', 'ts', 'tsx']) {
|
|
294
|
+
addExact(out, fileSet, `app/layout.${ext}`, 'entry-point', 'Root layout for the Next.js App Router', 82);
|
|
295
|
+
addExact(out, fileSet, `app/page.${ext}`, 'entry-point', 'Root page for the Next.js App Router', 81);
|
|
296
|
+
}
|
|
297
|
+
// Edge middleware
|
|
298
|
+
for (const f of ['middleware.js', 'middleware.ts']) {
|
|
299
|
+
addExact(out, fileSet, f, 'configuration', 'Next.js edge middleware', 78);
|
|
300
|
+
}
|
|
301
|
+
// Pages Router
|
|
302
|
+
addPrefix(out, flatFiles, 'pages/index.', 'entry-point', 'Home page for the Next.js Pages Router', 78);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ── Nuxt.js ───────────────────────────────────────────────────────────────
|
|
306
|
+
else if (pt.includes('nuxt')) {
|
|
307
|
+
for (const f of ['nuxt.config.js', 'nuxt.config.ts', 'nuxt.config.mjs']) {
|
|
308
|
+
addExact(out, fileSet, f, 'configuration', 'Nuxt.js framework configuration', 85);
|
|
309
|
+
}
|
|
310
|
+
for (const ext of ['js', 'vue', 'ts']) {
|
|
311
|
+
addExact(out, fileSet, `app.${ext}`, 'entry-point', 'Nuxt application root', 82);
|
|
312
|
+
addExact(out, fileSet, `pages/index.${ext}`, 'entry-point', 'Nuxt home page (Pages Router)', 78);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ── React (Vite / CRA) ───────────────────────────────────────────────────
|
|
317
|
+
else if (pt.includes('react')) {
|
|
318
|
+
addPrefix(out, flatFiles, 'src/main.', 'entry-point', 'Application entry point', 82);
|
|
319
|
+
addPrefix(out, flatFiles, 'src/App.', 'entry-point', 'Root application component', 78);
|
|
320
|
+
// CRA fallback
|
|
321
|
+
addExact(out, fileSet, 'public/index.html', 'entry-point', 'Application HTML entry point', 76);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ── Vue.js ────────────────────────────────────────────────────────────────
|
|
325
|
+
else if (pt.includes('vue')) {
|
|
326
|
+
for (const f of ['vue.config.js', 'vue.config.ts']) {
|
|
327
|
+
addExact(out, fileSet, f, 'configuration', 'Vue CLI configuration', 85);
|
|
328
|
+
}
|
|
329
|
+
addPrefix(out, flatFiles, 'src/main.', 'entry-point', 'Application entry point', 82);
|
|
330
|
+
addPrefix(out, flatFiles, 'src/App.', 'entry-point', 'Root application component', 78);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ── Angular ───────────────────────────────────────────────────────────────
|
|
334
|
+
else if (pt.includes('angular')) {
|
|
335
|
+
addExact(out, fileSet, 'angular.json', 'configuration', 'Angular workspace configuration', 85);
|
|
336
|
+
addPrefix(out, flatFiles, 'src/main.', 'entry-point', 'Angular bootstrap entry point', 82);
|
|
337
|
+
addExact(out, fileSet, 'src/app/app.module.ts', 'entry-point', 'Angular root module', 80);
|
|
338
|
+
addExact(out, fileSet, 'src/app/app.component.ts','entry-point','Angular root component', 78);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ── Svelte ────────────────────────────────────────────────────────────────
|
|
342
|
+
else if (pt.includes('svelte')) {
|
|
343
|
+
for (const f of ['svelte.config.js', 'svelte.config.ts']) {
|
|
344
|
+
addExact(out, fileSet, f, 'configuration', 'SvelteKit / Svelte configuration', 85);
|
|
345
|
+
}
|
|
346
|
+
addPrefix(out, flatFiles, 'src/routes/+page.', 'entry-point', 'SvelteKit home page route', 82);
|
|
347
|
+
addPrefix(out, flatFiles, 'src/main.', 'entry-point', 'Svelte application entry point', 80);
|
|
348
|
+
addPrefix(out, flatFiles, 'src/App.', 'entry-point', 'Svelte root component', 78);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ── Express / Fastify / Koa (Node.js server frameworks) ──────────────────
|
|
352
|
+
else if (pt.includes('express') || pt.includes('fastify') || pt.includes('koa')) {
|
|
353
|
+
addRootPrefix(out, flatFiles, 'server.', 'entry-point', 'Server application entry point', 82);
|
|
354
|
+
addRootPrefix(out, flatFiles, 'app.', 'entry-point', 'Application entry point', 80);
|
|
355
|
+
addPrefix(out, flatFiles, 'src/server.', 'entry-point', 'Server application entry point', 80);
|
|
356
|
+
addPrefix(out, flatFiles, 'src/app.', 'entry-point', 'Application entry point', 78);
|
|
357
|
+
// Common conventions
|
|
358
|
+
addExact(out, fileSet, 'src/index.js', 'entry-point', 'Application entry point', 76);
|
|
359
|
+
addExact(out, fileSet, 'src/index.ts', 'entry-point', 'Application entry point', 76);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ── Python ────────────────────────────────────────────────────────────────
|
|
363
|
+
if (pt.includes('python')) {
|
|
364
|
+
addExact(out, fileSet, 'main.py', 'entry-point', 'Main Python application entry point', 82);
|
|
365
|
+
addExact(out, fileSet, 'manage.py', 'entry-point', 'Django project management CLI', 82);
|
|
366
|
+
addExact(out, fileSet, 'app.py', 'entry-point', 'Flask / web framework application', 80);
|
|
367
|
+
addExact(out, fileSet, 'wsgi.py', 'configuration', 'WSGI server entry point', 75);
|
|
368
|
+
addExact(out, fileSet, 'asgi.py', 'configuration', 'ASGI server entry point', 75);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ── Java / Spring Boot ────────────────────────────────────────────────────
|
|
372
|
+
if (pt.includes('java') || pt.includes('spring')) {
|
|
373
|
+
addExact(out, fileSet, 'pom.xml', 'manifest', 'Maven project descriptor and dependency manifest', 100);
|
|
374
|
+
addExact(out, fileSet, 'build.gradle', 'manifest', 'Gradle build script and dependency manifest', 100);
|
|
375
|
+
addExact(out, fileSet, 'build.gradle.kts', 'manifest', 'Gradle Kotlin DSL build script', 100);
|
|
376
|
+
addSuffix(out, flatFiles, 'Application.java', 'entry-point', 'Spring Boot application entry point', 82);
|
|
377
|
+
addSuffix(out, flatFiles, 'Application.kt', 'entry-point', 'Spring Boot Kotlin application entry point', 82);
|
|
378
|
+
// Main application properties
|
|
379
|
+
addExact(out, fileSet, 'src/main/resources/application.properties', 'configuration',
|
|
380
|
+
'Spring Boot application configuration', 78);
|
|
381
|
+
addExact(out, fileSet, 'src/main/resources/application.yml', 'configuration',
|
|
382
|
+
'Spring Boot application configuration', 78);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ── Go ────────────────────────────────────────────────────────────────────
|
|
386
|
+
if (pt === 'go') {
|
|
387
|
+
addExact(out, fileSet, 'go.mod', 'manifest', 'Go module definition and dependency manifest', 100);
|
|
388
|
+
addExact(out, fileSet, 'go.sum', 'manifest', 'Go module checksums', 92);
|
|
389
|
+
addExact(out, fileSet, 'main.go', 'entry-point','Main Go application entry point', 82);
|
|
390
|
+
addExact(out, fileSet, 'Makefile', 'utility', 'Build and task automation', 72);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ── Rust ──────────────────────────────────────────────────────────────────
|
|
394
|
+
if (pt.includes('rust')) {
|
|
395
|
+
addExact(out, fileSet, 'Cargo.toml', 'manifest', 'Rust package manifest and dependency configuration', 100);
|
|
396
|
+
addExact(out, fileSet, 'Cargo.lock', 'manifest', 'Rust dependency lockfile', 92);
|
|
397
|
+
addExact(out, fileSet, 'src/main.rs', 'entry-point','Binary crate entry point', 82);
|
|
398
|
+
addExact(out, fileSet, 'src/lib.rs', 'entry-point','Library crate root', 82);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ── PHP / Laravel / Composer ──────────────────────────────────────────────
|
|
402
|
+
if (pt.includes('php') || pt.includes('composer')) {
|
|
403
|
+
addExact(out, fileSet, 'composer.json', 'manifest', 'PHP Composer dependency manifest', 100);
|
|
404
|
+
addExact(out, fileSet, 'artisan', 'utility', 'Laravel CLI tool', 82);
|
|
405
|
+
addExact(out, fileSet, 'routes/web.php', 'configuration','Web route definitions', 78);
|
|
406
|
+
addExact(out, fileSet, 'routes/api.php', 'configuration','API route definitions', 78);
|
|
407
|
+
addExact(out, fileSet, 'public/index.php','entry-point', 'HTTP front controller (Laravel / PHP)', 76);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ── Ruby / Rails ──────────────────────────────────────────────────────────
|
|
411
|
+
if (pt.includes('ruby') || pt.includes('rails')) {
|
|
412
|
+
addExact(out, fileSet, 'Gemfile', 'manifest', 'Ruby gem dependency manifest', 100);
|
|
413
|
+
addExact(out, fileSet, 'config/routes.rb', 'configuration','Rails route definitions', 78);
|
|
414
|
+
addExact(out, fileSet, 'config/application.rb', 'configuration','Rails application configuration', 76);
|
|
415
|
+
addExact(out, fileSet, 'config/environment.rb', 'configuration','Rails environment bootstrap', 74);
|
|
416
|
+
addExact(out, fileSet, 'app/controllers/application_controller.rb', 'entry-point',
|
|
417
|
+
'Rails base application controller', 72);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ── Elixir / Phoenix ─────────────────────────────────────────────────────
|
|
421
|
+
if (pt.includes('elixir') || pt.includes('phoenix')) {
|
|
422
|
+
addExact(out, fileSet, 'mix.exs', 'manifest', 'Elixir Mix build and dependency manifest', 100);
|
|
423
|
+
addExact(out, fileSet, 'mix.lock', 'manifest', 'Elixir Mix dependency lockfile', 92);
|
|
424
|
+
addPrefix(out, flatFiles, 'lib/', 'entry-point', 'Elixir application module', 75);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return out;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ---------------------------------------------------------------------------
|
|
431
|
+
// Deduplication and sort
|
|
432
|
+
// ---------------------------------------------------------------------------
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Merge a list of raw candidates into a deduplicated, sorted result.
|
|
436
|
+
* When the same path appears with different priorities, the highest wins.
|
|
437
|
+
* Ties in priority are broken by ascending alphabetical path order.
|
|
438
|
+
*
|
|
439
|
+
* @param {ImportantFile[]} rawCandidates
|
|
440
|
+
* @returns {ImportantFile[]}
|
|
441
|
+
*/
|
|
442
|
+
function deduplicateAndSort(rawCandidates) {
|
|
443
|
+
/** @type {Map<string, ImportantFile>} */
|
|
444
|
+
const best = new Map();
|
|
445
|
+
|
|
446
|
+
for (const candidate of rawCandidates) {
|
|
447
|
+
const existing = best.get(candidate.path);
|
|
448
|
+
if (!existing || candidate.priority > existing.priority) {
|
|
449
|
+
best.set(candidate.path, candidate);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return Array.from(best.values()).sort((a, b) => {
|
|
454
|
+
if (b.priority !== a.priority) return b.priority - a.priority; // higher first
|
|
455
|
+
return a.path.localeCompare(b.path); // alpha on ties
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ---------------------------------------------------------------------------
|
|
460
|
+
// Public API
|
|
461
|
+
// ---------------------------------------------------------------------------
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Detect strategically important files in a scanned project.
|
|
465
|
+
*
|
|
466
|
+
* Uses only data already produced by scan() — no new filesystem operations.
|
|
467
|
+
*
|
|
468
|
+
* @param {Object} params
|
|
469
|
+
* @param {string[]} params.flatFiles - All relative POSIX file paths from the scanner
|
|
470
|
+
* @param {string} params.projectType - Detected project type (e.g. 'React', 'Go')
|
|
471
|
+
* @param {string[]} params.entryPoints - Detected entry point paths
|
|
472
|
+
* @param {string[]} params.configs - Detected configuration file paths
|
|
473
|
+
* @returns {{ importantFiles: ImportantFile[] }}
|
|
474
|
+
*/
|
|
475
|
+
export function detectImportantFiles({ flatFiles, projectType, entryPoints, configs }) {
|
|
476
|
+
const fileSet = new Set(flatFiles);
|
|
477
|
+
|
|
478
|
+
/** @type {ImportantFile[]} */
|
|
479
|
+
const raw = [];
|
|
480
|
+
|
|
481
|
+
// ── 1. Generic files (always check, any project type) ────────────────────
|
|
482
|
+
for (const candidate of GENERIC_CANDIDATES) {
|
|
483
|
+
if (fileSet.has(candidate.path)) {
|
|
484
|
+
raw.push({ ...candidate });
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
for (const candidate of ECOSYSTEM_MANIFESTS) {
|
|
489
|
+
if (fileSet.has(candidate.path)) {
|
|
490
|
+
raw.push({ ...candidate });
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// ── 2. Primary entry point ────────────────────────────────────────────────
|
|
495
|
+
if (entryPoints.length > 0) {
|
|
496
|
+
const primary = entryPoints[0];
|
|
497
|
+
if (fileSet.has(primary)) {
|
|
498
|
+
raw.push({
|
|
499
|
+
path: primary,
|
|
500
|
+
type: 'entry-point',
|
|
501
|
+
reason: 'Primary application entry point',
|
|
502
|
+
priority: 90,
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// ── 3. Project-specific candidates ───────────────────────────────────────
|
|
508
|
+
const projectCandidates = getProjectCandidates(projectType, flatFiles, fileSet);
|
|
509
|
+
for (const c of projectCandidates) {
|
|
510
|
+
raw.push(c);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// ── 4. Deduplicate, resolve priority conflicts, and sort ──────────────────
|
|
514
|
+
return { importantFiles: deduplicateAndSort(raw) };
|
|
515
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Toren — Package Manager Detector
|
|
3
|
+
*
|
|
4
|
+
* Identifies which package manager a project uses by inspecting the presence
|
|
5
|
+
* of well-known lockfiles in the repository root.
|
|
6
|
+
*
|
|
7
|
+
* Design contract:
|
|
8
|
+
* - Pure function. Accepts only the already-scanned flatFiles array.
|
|
9
|
+
* - No filesystem I/O. No recursive rescan. No external dependencies.
|
|
10
|
+
* - Only root-level files are considered. A lockfile nested under a
|
|
11
|
+
* subdirectory (e.g. examples/app/package-lock.json) does NOT influence
|
|
12
|
+
* the result — it belongs to that sub-project, not the repository root.
|
|
13
|
+
* - Deterministic: same input always produces the same output.
|
|
14
|
+
* - Handles ambiguity explicitly: when multiple different package managers
|
|
15
|
+
* are detected at root, packageManager is set to null and ambiguous is true.
|
|
16
|
+
*
|
|
17
|
+
* Supported lockfiles → package manager:
|
|
18
|
+
* package-lock.json → npm
|
|
19
|
+
* pnpm-lock.yaml → pnpm
|
|
20
|
+
* yarn.lock → yarn
|
|
21
|
+
* bun.lock → bun
|
|
22
|
+
* bun.lockb → bun
|
|
23
|
+
*
|
|
24
|
+
* @module detectors/package-manager-detector
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Lockfile registry
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Maps a root-level lockfile basename to its package manager name.
|
|
33
|
+
* Evaluation is case-sensitive — lockfile names are always lowercase on all
|
|
34
|
+
* supported platforms.
|
|
35
|
+
*
|
|
36
|
+
* @type {Map<string, string>}
|
|
37
|
+
*/
|
|
38
|
+
const LOCKFILE_MAP = new Map([
|
|
39
|
+
['package-lock.json', 'npm'],
|
|
40
|
+
['pnpm-lock.yaml', 'pnpm'],
|
|
41
|
+
['yarn.lock', 'yarn'],
|
|
42
|
+
['bun.lock', 'bun'],
|
|
43
|
+
['bun.lockb', 'bun'],
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Types (JSDoc — no TypeScript dependency required)
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @typedef {Object} PackageManagerResult
|
|
52
|
+
* @property {string|null} packageManager - Detected package manager name, or null
|
|
53
|
+
* when none or multiple are found.
|
|
54
|
+
* @property {string[]} packageManagers - Sorted list of all detected package managers.
|
|
55
|
+
* Empty when none detected.
|
|
56
|
+
* @property {boolean} ambiguous - True when multiple different package managers
|
|
57
|
+
* are detected at root simultaneously.
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Public API
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Detect the package manager used by a project from its root lockfiles.
|
|
66
|
+
*
|
|
67
|
+
* Only root-level entries in `flatFiles` are examined. A POSIX-style path
|
|
68
|
+
* with no '/' character is at the repository root (e.g. "yarn.lock").
|
|
69
|
+
* Paths that contain '/' are nested files (e.g. "packages/app/yarn.lock")
|
|
70
|
+
* and are intentionally ignored.
|
|
71
|
+
*
|
|
72
|
+
* @param {string[]} flatFiles - POSIX-relative file paths from the scanner.
|
|
73
|
+
* Must not be null or undefined.
|
|
74
|
+
* @returns {PackageManagerResult}
|
|
75
|
+
*/
|
|
76
|
+
export function detectPackageManager(flatFiles) {
|
|
77
|
+
const detected = new Set();
|
|
78
|
+
|
|
79
|
+
for (const file of flatFiles) {
|
|
80
|
+
// Root-level only: a POSIX relative path with no '/' is at the root.
|
|
81
|
+
// e.g. "package-lock.json" ✓ root
|
|
82
|
+
// "apps/web/yarn.lock" ✗ nested — skip
|
|
83
|
+
if (file.includes('/')) continue;
|
|
84
|
+
|
|
85
|
+
const pm = LOCKFILE_MAP.get(file);
|
|
86
|
+
if (pm !== undefined) {
|
|
87
|
+
detected.add(pm);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Sort for deterministic output — order of detection must not vary between runs.
|
|
92
|
+
const packageManagers = Array.from(detected).sort();
|
|
93
|
+
|
|
94
|
+
if (packageManagers.length === 0) {
|
|
95
|
+
return { packageManager: null, packageManagers: [], ambiguous: false };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (packageManagers.length === 1) {
|
|
99
|
+
return {
|
|
100
|
+
packageManager: packageManagers[0],
|
|
101
|
+
packageManagers: packageManagers,
|
|
102
|
+
ambiguous: false,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Multiple different package managers at root — do not guess.
|
|
107
|
+
return {
|
|
108
|
+
packageManager: null,
|
|
109
|
+
packageManagers: packageManagers,
|
|
110
|
+
ambiguous: true,
|
|
111
|
+
};
|
|
112
|
+
}
|