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/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');
@@ -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 = ids['API Routes'] || ids['Hooks'] || ids['Components'] || root;
398
+ const parent = pickNodeId('API Routes', 'Hooks', 'Components');
344
399
  edges.push(` ${parent} --> ${ids['lib/']}`);
345
- } else if (dirNames.includes('src') && !hasAppRouter && !hasPages) {
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 = ids['src/'] || ids['Entry Point'];
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 = ids['API Layer'] || ids['Routes'] || ids['src/'] || ids['Entry Point'];
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 = ids['Services'] || ids['API Layer'] || ids['Routes'] || ids['src/'] || ids['Entry Point'];
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 = ids['src/'] || ids['Services'] || ids['Entry Point'];
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 = ids['API Layer'] || ids['Routes'] || ids['Entry Point'];
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 = ids['src/'] || ids['Entry Point'];
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
- ### Next.js App Router
421
- - Default to Server Components. Add 'use client' only when needed (hooks, events, browser APIs)
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 for project name/description ---
659
- const pkg = ctx.jsonFile('package.json');
660
- const projectName = (pkg && pkg.name) ? pkg.name : path.basename(ctx.dir);
661
- 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}` : '';
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
- ## Code Style
678
- - Follow existing patterns in the codebase
679
- - Write tests for new features
680
- - Keep functions small and focused (< 50 lines)
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
- console.log('');
997
- console.log('\x1b[1m claudex-setup\x1b[0m');
998
- console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
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
- 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`);
1002
1079
  }
1003
- console.log('');
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
- console.log(` \x1b[32m✅\x1b[0m Created ${filePath}`);
1118
+ writtenFiles.push(filePath);
1119
+ log(` \x1b[32m✅\x1b[0m Created ${filePath}`);
1042
1120
  created++;
1043
1121
  } else {
1044
- 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`);
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
- 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)}`);
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
- 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)`);
1111
1205
  created++;
1112
1206
  }
1113
1207
  }
1114
1208
 
1115
- console.log('');
1209
+ log('');
1116
1210
  if (created === 0 && skipped > 0) {
1117
- console.log(' \x1b[32m✅\x1b[0m Your project is already well configured!');
1118
- console.log(` \x1b[2m ${skipped} files already exist and were preserved.\x1b[0m`);
1119
- 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');
1120
1214
  } else if (created > 0) {
1121
- console.log(` \x1b[1m${created} files created.\x1b[0m`);
1215
+ log(` \x1b[1m${created} files created.\x1b[0m`);
1122
1216
  if (skipped > 0) {
1123
- console.log(` \x1b[2m${skipped} existing files preserved (not overwritten).\x1b[0m`);
1217
+ log(` \x1b[2m${skipped} existing files preserved (not overwritten).\x1b[0m`);
1124
1218
  }
1125
1219
  }
1126
1220
 
1127
- console.log('');
1128
- console.log(' Run \x1b[1mnpx claudex-setup audit\x1b[0m to check your score.');
1129
- 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
+ };
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;