claudex-setup 1.6.0 → 1.7.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.
- package/CHANGELOG.md +16 -0
- package/README.md +90 -7
- package/bin/cli.js +214 -11
- package/package.json +1 -1
- package/src/activity.js +60 -0
- package/src/analyze.js +397 -0
- package/src/audit.js +25 -20
- package/src/benchmark.js +176 -0
- package/src/governance.js +192 -0
- package/src/index.js +13 -1
- package/src/interactive.js +2 -2
- package/src/plans.js +355 -0
- package/src/setup.js +154 -52
- package/src/techniques.js +8 -0
package/src/setup.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Setup engine - applies recommended Claude Code configuration to a project.
|
|
3
|
-
*
|
|
3
|
+
* v1.7.0 - Starter-safe setup engine with reusable planning primitives.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const fs = require('fs');
|
|
@@ -248,9 +248,9 @@ function detectDependencies(ctx) {
|
|
|
248
248
|
// Helper: detect main directories
|
|
249
249
|
// ============================================================
|
|
250
250
|
function detectMainDirs(ctx) {
|
|
251
|
-
const candidates = ['src', 'lib', 'app', 'pages', 'components', 'api', 'routes', 'utils', 'helpers', 'services', 'models', 'controllers', 'views', 'public', 'assets', 'config', 'tests', 'test', '__tests__', 'spec', 'scripts', 'prisma', 'db', 'middleware', 'hooks'];
|
|
251
|
+
const candidates = ['src', 'lib', 'app', 'pages', 'components', 'api', 'routes', 'utils', 'helpers', 'services', 'models', 'controllers', 'views', 'public', 'assets', 'config', 'tests', 'test', '__tests__', 'spec', 'scripts', 'prisma', 'db', 'middleware', 'hooks', 'agents', 'chains', 'workers', 'jobs', 'dags', 'macros', 'migrations'];
|
|
252
252
|
// Also check inside src/ for nested structure (common in Next.js, React)
|
|
253
|
-
const srcNested = ['src/components', 'src/app', 'src/pages', 'src/api', 'src/lib', 'src/hooks', 'src/utils', 'src/services', 'src/models', 'src/middleware', 'src/app/api', 'app/api'];
|
|
253
|
+
const srcNested = ['src/components', 'src/app', 'src/pages', 'src/api', 'src/lib', 'src/hooks', 'src/utils', 'src/services', 'src/models', 'src/middleware', 'src/app/api', 'app/api', 'src/agents', 'src/chains', 'src/workers', 'src/jobs', 'models/staging', 'models/marts'];
|
|
254
254
|
const found = [];
|
|
255
255
|
const seenNames = new Set();
|
|
256
256
|
|
|
@@ -267,6 +267,55 @@ function detectMainDirs(ctx) {
|
|
|
267
267
|
return found;
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
+
function escapeRegex(value) {
|
|
271
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function extractTomlSection(content, sectionName) {
|
|
275
|
+
const pattern = new RegExp(`\\[${escapeRegex(sectionName)}\\]([\\s\\S]*?)(?:\\n\\s*\\[|$)`);
|
|
276
|
+
const match = content.match(pattern);
|
|
277
|
+
return match ? match[1] : null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function extractTomlValue(sectionContent, key) {
|
|
281
|
+
if (!sectionContent) return null;
|
|
282
|
+
const pattern = new RegExp(`^\\s*${escapeRegex(key)}\\s*=\\s*["']([^"']+)["']`, 'm');
|
|
283
|
+
const match = sectionContent.match(pattern);
|
|
284
|
+
return match ? match[1].trim() : null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function detectProjectMetadata(ctx) {
|
|
288
|
+
const pkg = ctx.jsonFile('package.json');
|
|
289
|
+
if (pkg && (pkg.name || pkg.description)) {
|
|
290
|
+
return {
|
|
291
|
+
name: pkg.name || path.basename(ctx.dir),
|
|
292
|
+
description: pkg.description || '',
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const pyproject = ctx.fileContent('pyproject.toml') || '';
|
|
297
|
+
if (pyproject) {
|
|
298
|
+
const projectSection = extractTomlSection(pyproject, 'project');
|
|
299
|
+
const poetrySection = extractTomlSection(pyproject, 'tool.poetry');
|
|
300
|
+
const name = extractTomlValue(projectSection, 'name') ||
|
|
301
|
+
extractTomlValue(poetrySection, 'name');
|
|
302
|
+
const description = extractTomlValue(projectSection, 'description') ||
|
|
303
|
+
extractTomlValue(poetrySection, 'description');
|
|
304
|
+
|
|
305
|
+
if (name || description) {
|
|
306
|
+
return {
|
|
307
|
+
name: name || path.basename(ctx.dir),
|
|
308
|
+
description: description || '',
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
name: path.basename(ctx.dir),
|
|
315
|
+
description: '',
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
270
319
|
// ============================================================
|
|
271
320
|
// Helper: generate Mermaid diagram from directory structure
|
|
272
321
|
// ============================================================
|
|
@@ -295,6 +344,11 @@ function generateMermaid(dirs, stacks) {
|
|
|
295
344
|
const hasSrcComponents = dirNames.includes('src/components') || dirNames.includes('components');
|
|
296
345
|
const hasSrcHooks = dirNames.includes('src/hooks') || dirNames.includes('hooks');
|
|
297
346
|
const hasSrcLib = dirNames.includes('src/lib') || dirNames.includes('lib');
|
|
347
|
+
const hasSrcNode = dirNames.includes('src');
|
|
348
|
+
const hasAgents = dirNames.includes('src/agents') || dirNames.includes('agents');
|
|
349
|
+
const hasChains = dirNames.includes('src/chains') || dirNames.includes('chains');
|
|
350
|
+
const hasWorkers = dirNames.includes('src/workers') || dirNames.includes('workers') || dirNames.includes('jobs');
|
|
351
|
+
const hasPipelines = dirNames.includes('dags') || dirNames.includes('macros');
|
|
298
352
|
|
|
299
353
|
// Smart entry point based on framework
|
|
300
354
|
const isNextJs = stackKeys.includes('nextjs');
|
|
@@ -312,6 +366,7 @@ function generateMermaid(dirs, stacks) {
|
|
|
312
366
|
}
|
|
313
367
|
|
|
314
368
|
const root = ids['Next.js'] || ids['Django'] || ids['FastAPI'] || ids['Entry Point'];
|
|
369
|
+
const pickNodeId = (...labels) => labels.map(label => ids[label]).find(Boolean) || root;
|
|
315
370
|
|
|
316
371
|
// Detect layers
|
|
317
372
|
if (hasAppRouter || hasPages) {
|
|
@@ -340,9 +395,9 @@ function generateMermaid(dirs, stacks) {
|
|
|
340
395
|
|
|
341
396
|
if (hasSrcLib) {
|
|
342
397
|
nodes.push(addNode('lib/', 'default'));
|
|
343
|
-
const parent =
|
|
398
|
+
const parent = pickNodeId('API Routes', 'Hooks', 'Components');
|
|
344
399
|
edges.push(` ${parent} --> ${ids['lib/']}`);
|
|
345
|
-
} else if (
|
|
400
|
+
} else if (hasSrcNode && !hasAppRouter && !hasPages) {
|
|
346
401
|
nodes.push(addNode('src/', 'default'));
|
|
347
402
|
edges.push(` ${root} --> ${ids['src/']}`);
|
|
348
403
|
}
|
|
@@ -350,19 +405,19 @@ function generateMermaid(dirs, stacks) {
|
|
|
350
405
|
if (dirNames.includes('api') || dirNames.includes('routes') || dirNames.includes('controllers')) {
|
|
351
406
|
const label = dirNames.includes('api') ? 'API Layer' : 'Routes';
|
|
352
407
|
nodes.push(addNode(label, 'default'));
|
|
353
|
-
const parent =
|
|
408
|
+
const parent = pickNodeId('src/', 'App Router', 'Pages');
|
|
354
409
|
edges.push(` ${parent} --> ${ids[label]}`);
|
|
355
410
|
}
|
|
356
411
|
|
|
357
412
|
if (dirNames.includes('services')) {
|
|
358
413
|
nodes.push(addNode('Services', 'default'));
|
|
359
|
-
const parent =
|
|
414
|
+
const parent = pickNodeId('API Layer', 'Routes', 'src/', 'App Router', 'Pages');
|
|
360
415
|
edges.push(` ${parent} --> ${ids['Services']}`);
|
|
361
416
|
}
|
|
362
417
|
|
|
363
418
|
if (dirNames.includes('models') || dirNames.includes('prisma') || dirNames.includes('db')) {
|
|
364
419
|
nodes.push(addNode('Data Layer', 'default'));
|
|
365
|
-
const parent =
|
|
420
|
+
const parent = pickNodeId('Services', 'API Layer', 'Routes', 'src/', 'App Router', 'Pages');
|
|
366
421
|
edges.push(` ${parent} --> ${ids['Data Layer']}`);
|
|
367
422
|
nodes.push(addNode('Database', 'db'));
|
|
368
423
|
edges.push(` ${ids['Data Layer']} --> ${ids['Database']}`);
|
|
@@ -370,19 +425,43 @@ function generateMermaid(dirs, stacks) {
|
|
|
370
425
|
|
|
371
426
|
if (dirNames.includes('utils') || dirNames.includes('helpers')) {
|
|
372
427
|
nodes.push(addNode('Utils', 'default'));
|
|
373
|
-
const parent =
|
|
428
|
+
const parent = pickNodeId('src/', 'Services', 'lib/', 'Components');
|
|
374
429
|
edges.push(` ${parent} --> ${ids['Utils']}`);
|
|
375
430
|
}
|
|
376
431
|
|
|
377
432
|
if (dirNames.includes('middleware')) {
|
|
378
433
|
nodes.push(addNode('Middleware', 'default'));
|
|
379
|
-
const parent =
|
|
434
|
+
const parent = pickNodeId('API Layer', 'Routes', 'App Router', 'Pages');
|
|
380
435
|
edges.push(` ${parent} --> ${ids['Middleware']}`);
|
|
381
436
|
}
|
|
382
437
|
|
|
438
|
+
if (hasChains) {
|
|
439
|
+
nodes.push(addNode('Chains', 'default'));
|
|
440
|
+
const parent = pickNodeId('Services', 'src/', 'lib/', 'API Layer');
|
|
441
|
+
edges.push(` ${parent} --> ${ids['Chains']}`);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (hasAgents) {
|
|
445
|
+
nodes.push(addNode('Agents', 'default'));
|
|
446
|
+
const parent = pickNodeId('Chains', 'Services', 'src/', 'lib/');
|
|
447
|
+
edges.push(` ${parent} --> ${ids['Agents']}`);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (hasWorkers) {
|
|
451
|
+
nodes.push(addNode('Workers', 'default'));
|
|
452
|
+
const parent = pickNodeId('Services', 'API Layer', 'src/');
|
|
453
|
+
edges.push(` ${parent} --> ${ids['Workers']}`);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (hasPipelines) {
|
|
457
|
+
nodes.push(addNode('Pipelines', 'default'));
|
|
458
|
+
const parent = pickNodeId('Services', 'Data Layer', 'src/');
|
|
459
|
+
edges.push(` ${parent} --> ${ids['Pipelines']}`);
|
|
460
|
+
}
|
|
461
|
+
|
|
383
462
|
if (dirNames.includes('tests') || dirNames.includes('test') || dirNames.includes('__tests__') || dirNames.includes('spec')) {
|
|
384
463
|
nodes.push(addNode('Tests', 'round'));
|
|
385
|
-
const parent =
|
|
464
|
+
const parent = pickNodeId('src/', 'App Router', 'Pages', 'Services', 'Components');
|
|
386
465
|
edges.push(` ${ids['Tests']} -.-> ${parent}`);
|
|
387
466
|
}
|
|
388
467
|
|
|
@@ -416,13 +495,9 @@ function getFrameworkInstructions(stacks) {
|
|
|
416
495
|
- Use next/image for images, next/link for navigation
|
|
417
496
|
- API routes go in app/api/ (App Router) or pages/api/ (Pages Router)
|
|
418
497
|
- Use loading.tsx, error.tsx, and not-found.tsx for route-level UX
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
-
|
|
422
|
-
- Use Server Actions for mutations. Validate with Zod, call revalidatePath after writes
|
|
423
|
-
- Route handlers in app/api/ export named functions: GET, POST, PUT, DELETE
|
|
424
|
-
- Use loading.tsx, error.tsx, not-found.tsx for route-level UI states
|
|
425
|
-
- Middleware in middleware.ts for auth checks, redirects, headers`);
|
|
498
|
+
- If app/ exists, use Server Actions for mutations, validate with Zod, and call revalidatePath after writes
|
|
499
|
+
- Route handlers in app/api/ should export named functions: GET, POST, PUT, DELETE
|
|
500
|
+
- Middleware in middleware.ts should handle auth checks, redirects, and headers`);
|
|
426
501
|
} else if (stackKeys.includes('react')) {
|
|
427
502
|
sections.push(`### React
|
|
428
503
|
- Use functional components with hooks exclusively
|
|
@@ -655,10 +730,10 @@ ${depGuidelines.join('\n')}
|
|
|
655
730
|
}
|
|
656
731
|
verificationSteps.push(`${verificationSteps.length + 1}. Changes match the requested scope (no gold-plating)`);
|
|
657
732
|
|
|
658
|
-
// --- Read package.json
|
|
659
|
-
const
|
|
660
|
-
const projectName =
|
|
661
|
-
const projectDesc =
|
|
733
|
+
// --- Read project metadata from package.json or pyproject.toml ---
|
|
734
|
+
const projectMeta = detectProjectMetadata(ctx);
|
|
735
|
+
const projectName = projectMeta.name;
|
|
736
|
+
const projectDesc = projectMeta.description ? ` — ${projectMeta.description}` : '';
|
|
662
737
|
|
|
663
738
|
// --- Assemble the final CLAUDE.md ---
|
|
664
739
|
return `# ${projectName}${projectDesc}
|
|
@@ -674,11 +749,10 @@ ${stackSection}${tsSection}${depSection}
|
|
|
674
749
|
${buildSection}
|
|
675
750
|
\`\`\`
|
|
676
751
|
|
|
677
|
-
##
|
|
678
|
-
-
|
|
679
|
-
-
|
|
680
|
-
- Keep
|
|
681
|
-
- Use descriptive variable names; avoid abbreviations
|
|
752
|
+
## Working Notes
|
|
753
|
+
- You are a careful engineer working inside this repository. Preserve its existing architecture and naming patterns unless the task requires a change
|
|
754
|
+
- Prefer extending existing modules over creating parallel abstractions
|
|
755
|
+
- Keep changes scoped to the requested task and verify them before marking work complete
|
|
682
756
|
|
|
683
757
|
<constraints>
|
|
684
758
|
- Never commit secrets, API keys, or .env files
|
|
@@ -700,12 +774,6 @@ ${verificationSteps.join('\n')}
|
|
|
700
774
|
- If a session gets too long, start fresh with /clear
|
|
701
775
|
- Use subagents for research tasks to keep main context clean
|
|
702
776
|
|
|
703
|
-
## Workflow
|
|
704
|
-
- Verify changes with tests before committing
|
|
705
|
-
- Use descriptive commit messages (why, not what)
|
|
706
|
-
- Create focused PRs — one concern per PR
|
|
707
|
-
- Document non-obvious decisions in code comments
|
|
708
|
-
|
|
709
777
|
---
|
|
710
778
|
*Generated by [claudex-setup](https://github.com/DnaFin/claudex-setup) v${require('../package.json').version} on ${new Date().toISOString().split('T')[0]}. Customize this file for your project — a hand-crafted CLAUDE.md will always be better than a generated one.*
|
|
711
779
|
`;
|
|
@@ -992,15 +1060,24 @@ graph TD
|
|
|
992
1060
|
async function setup(options) {
|
|
993
1061
|
const ctx = new ProjectContext(options.dir);
|
|
994
1062
|
const stacks = ctx.detectStacks(STACKS);
|
|
1063
|
+
const silent = options.silent === true;
|
|
1064
|
+
const writtenFiles = [];
|
|
1065
|
+
const preservedFiles = [];
|
|
1066
|
+
|
|
1067
|
+
function log(message = '') {
|
|
1068
|
+
if (!silent) {
|
|
1069
|
+
console.log(message);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
995
1072
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1073
|
+
log('');
|
|
1074
|
+
log('\x1b[1m claudex-setup\x1b[0m');
|
|
1075
|
+
log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
|
|
999
1076
|
|
|
1000
1077
|
if (stacks.length > 0) {
|
|
1001
|
-
|
|
1078
|
+
log(`\x1b[36m Detected: ${stacks.map(s => s.label).join(', ')}\x1b[0m`);
|
|
1002
1079
|
}
|
|
1003
|
-
|
|
1080
|
+
log('');
|
|
1004
1081
|
|
|
1005
1082
|
let created = 0;
|
|
1006
1083
|
let skipped = 0;
|
|
@@ -1038,10 +1115,12 @@ async function setup(options) {
|
|
|
1038
1115
|
|
|
1039
1116
|
if (!fs.existsSync(fullPath)) {
|
|
1040
1117
|
fs.writeFileSync(fullPath, result, 'utf8');
|
|
1041
|
-
|
|
1118
|
+
writtenFiles.push(filePath);
|
|
1119
|
+
log(` \x1b[32m✅\x1b[0m Created ${filePath}`);
|
|
1042
1120
|
created++;
|
|
1043
1121
|
} else {
|
|
1044
|
-
|
|
1122
|
+
preservedFiles.push(filePath);
|
|
1123
|
+
log(` \x1b[2m⏭️ Skipped ${filePath} (already exists — your version is kept)\x1b[0m`);
|
|
1045
1124
|
skipped++;
|
|
1046
1125
|
}
|
|
1047
1126
|
} else if (typeof result === 'object') {
|
|
@@ -1068,9 +1147,11 @@ async function setup(options) {
|
|
|
1068
1147
|
}
|
|
1069
1148
|
if (!fs.existsSync(filePath)) {
|
|
1070
1149
|
fs.writeFileSync(filePath, content, 'utf8');
|
|
1071
|
-
|
|
1150
|
+
writtenFiles.push(path.relative(options.dir, filePath));
|
|
1151
|
+
log(` \x1b[32m✅\x1b[0m Created ${path.relative(options.dir, filePath)}`);
|
|
1072
1152
|
created++;
|
|
1073
1153
|
} else {
|
|
1154
|
+
preservedFiles.push(path.relative(options.dir, filePath));
|
|
1074
1155
|
skipped++;
|
|
1075
1156
|
}
|
|
1076
1157
|
}
|
|
@@ -1084,6 +1165,18 @@ async function setup(options) {
|
|
|
1084
1165
|
const hookFiles = fs.readdirSync(hooksDir).filter(f => f.endsWith('.sh'));
|
|
1085
1166
|
if (hookFiles.length > 0) {
|
|
1086
1167
|
const settings = {
|
|
1168
|
+
permissions: {
|
|
1169
|
+
defaultMode: "acceptEdits",
|
|
1170
|
+
deny: [
|
|
1171
|
+
"Read(./.env*)",
|
|
1172
|
+
"Read(./secrets/**)",
|
|
1173
|
+
"Bash(rm -rf *)",
|
|
1174
|
+
"Bash(git reset --hard *)",
|
|
1175
|
+
"Bash(git checkout -- *)",
|
|
1176
|
+
"Bash(git clean *)",
|
|
1177
|
+
"Bash(git push --force *)"
|
|
1178
|
+
]
|
|
1179
|
+
},
|
|
1087
1180
|
hooks: {
|
|
1088
1181
|
PostToolUse: [{
|
|
1089
1182
|
matcher: "Write|Edit",
|
|
@@ -1107,26 +1200,35 @@ async function setup(options) {
|
|
|
1107
1200
|
}];
|
|
1108
1201
|
}
|
|
1109
1202
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
|
|
1110
|
-
|
|
1203
|
+
writtenFiles.push('.claude/settings.json');
|
|
1204
|
+
log(` \x1b[32m✅\x1b[0m Created .claude/settings.json (hooks registered)`);
|
|
1111
1205
|
created++;
|
|
1112
1206
|
}
|
|
1113
1207
|
}
|
|
1114
1208
|
|
|
1115
|
-
|
|
1209
|
+
log('');
|
|
1116
1210
|
if (created === 0 && skipped > 0) {
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1211
|
+
log(' \x1b[32m✅\x1b[0m Your project is already well configured!');
|
|
1212
|
+
log(` \x1b[2m ${skipped} files already exist and were preserved.\x1b[0m`);
|
|
1213
|
+
log(' \x1b[2m We never overwrite your existing config — your setup is kept.\x1b[0m');
|
|
1120
1214
|
} else if (created > 0) {
|
|
1121
|
-
|
|
1215
|
+
log(` \x1b[1m${created} files created.\x1b[0m`);
|
|
1122
1216
|
if (skipped > 0) {
|
|
1123
|
-
|
|
1217
|
+
log(` \x1b[2m${skipped} existing files preserved (not overwritten).\x1b[0m`);
|
|
1124
1218
|
}
|
|
1125
1219
|
}
|
|
1126
1220
|
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1221
|
+
log('');
|
|
1222
|
+
log(' Run \x1b[1mnpx claudex-setup audit\x1b[0m to check your score.');
|
|
1223
|
+
log('');
|
|
1224
|
+
|
|
1225
|
+
return {
|
|
1226
|
+
created,
|
|
1227
|
+
skipped,
|
|
1228
|
+
writtenFiles,
|
|
1229
|
+
preservedFiles,
|
|
1230
|
+
stacks,
|
|
1231
|
+
};
|
|
1130
1232
|
}
|
|
1131
1233
|
|
|
1132
|
-
module.exports = { setup };
|
|
1234
|
+
module.exports = { setup, TEMPLATES };
|
package/src/techniques.js
CHANGED
|
@@ -170,6 +170,10 @@ const TECHNIQUES = {
|
|
|
170
170
|
id: 91701,
|
|
171
171
|
name: '.gitignore blocks node_modules',
|
|
172
172
|
check: (ctx) => {
|
|
173
|
+
const hasNodeSignals = ctx.files.includes('package.json') ||
|
|
174
|
+
ctx.files.includes('tsconfig.json') ||
|
|
175
|
+
ctx.files.some(f => /package-lock\.json|pnpm-lock\.yaml|yarn\.lock|next\.config|vite\.config/i.test(f));
|
|
176
|
+
if (!hasNodeSignals) return null;
|
|
173
177
|
const gitignore = ctx.fileContent('.gitignore') || '';
|
|
174
178
|
return gitignore.includes('node_modules');
|
|
175
179
|
},
|
|
@@ -603,6 +607,10 @@ const TECHNIQUES = {
|
|
|
603
607
|
id: 5002,
|
|
604
608
|
name: 'Node version pinned',
|
|
605
609
|
check: (ctx) => {
|
|
610
|
+
const hasNodeSignals = ctx.files.includes('package.json') ||
|
|
611
|
+
ctx.files.includes('tsconfig.json') ||
|
|
612
|
+
ctx.files.some(f => /package-lock\.json|pnpm-lock\.yaml|yarn\.lock|next\.config|vite\.config/i.test(f));
|
|
613
|
+
if (!hasNodeSignals) return null;
|
|
606
614
|
if (ctx.files.includes('.nvmrc') || ctx.files.includes('.node-version')) return true;
|
|
607
615
|
const pkg = ctx.jsonFile('package.json');
|
|
608
616
|
return pkg && pkg.engines && pkg.engines.node;
|