claudex-setup 1.6.0 → 1.8.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.8.0 - Starter-safe setup engine with reusable planning primitives.
4
4
  */
5
5
 
6
6
  const fs = require('fs');
@@ -8,6 +8,7 @@ const path = require('path');
8
8
  const { TECHNIQUES, STACKS } = require('./techniques');
9
9
  const { ProjectContext } = require('./context');
10
10
  const { audit } = require('./audit');
11
+ const { buildSettingsForProfile } = require('./governance');
11
12
 
12
13
  // ============================================================
13
14
  // Helper: detect project scripts from package.json
@@ -248,9 +249,9 @@ function detectDependencies(ctx) {
248
249
  // Helper: detect main directories
249
250
  // ============================================================
250
251
  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'];
252
+ 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
253
  // 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'];
254
+ 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
255
  const found = [];
255
256
  const seenNames = new Set();
256
257
 
@@ -267,6 +268,55 @@ function detectMainDirs(ctx) {
267
268
  return found;
268
269
  }
269
270
 
271
+ function escapeRegex(value) {
272
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
273
+ }
274
+
275
+ function extractTomlSection(content, sectionName) {
276
+ const pattern = new RegExp(`\\[${escapeRegex(sectionName)}\\]([\\s\\S]*?)(?:\\n\\s*\\[|$)`);
277
+ const match = content.match(pattern);
278
+ return match ? match[1] : null;
279
+ }
280
+
281
+ function extractTomlValue(sectionContent, key) {
282
+ if (!sectionContent) return null;
283
+ const pattern = new RegExp(`^\\s*${escapeRegex(key)}\\s*=\\s*["']([^"']+)["']`, 'm');
284
+ const match = sectionContent.match(pattern);
285
+ return match ? match[1].trim() : null;
286
+ }
287
+
288
+ function detectProjectMetadata(ctx) {
289
+ const pkg = ctx.jsonFile('package.json');
290
+ if (pkg && (pkg.name || pkg.description)) {
291
+ return {
292
+ name: pkg.name || path.basename(ctx.dir),
293
+ description: pkg.description || '',
294
+ };
295
+ }
296
+
297
+ const pyproject = ctx.fileContent('pyproject.toml') || '';
298
+ if (pyproject) {
299
+ const projectSection = extractTomlSection(pyproject, 'project');
300
+ const poetrySection = extractTomlSection(pyproject, 'tool.poetry');
301
+ const name = extractTomlValue(projectSection, 'name') ||
302
+ extractTomlValue(poetrySection, 'name');
303
+ const description = extractTomlValue(projectSection, 'description') ||
304
+ extractTomlValue(poetrySection, 'description');
305
+
306
+ if (name || description) {
307
+ return {
308
+ name: name || path.basename(ctx.dir),
309
+ description: description || '',
310
+ };
311
+ }
312
+ }
313
+
314
+ return {
315
+ name: path.basename(ctx.dir),
316
+ description: '',
317
+ };
318
+ }
319
+
270
320
  // ============================================================
271
321
  // Helper: generate Mermaid diagram from directory structure
272
322
  // ============================================================
@@ -295,6 +345,11 @@ function generateMermaid(dirs, stacks) {
295
345
  const hasSrcComponents = dirNames.includes('src/components') || dirNames.includes('components');
296
346
  const hasSrcHooks = dirNames.includes('src/hooks') || dirNames.includes('hooks');
297
347
  const hasSrcLib = dirNames.includes('src/lib') || dirNames.includes('lib');
348
+ const hasSrcNode = dirNames.includes('src');
349
+ const hasAgents = dirNames.includes('src/agents') || dirNames.includes('agents');
350
+ const hasChains = dirNames.includes('src/chains') || dirNames.includes('chains');
351
+ const hasWorkers = dirNames.includes('src/workers') || dirNames.includes('workers') || dirNames.includes('jobs');
352
+ const hasPipelines = dirNames.includes('dags') || dirNames.includes('macros');
298
353
 
299
354
  // Smart entry point based on framework
300
355
  const isNextJs = stackKeys.includes('nextjs');
@@ -312,6 +367,7 @@ function generateMermaid(dirs, stacks) {
312
367
  }
313
368
 
314
369
  const root = ids['Next.js'] || ids['Django'] || ids['FastAPI'] || ids['Entry Point'];
370
+ const pickNodeId = (...labels) => labels.map(label => ids[label]).find(Boolean) || root;
315
371
 
316
372
  // Detect layers
317
373
  if (hasAppRouter || hasPages) {
@@ -340,9 +396,9 @@ function generateMermaid(dirs, stacks) {
340
396
 
341
397
  if (hasSrcLib) {
342
398
  nodes.push(addNode('lib/', 'default'));
343
- const parent = ids['API Routes'] || ids['Hooks'] || ids['Components'] || root;
399
+ const parent = pickNodeId('API Routes', 'Hooks', 'Components');
344
400
  edges.push(` ${parent} --> ${ids['lib/']}`);
345
- } else if (dirNames.includes('src') && !hasAppRouter && !hasPages) {
401
+ } else if (hasSrcNode && !hasAppRouter && !hasPages) {
346
402
  nodes.push(addNode('src/', 'default'));
347
403
  edges.push(` ${root} --> ${ids['src/']}`);
348
404
  }
@@ -350,19 +406,19 @@ function generateMermaid(dirs, stacks) {
350
406
  if (dirNames.includes('api') || dirNames.includes('routes') || dirNames.includes('controllers')) {
351
407
  const label = dirNames.includes('api') ? 'API Layer' : 'Routes';
352
408
  nodes.push(addNode(label, 'default'));
353
- const parent = ids['src/'] || ids['Entry Point'];
409
+ const parent = pickNodeId('src/', 'App Router', 'Pages');
354
410
  edges.push(` ${parent} --> ${ids[label]}`);
355
411
  }
356
412
 
357
413
  if (dirNames.includes('services')) {
358
414
  nodes.push(addNode('Services', 'default'));
359
- const parent = ids['API Layer'] || ids['Routes'] || ids['src/'] || ids['Entry Point'];
415
+ const parent = pickNodeId('API Layer', 'Routes', 'src/', 'App Router', 'Pages');
360
416
  edges.push(` ${parent} --> ${ids['Services']}`);
361
417
  }
362
418
 
363
419
  if (dirNames.includes('models') || dirNames.includes('prisma') || dirNames.includes('db')) {
364
420
  nodes.push(addNode('Data Layer', 'default'));
365
- const parent = ids['Services'] || ids['API Layer'] || ids['Routes'] || ids['src/'] || ids['Entry Point'];
421
+ const parent = pickNodeId('Services', 'API Layer', 'Routes', 'src/', 'App Router', 'Pages');
366
422
  edges.push(` ${parent} --> ${ids['Data Layer']}`);
367
423
  nodes.push(addNode('Database', 'db'));
368
424
  edges.push(` ${ids['Data Layer']} --> ${ids['Database']}`);
@@ -370,19 +426,43 @@ function generateMermaid(dirs, stacks) {
370
426
 
371
427
  if (dirNames.includes('utils') || dirNames.includes('helpers')) {
372
428
  nodes.push(addNode('Utils', 'default'));
373
- const parent = ids['src/'] || ids['Services'] || ids['Entry Point'];
429
+ const parent = pickNodeId('src/', 'Services', 'lib/', 'Components');
374
430
  edges.push(` ${parent} --> ${ids['Utils']}`);
375
431
  }
376
432
 
377
433
  if (dirNames.includes('middleware')) {
378
434
  nodes.push(addNode('Middleware', 'default'));
379
- const parent = ids['API Layer'] || ids['Routes'] || ids['Entry Point'];
435
+ const parent = pickNodeId('API Layer', 'Routes', 'App Router', 'Pages');
380
436
  edges.push(` ${parent} --> ${ids['Middleware']}`);
381
437
  }
382
438
 
439
+ if (hasChains) {
440
+ nodes.push(addNode('Chains', 'default'));
441
+ const parent = pickNodeId('Services', 'src/', 'lib/', 'API Layer');
442
+ edges.push(` ${parent} --> ${ids['Chains']}`);
443
+ }
444
+
445
+ if (hasAgents) {
446
+ nodes.push(addNode('Agents', 'default'));
447
+ const parent = pickNodeId('Chains', 'Services', 'src/', 'lib/');
448
+ edges.push(` ${parent} --> ${ids['Agents']}`);
449
+ }
450
+
451
+ if (hasWorkers) {
452
+ nodes.push(addNode('Workers', 'default'));
453
+ const parent = pickNodeId('Services', 'API Layer', 'src/');
454
+ edges.push(` ${parent} --> ${ids['Workers']}`);
455
+ }
456
+
457
+ if (hasPipelines) {
458
+ nodes.push(addNode('Pipelines', 'default'));
459
+ const parent = pickNodeId('Services', 'Data Layer', 'src/');
460
+ edges.push(` ${parent} --> ${ids['Pipelines']}`);
461
+ }
462
+
383
463
  if (dirNames.includes('tests') || dirNames.includes('test') || dirNames.includes('__tests__') || dirNames.includes('spec')) {
384
464
  nodes.push(addNode('Tests', 'round'));
385
- const parent = ids['src/'] || ids['Entry Point'];
465
+ const parent = pickNodeId('src/', 'App Router', 'Pages', 'Services', 'Components');
386
466
  edges.push(` ${ids['Tests']} -.-> ${parent}`);
387
467
  }
388
468
 
@@ -416,13 +496,9 @@ function getFrameworkInstructions(stacks) {
416
496
  - Use next/image for images, next/link for navigation
417
497
  - API routes go in app/api/ (App Router) or pages/api/ (Pages Router)
418
498
  - 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`);
499
+ - If app/ exists, use Server Actions for mutations, validate with Zod, and call revalidatePath after writes
500
+ - Route handlers in app/api/ should export named functions: GET, POST, PUT, DELETE
501
+ - Middleware in middleware.ts should handle auth checks, redirects, and headers`);
426
502
  } else if (stackKeys.includes('react')) {
427
503
  sections.push(`### React
428
504
  - Use functional components with hooks exclusively
@@ -655,10 +731,10 @@ ${depGuidelines.join('\n')}
655
731
  }
656
732
  verificationSteps.push(`${verificationSteps.length + 1}. Changes match the requested scope (no gold-plating)`);
657
733
 
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}` : '';
734
+ // --- Read project metadata from package.json or pyproject.toml ---
735
+ const projectMeta = detectProjectMetadata(ctx);
736
+ const projectName = projectMeta.name;
737
+ const projectDesc = projectMeta.description ? ` — ${projectMeta.description}` : '';
662
738
 
663
739
  // --- Assemble the final CLAUDE.md ---
664
740
  return `# ${projectName}${projectDesc}
@@ -674,11 +750,10 @@ ${stackSection}${tsSection}${depSection}
674
750
  ${buildSection}
675
751
  \`\`\`
676
752
 
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
753
+ ## Working Notes
754
+ - You are a careful engineer working inside this repository. Preserve its existing architecture and naming patterns unless the task requires a change
755
+ - Prefer extending existing modules over creating parallel abstractions
756
+ - Keep changes scoped to the requested task and verify them before marking work complete
682
757
 
683
758
  <constraints>
684
759
  - Never commit secrets, API keys, or .env files
@@ -700,12 +775,6 @@ ${verificationSteps.join('\n')}
700
775
  - If a session gets too long, start fresh with /clear
701
776
  - Use subagents for research tasks to keep main context clean
702
777
 
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
778
  ---
710
779
  *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
780
  `;
@@ -758,6 +827,19 @@ mkdir -p "$LOG_DIR"
758
827
  TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
759
828
  echo "[$TIMESTAMP] $TOOL_NAME: $FILE_PATH" >> "$LOG_FILE"
760
829
 
830
+ exit 0
831
+ `,
832
+ 'session-start.sh': `#!/bin/bash
833
+ # SessionStart hook - prepares logs and records session entry
834
+
835
+ LOG_DIR=".claude/logs"
836
+ LOG_FILE="$LOG_DIR/sessions.log"
837
+
838
+ mkdir -p "$LOG_DIR"
839
+
840
+ TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
841
+ echo "[$TIMESTAMP] session started" >> "$LOG_FILE"
842
+
761
843
  exit 0
762
844
  `,
763
845
  }),
@@ -809,6 +891,15 @@ exit 0
809
891
  1. Run \`git diff\` to see all changes
810
892
  2. Check for: bugs, security issues, missing tests, code style
811
893
  3. Provide actionable feedback
894
+ `;
895
+
896
+ cmds['security-review.md'] = `Run a focused security review using Claude Code's built-in security workflow.
897
+
898
+ ## Steps:
899
+ 1. Review auth, permissions, secrets handling, and data access paths
900
+ 2. Run \`/security-review\` for OWASP-focused analysis
901
+ 3. Check for unsafe shell commands, token leakage, and risky file access
902
+ 4. Report findings ordered by severity with concrete fixes
812
903
  `;
813
904
 
814
905
  // Deploy - stack-specific
@@ -912,6 +1003,17 @@ Fix the GitHub issue: $ARGUMENTS
912
1003
  3. Implement the fix
913
1004
  4. Write tests
914
1005
  5. Create a descriptive commit
1006
+ `,
1007
+ 'release-check/SKILL.md': `---
1008
+ name: release-check
1009
+ description: Prepare a release candidate and verify publish readiness
1010
+ ---
1011
+ Prepare a release candidate for: $ARGUMENTS
1012
+
1013
+ 1. Read CHANGELOG.md and package.json version
1014
+ 2. Run the test suite and packaging checks
1015
+ 3. Verify docs, tags, and release notes are aligned
1016
+ 4. Flag anything that would make the release unsafe or misleading
915
1017
  `,
916
1018
  }),
917
1019
 
@@ -959,6 +1061,12 @@ Fix the GitHub issue: $ARGUMENTS
959
1061
  - Never skip or disable tests without a tracking issue
960
1062
  - Mock external dependencies, not internal logic
961
1063
  - Include both happy path and edge case tests
1064
+ `;
1065
+ rules['repository.md'] = `When changing release, packaging, or workflow files:
1066
+ - Keep package.json, CHANGELOG.md, README.md, and docs in sync
1067
+ - Prefer tagged release references over floating branch references in public docs
1068
+ - Preserve backward compatibility in CLI flags where practical
1069
+ - Any automation that writes files must document rollback expectations
962
1070
  `;
963
1071
  return rules;
964
1072
  },
@@ -969,12 +1077,26 @@ name: security-reviewer
969
1077
  description: Reviews code for security vulnerabilities
970
1078
  tools: [Read, Grep, Glob]
971
1079
  model: sonnet
1080
+ maxTurns: 50
972
1081
  ---
973
1082
  Review code for security issues:
974
1083
  - Injection vulnerabilities (SQL, XSS, command injection)
975
1084
  - Authentication and authorization flaws
976
1085
  - Secrets or credentials in code
977
1086
  - Insecure data handling
1087
+ `,
1088
+ 'release-manager.md': `---
1089
+ name: release-manager
1090
+ description: Checks release readiness and packaging consistency
1091
+ tools: [Read, Grep, Glob]
1092
+ model: sonnet
1093
+ maxTurns: 50
1094
+ ---
1095
+ Review release readiness:
1096
+ - version alignment across package.json, changelog, and docs
1097
+ - publish safety and packaging scope
1098
+ - missing rollback or migration notes
1099
+ - documentation drift that would confuse adopters
978
1100
  `,
979
1101
  }),
980
1102
 
@@ -992,15 +1114,24 @@ graph TD
992
1114
  async function setup(options) {
993
1115
  const ctx = new ProjectContext(options.dir);
994
1116
  const stacks = ctx.detectStacks(STACKS);
1117
+ const silent = options.silent === true;
1118
+ const writtenFiles = [];
1119
+ const preservedFiles = [];
1120
+
1121
+ function log(message = '') {
1122
+ if (!silent) {
1123
+ console.log(message);
1124
+ }
1125
+ }
995
1126
 
996
- console.log('');
997
- console.log('\x1b[1m claudex-setup\x1b[0m');
998
- console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
1127
+ log('');
1128
+ log('\x1b[1m claudex-setup\x1b[0m');
1129
+ log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
999
1130
 
1000
1131
  if (stacks.length > 0) {
1001
- console.log(`\x1b[36m Detected: ${stacks.map(s => s.label).join(', ')}\x1b[0m`);
1132
+ log(`\x1b[36m Detected: ${stacks.map(s => s.label).join(', ')}\x1b[0m`);
1002
1133
  }
1003
- console.log('');
1134
+ log('');
1004
1135
 
1005
1136
  let created = 0;
1006
1137
  let skipped = 0;
@@ -1038,10 +1169,12 @@ async function setup(options) {
1038
1169
 
1039
1170
  if (!fs.existsSync(fullPath)) {
1040
1171
  fs.writeFileSync(fullPath, result, 'utf8');
1041
- console.log(` \x1b[32m✅\x1b[0m Created ${filePath}`);
1172
+ writtenFiles.push(filePath);
1173
+ log(` \x1b[32m✅\x1b[0m Created ${filePath}`);
1042
1174
  created++;
1043
1175
  } else {
1044
- console.log(` \x1b[2m⏭️ Skipped ${filePath} (already exists — your version is kept)\x1b[0m`);
1176
+ preservedFiles.push(filePath);
1177
+ log(` \x1b[2m⏭️ Skipped ${filePath} (already exists — your version is kept)\x1b[0m`);
1045
1178
  skipped++;
1046
1179
  }
1047
1180
  } else if (typeof result === 'object') {
@@ -1068,9 +1201,11 @@ async function setup(options) {
1068
1201
  }
1069
1202
  if (!fs.existsSync(filePath)) {
1070
1203
  fs.writeFileSync(filePath, content, 'utf8');
1071
- console.log(` \x1b[32m✅\x1b[0m Created ${path.relative(options.dir, filePath)}`);
1204
+ writtenFiles.push(path.relative(options.dir, filePath));
1205
+ log(` \x1b[32m✅\x1b[0m Created ${path.relative(options.dir, filePath)}`);
1072
1206
  created++;
1073
1207
  } else {
1208
+ preservedFiles.push(path.relative(options.dir, filePath));
1074
1209
  skipped++;
1075
1210
  }
1076
1211
  }
@@ -1083,50 +1218,41 @@ async function setup(options) {
1083
1218
  if (fs.existsSync(hooksDir) && !fs.existsSync(settingsPath)) {
1084
1219
  const hookFiles = fs.readdirSync(hooksDir).filter(f => f.endsWith('.sh'));
1085
1220
  if (hookFiles.length > 0) {
1086
- const settings = {
1087
- hooks: {
1088
- PostToolUse: [{
1089
- matcher: "Write|Edit",
1090
- hooks: hookFiles.filter(f => f !== 'protect-secrets.sh').map(f => ({
1091
- type: "command",
1092
- command: `bash .claude/hooks/${f}`,
1093
- timeout: 10
1094
- }))
1095
- }]
1096
- }
1097
- };
1098
- // Add protect-secrets as PreToolUse if it exists
1099
- if (hookFiles.includes('protect-secrets.sh')) {
1100
- settings.hooks.PreToolUse = [{
1101
- matcher: "Read|Write|Edit",
1102
- hooks: [{
1103
- type: "command",
1104
- command: "bash .claude/hooks/protect-secrets.sh",
1105
- timeout: 5
1106
- }]
1107
- }];
1108
- }
1221
+ const settings = buildSettingsForProfile({
1222
+ profileKey: options.profile || 'safe-write',
1223
+ hookFiles,
1224
+ mcpPackKeys: options.mcpPacks || [],
1225
+ });
1109
1226
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
1110
- console.log(` \x1b[32m✅\x1b[0m Created .claude/settings.json (hooks registered)`);
1227
+ writtenFiles.push('.claude/settings.json');
1228
+ log(` \x1b[32m✅\x1b[0m Created .claude/settings.json (hooks registered)`);
1111
1229
  created++;
1112
1230
  }
1113
1231
  }
1114
1232
 
1115
- console.log('');
1233
+ log('');
1116
1234
  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');
1235
+ log(' \x1b[32m✅\x1b[0m Your project is already well configured!');
1236
+ log(` \x1b[2m ${skipped} files already exist and were preserved.\x1b[0m`);
1237
+ log(' \x1b[2m We never overwrite your existing config — your setup is kept.\x1b[0m');
1120
1238
  } else if (created > 0) {
1121
- console.log(` \x1b[1m${created} files created.\x1b[0m`);
1239
+ log(` \x1b[1m${created} files created.\x1b[0m`);
1122
1240
  if (skipped > 0) {
1123
- console.log(` \x1b[2m${skipped} existing files preserved (not overwritten).\x1b[0m`);
1241
+ log(` \x1b[2m${skipped} existing files preserved (not overwritten).\x1b[0m`);
1124
1242
  }
1125
1243
  }
1126
1244
 
1127
- console.log('');
1128
- console.log(' Run \x1b[1mnpx claudex-setup audit\x1b[0m to check your score.');
1129
- console.log('');
1245
+ log('');
1246
+ log(' Run \x1b[1mnpx claudex-setup audit\x1b[0m to check your score.');
1247
+ log('');
1248
+
1249
+ return {
1250
+ created,
1251
+ skipped,
1252
+ writtenFiles,
1253
+ preservedFiles,
1254
+ stacks,
1255
+ };
1130
1256
  }
1131
1257
 
1132
- module.exports = { setup };
1258
+ module.exports = { setup, TEMPLATES };
package/src/techniques.js CHANGED
@@ -4,6 +4,12 @@
4
4
  * Each technique includes: what to check, how to fix, impact level.
5
5
  */
6
6
 
7
+ function hasFrontendSignals(ctx) {
8
+ const pkg = ctx.fileContent('package.json') || '';
9
+ return /react|vue|angular|next|svelte|tailwind|vite|astro/i.test(pkg) ||
10
+ ctx.files.some(f => /tailwind\.config|vite\.config|next\.config|svelte\.config|nuxt\.config|pages\/|components\/|app\//i.test(f));
11
+ }
12
+
7
13
  const TECHNIQUES = {
8
14
  // ============================================================
9
15
  // === MEMORY & CONTEXT (category: 'memory') ==================
@@ -142,8 +148,13 @@ const TECHNIQUES = {
142
148
  name: '.claude/ tracked in git',
143
149
  check: (ctx) => {
144
150
  if (!ctx.fileContent('.gitignore')) return true; // no gitignore = ok
145
- const content = ctx.fileContent('.gitignore');
146
- return !content.includes('.claude/') || content.includes('!.claude/');
151
+ const lines = ctx.fileContent('.gitignore')
152
+ .split(/\r?\n/)
153
+ .map(line => line.trim())
154
+ .filter(line => line && !line.startsWith('#'));
155
+ const ignoresClaudeDir = lines.some(line => /^(\/|\*\*\/)?\.claude\/?$/.test(line));
156
+ const unignoresClaudeDir = lines.some(line => /^!(\/)?\.claude(\/|\*\*)?$/.test(line));
157
+ return !ignoresClaudeDir || unignoresClaudeDir;
147
158
  },
148
159
  impact: 'high',
149
160
  rating: 4,
@@ -170,6 +181,10 @@ const TECHNIQUES = {
170
181
  id: 91701,
171
182
  name: '.gitignore blocks node_modules',
172
183
  check: (ctx) => {
184
+ const hasNodeSignals = ctx.files.includes('package.json') ||
185
+ ctx.files.includes('tsconfig.json') ||
186
+ ctx.files.some(f => /package-lock\.json|pnpm-lock\.yaml|yarn\.lock|next\.config|vite\.config/i.test(f));
187
+ if (!hasNodeSignals) return null;
173
188
  const gitignore = ctx.fileContent('.gitignore') || '';
174
189
  return gitignore.includes('node_modules');
175
190
  },
@@ -314,7 +329,7 @@ const TECHNIQUES = {
314
329
  name: 'Permission configuration',
315
330
  check: (ctx) => {
316
331
  const settings = ctx.jsonFile('.claude/settings.local.json') || ctx.jsonFile('.claude/settings.json');
317
- return settings && settings.permissions;
332
+ return !!(settings && settings.permissions);
318
333
  },
319
334
  impact: 'medium',
320
335
  rating: 4,
@@ -467,6 +482,7 @@ const TECHNIQUES = {
467
482
  id: 1025,
468
483
  name: 'Frontend design skill for anti-AI-slop',
469
484
  check: (ctx) => {
485
+ if (!hasFrontendSignals(ctx)) return null;
470
486
  const md = ctx.fileContent('CLAUDE.md') || '';
471
487
  return md.includes('frontend_aesthetics') || md.includes('anti-AI-slop') || md.includes('frontend-design');
472
488
  },
@@ -481,6 +497,7 @@ const TECHNIQUES = {
481
497
  id: 102501,
482
498
  name: 'Tailwind CSS configured',
483
499
  check: (ctx) => {
500
+ if (!hasFrontendSignals(ctx)) return null;
484
501
  const pkg = ctx.fileContent('package.json') || '';
485
502
  return pkg.includes('tailwind') ||
486
503
  ctx.files.some(f => /tailwind\.config/.test(f));
@@ -603,9 +620,13 @@ const TECHNIQUES = {
603
620
  id: 5002,
604
621
  name: 'Node version pinned',
605
622
  check: (ctx) => {
623
+ const hasNodeSignals = ctx.files.includes('package.json') ||
624
+ ctx.files.includes('tsconfig.json') ||
625
+ ctx.files.some(f => /package-lock\.json|pnpm-lock\.yaml|yarn\.lock|next\.config|vite\.config/i.test(f));
626
+ if (!hasNodeSignals) return null;
606
627
  if (ctx.files.includes('.nvmrc') || ctx.files.includes('.node-version')) return true;
607
628
  const pkg = ctx.jsonFile('package.json');
608
- return pkg && pkg.engines && pkg.engines.node;
629
+ return !!(pkg && pkg.engines && pkg.engines.node);
609
630
  },
610
631
  impact: 'low',
611
632
  rating: 3,
@@ -655,7 +676,7 @@ const TECHNIQUES = {
655
676
  name: 'MCP servers configured',
656
677
  check: (ctx) => {
657
678
  const settings = ctx.jsonFile('.claude/settings.local.json') || ctx.jsonFile('.claude/settings.json');
658
- return settings && settings.mcpServers && Object.keys(settings.mcpServers).length > 0;
679
+ return !!(settings && settings.mcpServers && Object.keys(settings.mcpServers).length > 0);
659
680
  },
660
681
  impact: 'medium',
661
682
  rating: 3,
@@ -669,7 +690,7 @@ const TECHNIQUES = {
669
690
  name: '2+ MCP servers for rich tooling',
670
691
  check: (ctx) => {
671
692
  const settings = ctx.jsonFile('.claude/settings.local.json') || ctx.jsonFile('.claude/settings.json');
672
- return settings && settings.mcpServers && Object.keys(settings.mcpServers).length >= 2;
693
+ return !!(settings && settings.mcpServers && Object.keys(settings.mcpServers).length >= 2);
673
694
  },
674
695
  impact: 'medium',
675
696
  rating: 4,