claudex-setup 1.5.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/src/setup.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Setup engine - applies recommended Claude Code configuration to a project.
3
- * v0.3.0 - Smart CLAUDE.md generation with project analysis.
3
+ * v1.7.0 - Starter-safe setup engine with reusable planning primitives.
4
4
  */
5
5
 
6
6
  const fs = require('fs');
@@ -95,6 +95,17 @@ function detectDependencies(ctx) {
95
95
  guidelines.push('- Use Playwright for E2E tests. Keep tests in tests/ or e2e/');
96
96
  }
97
97
 
98
+ // Testing tools
99
+ if (allDeps['msw']) {
100
+ guidelines.push('- Use MSW (Mock Service Worker) for API mocking in tests. Define handlers in __mocks__/');
101
+ }
102
+ if (allDeps['@testing-library/react']) {
103
+ guidelines.push('- Use Testing Library for component tests. Prefer userEvent over fireEvent, query by role/label');
104
+ }
105
+ if (allDeps['@vitest/coverage-v8'] || allDeps['@vitest/coverage-istanbul']) {
106
+ guidelines.push('- Coverage configured. Maintain coverage thresholds. Check reports before merging');
107
+ }
108
+
98
109
  // tRPC
99
110
  if (allDeps['@trpc/server'] || allDeps['@trpc/client']) {
100
111
  guidelines.push('- Use tRPC for type-safe API calls. Define routers in server, use client hooks in components');
@@ -213,6 +224,23 @@ function detectDependencies(ctx) {
213
224
  guidelines.push('- Lambda handlers: keep cold start fast, use layers for deps, set appropriate memory/timeout');
214
225
  }
215
226
 
227
+ // Deprecated dependency warnings
228
+ if (allDeps['moment']) {
229
+ guidelines.push('- ⚠️ moment.js is deprecated and heavy (330KB). Migrate to date-fns or dayjs');
230
+ }
231
+ if (allDeps['request']) {
232
+ guidelines.push('- ⚠️ request is deprecated. Use fetch (native) or axios instead');
233
+ }
234
+ if (allDeps['lodash'] && !allDeps['lodash-es']) {
235
+ guidelines.push('- Consider replacing lodash with native JS methods or lodash-es for tree-shaking');
236
+ }
237
+ if (allDeps['node-sass']) {
238
+ guidelines.push('- ⚠️ node-sass is deprecated. Migrate to sass (dart-sass)');
239
+ }
240
+ if (allDeps['tslint']) {
241
+ guidelines.push('- ⚠️ TSLint is deprecated. Migrate to ESLint with @typescript-eslint');
242
+ }
243
+
216
244
  return guidelines;
217
245
  }
218
246
 
@@ -220,9 +248,9 @@ function detectDependencies(ctx) {
220
248
  // Helper: detect main directories
221
249
  // ============================================================
222
250
  function detectMainDirs(ctx) {
223
- 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'];
224
252
  // Also check inside src/ for nested structure (common in Next.js, React)
225
- 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'];
226
254
  const found = [];
227
255
  const seenNames = new Set();
228
256
 
@@ -239,6 +267,55 @@ function detectMainDirs(ctx) {
239
267
  return found;
240
268
  }
241
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
+
242
319
  // ============================================================
243
320
  // Helper: generate Mermaid diagram from directory structure
244
321
  // ============================================================
@@ -267,6 +344,11 @@ function generateMermaid(dirs, stacks) {
267
344
  const hasSrcComponents = dirNames.includes('src/components') || dirNames.includes('components');
268
345
  const hasSrcHooks = dirNames.includes('src/hooks') || dirNames.includes('hooks');
269
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');
270
352
 
271
353
  // Smart entry point based on framework
272
354
  const isNextJs = stackKeys.includes('nextjs');
@@ -284,6 +366,7 @@ function generateMermaid(dirs, stacks) {
284
366
  }
285
367
 
286
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;
287
370
 
288
371
  // Detect layers
289
372
  if (hasAppRouter || hasPages) {
@@ -312,9 +395,9 @@ function generateMermaid(dirs, stacks) {
312
395
 
313
396
  if (hasSrcLib) {
314
397
  nodes.push(addNode('lib/', 'default'));
315
- const parent = ids['API Routes'] || ids['Hooks'] || ids['Components'] || root;
398
+ const parent = pickNodeId('API Routes', 'Hooks', 'Components');
316
399
  edges.push(` ${parent} --> ${ids['lib/']}`);
317
- } else if (dirNames.includes('src') && !hasAppRouter && !hasPages) {
400
+ } else if (hasSrcNode && !hasAppRouter && !hasPages) {
318
401
  nodes.push(addNode('src/', 'default'));
319
402
  edges.push(` ${root} --> ${ids['src/']}`);
320
403
  }
@@ -322,19 +405,19 @@ function generateMermaid(dirs, stacks) {
322
405
  if (dirNames.includes('api') || dirNames.includes('routes') || dirNames.includes('controllers')) {
323
406
  const label = dirNames.includes('api') ? 'API Layer' : 'Routes';
324
407
  nodes.push(addNode(label, 'default'));
325
- const parent = ids['src/'] || ids['Entry Point'];
408
+ const parent = pickNodeId('src/', 'App Router', 'Pages');
326
409
  edges.push(` ${parent} --> ${ids[label]}`);
327
410
  }
328
411
 
329
412
  if (dirNames.includes('services')) {
330
413
  nodes.push(addNode('Services', 'default'));
331
- const parent = ids['API Layer'] || ids['Routes'] || ids['src/'] || ids['Entry Point'];
414
+ const parent = pickNodeId('API Layer', 'Routes', 'src/', 'App Router', 'Pages');
332
415
  edges.push(` ${parent} --> ${ids['Services']}`);
333
416
  }
334
417
 
335
418
  if (dirNames.includes('models') || dirNames.includes('prisma') || dirNames.includes('db')) {
336
419
  nodes.push(addNode('Data Layer', 'default'));
337
- const parent = ids['Services'] || ids['API Layer'] || ids['Routes'] || ids['src/'] || ids['Entry Point'];
420
+ const parent = pickNodeId('Services', 'API Layer', 'Routes', 'src/', 'App Router', 'Pages');
338
421
  edges.push(` ${parent} --> ${ids['Data Layer']}`);
339
422
  nodes.push(addNode('Database', 'db'));
340
423
  edges.push(` ${ids['Data Layer']} --> ${ids['Database']}`);
@@ -342,19 +425,43 @@ function generateMermaid(dirs, stacks) {
342
425
 
343
426
  if (dirNames.includes('utils') || dirNames.includes('helpers')) {
344
427
  nodes.push(addNode('Utils', 'default'));
345
- const parent = ids['src/'] || ids['Services'] || ids['Entry Point'];
428
+ const parent = pickNodeId('src/', 'Services', 'lib/', 'Components');
346
429
  edges.push(` ${parent} --> ${ids['Utils']}`);
347
430
  }
348
431
 
349
432
  if (dirNames.includes('middleware')) {
350
433
  nodes.push(addNode('Middleware', 'default'));
351
- const parent = ids['API Layer'] || ids['Routes'] || ids['Entry Point'];
434
+ const parent = pickNodeId('API Layer', 'Routes', 'App Router', 'Pages');
352
435
  edges.push(` ${parent} --> ${ids['Middleware']}`);
353
436
  }
354
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
+
355
462
  if (dirNames.includes('tests') || dirNames.includes('test') || dirNames.includes('__tests__') || dirNames.includes('spec')) {
356
463
  nodes.push(addNode('Tests', 'round'));
357
- const parent = ids['src/'] || ids['Entry Point'];
464
+ const parent = pickNodeId('src/', 'App Router', 'Pages', 'Services', 'Components');
358
465
  edges.push(` ${ids['Tests']} -.-> ${parent}`);
359
466
  }
360
467
 
@@ -388,13 +495,9 @@ function getFrameworkInstructions(stacks) {
388
495
  - Use next/image for images, next/link for navigation
389
496
  - API routes go in app/api/ (App Router) or pages/api/ (Pages Router)
390
497
  - Use loading.tsx, error.tsx, and not-found.tsx for route-level UX
391
-
392
- ### Next.js App Router
393
- - Default to Server Components. Add 'use client' only when needed (hooks, events, browser APIs)
394
- - Use Server Actions for mutations. Validate with Zod, call revalidatePath after writes
395
- - Route handlers in app/api/ export named functions: GET, POST, PUT, DELETE
396
- - Use loading.tsx, error.tsx, not-found.tsx for route-level UI states
397
- - 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`);
398
501
  } else if (stackKeys.includes('react')) {
399
502
  sections.push(`### React
400
503
  - Use functional components with hooks exclusively
@@ -473,6 +576,22 @@ function getFrameworkInstructions(stacks) {
473
576
  - Organize: cmd/ for entry points, internal/ for private packages, pkg/ for public`);
474
577
  }
475
578
 
579
+ if (stackKeys.includes('cpp')) {
580
+ sections.push(`### C++
581
+ - Follow project coding standards (check .clang-format if present)
582
+ - Use smart pointers (unique_ptr, shared_ptr) over raw pointers
583
+ - Run clang-tidy for static analysis
584
+ - Prefer const references for function parameters
585
+ - Use CMake targets, not raw compiler flags`);
586
+ }
587
+
588
+ if (stackKeys.includes('bazel')) {
589
+ sections.push(`### Bazel
590
+ - Define BUILD files per package. Keep targets focused
591
+ - Use visibility carefully — prefer package-private
592
+ - Run buildifier for formatting`);
593
+ }
594
+
476
595
  if (stackKeys.includes('terraform')) {
477
596
  sections.push(`### Terraform
478
597
  - Use modules for reusable infrastructure components
@@ -611,10 +730,10 @@ ${depGuidelines.join('\n')}
611
730
  }
612
731
  verificationSteps.push(`${verificationSteps.length + 1}. Changes match the requested scope (no gold-plating)`);
613
732
 
614
- // --- Read package.json for project name/description ---
615
- const pkg = ctx.jsonFile('package.json');
616
- const projectName = (pkg && pkg.name) ? pkg.name : path.basename(ctx.dir);
617
- const projectDesc = (pkg && pkg.description) ? ` — ${pkg.description}` : '';
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}` : '';
618
737
 
619
738
  // --- Assemble the final CLAUDE.md ---
620
739
  return `# ${projectName}${projectDesc}
@@ -630,11 +749,10 @@ ${stackSection}${tsSection}${depSection}
630
749
  ${buildSection}
631
750
  \`\`\`
632
751
 
633
- ## Code Style
634
- - Follow existing patterns in the codebase
635
- - Write tests for new features
636
- - Keep functions small and focused (< 50 lines)
637
- - 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
638
756
 
639
757
  <constraints>
640
758
  - Never commit secrets, API keys, or .env files
@@ -656,12 +774,6 @@ ${verificationSteps.join('\n')}
656
774
  - If a session gets too long, start fresh with /clear
657
775
  - Use subagents for research tasks to keep main context clean
658
776
 
659
- ## Workflow
660
- - Verify changes with tests before committing
661
- - Use descriptive commit messages (why, not what)
662
- - Create focused PRs — one concern per PR
663
- - Document non-obvious decisions in code comments
664
-
665
777
  ---
666
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.*
667
779
  `;
@@ -948,15 +1060,24 @@ graph TD
948
1060
  async function setup(options) {
949
1061
  const ctx = new ProjectContext(options.dir);
950
1062
  const stacks = ctx.detectStacks(STACKS);
1063
+ const silent = options.silent === true;
1064
+ const writtenFiles = [];
1065
+ const preservedFiles = [];
951
1066
 
952
- console.log('');
953
- console.log('\x1b[1m claudex-setup\x1b[0m');
954
- console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
1067
+ function log(message = '') {
1068
+ if (!silent) {
1069
+ console.log(message);
1070
+ }
1071
+ }
1072
+
1073
+ log('');
1074
+ log('\x1b[1m claudex-setup\x1b[0m');
1075
+ log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
955
1076
 
956
1077
  if (stacks.length > 0) {
957
- console.log(`\x1b[36m Detected: ${stacks.map(s => s.label).join(', ')}\x1b[0m`);
1078
+ log(`\x1b[36m Detected: ${stacks.map(s => s.label).join(', ')}\x1b[0m`);
958
1079
  }
959
- console.log('');
1080
+ log('');
960
1081
 
961
1082
  let created = 0;
962
1083
  let skipped = 0;
@@ -994,10 +1115,12 @@ async function setup(options) {
994
1115
 
995
1116
  if (!fs.existsSync(fullPath)) {
996
1117
  fs.writeFileSync(fullPath, result, 'utf8');
997
- console.log(` \x1b[32m✅\x1b[0m Created ${filePath}`);
1118
+ writtenFiles.push(filePath);
1119
+ log(` \x1b[32m✅\x1b[0m Created ${filePath}`);
998
1120
  created++;
999
1121
  } else {
1000
- console.log(` \x1b[2m⏭️ Skipped ${filePath} (already exists — your version is kept)\x1b[0m`);
1122
+ preservedFiles.push(filePath);
1123
+ log(` \x1b[2m⏭️ Skipped ${filePath} (already exists — your version is kept)\x1b[0m`);
1001
1124
  skipped++;
1002
1125
  }
1003
1126
  } else if (typeof result === 'object') {
@@ -1024,9 +1147,11 @@ async function setup(options) {
1024
1147
  }
1025
1148
  if (!fs.existsSync(filePath)) {
1026
1149
  fs.writeFileSync(filePath, content, 'utf8');
1027
- console.log(` \x1b[32m✅\x1b[0m Created ${path.relative(options.dir, filePath)}`);
1150
+ writtenFiles.push(path.relative(options.dir, filePath));
1151
+ log(` \x1b[32m✅\x1b[0m Created ${path.relative(options.dir, filePath)}`);
1028
1152
  created++;
1029
1153
  } else {
1154
+ preservedFiles.push(path.relative(options.dir, filePath));
1030
1155
  skipped++;
1031
1156
  }
1032
1157
  }
@@ -1040,6 +1165,18 @@ async function setup(options) {
1040
1165
  const hookFiles = fs.readdirSync(hooksDir).filter(f => f.endsWith('.sh'));
1041
1166
  if (hookFiles.length > 0) {
1042
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
+ },
1043
1180
  hooks: {
1044
1181
  PostToolUse: [{
1045
1182
  matcher: "Write|Edit",
@@ -1063,26 +1200,35 @@ async function setup(options) {
1063
1200
  }];
1064
1201
  }
1065
1202
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
1066
- console.log(` \x1b[32m✅\x1b[0m Created .claude/settings.json (hooks registered)`);
1203
+ writtenFiles.push('.claude/settings.json');
1204
+ log(` \x1b[32m✅\x1b[0m Created .claude/settings.json (hooks registered)`);
1067
1205
  created++;
1068
1206
  }
1069
1207
  }
1070
1208
 
1071
- console.log('');
1209
+ log('');
1072
1210
  if (created === 0 && skipped > 0) {
1073
- console.log(' \x1b[32m✅\x1b[0m Your project is already well configured!');
1074
- console.log(` \x1b[2m ${skipped} files already exist and were preserved.\x1b[0m`);
1075
- console.log(' \x1b[2m We never overwrite your existing config — your setup is kept.\x1b[0m');
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');
1076
1214
  } else if (created > 0) {
1077
- console.log(` \x1b[1m${created} files created.\x1b[0m`);
1215
+ log(` \x1b[1m${created} files created.\x1b[0m`);
1078
1216
  if (skipped > 0) {
1079
- console.log(` \x1b[2m${skipped} existing files preserved (not overwritten).\x1b[0m`);
1217
+ log(` \x1b[2m${skipped} existing files preserved (not overwritten).\x1b[0m`);
1080
1218
  }
1081
1219
  }
1082
1220
 
1083
- console.log('');
1084
- console.log(' Run \x1b[1mnpx claudex-setup audit\x1b[0m to check your score.');
1085
- console.log('');
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
+ };
1086
1232
  }
1087
1233
 
1088
- 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;
@@ -955,6 +963,8 @@ const STACKS = {
955
963
  swift: { files: ['Package.swift'], content: {}, label: 'Swift' },
956
964
  terraform: { files: ['main.tf', 'terraform'], content: {}, label: 'Terraform' },
957
965
  kubernetes: { files: ['k8s', 'kubernetes', 'helm'], content: {}, label: 'Kubernetes' },
966
+ cpp: { files: ['CMakeLists.txt', 'Makefile', '.clang-format'], content: {}, label: 'C++' },
967
+ bazel: { files: ['BUILD', 'WORKSPACE', 'BUILD.bazel', 'WORKSPACE.bazel'], content: {}, label: 'Bazel' },
958
968
  };
959
969
 
960
970
  module.exports = { TECHNIQUES, STACKS };