claudex-setup 1.0.0 → 1.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudex-setup",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Audit and optimize any project for Claude Code. Powered by 1107 verified techniques.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/audit.js CHANGED
@@ -61,17 +61,20 @@ async function audit(options) {
61
61
  });
62
62
  }
63
63
 
64
- const passed = results.filter(r => r.passed);
65
- const failed = results.filter(r => !r.passed);
64
+ // null = not applicable (skip), true = pass, false = fail
65
+ const applicable = results.filter(r => r.passed !== null);
66
+ const skipped = results.filter(r => r.passed === null);
67
+ const passed = applicable.filter(r => r.passed);
68
+ const failed = applicable.filter(r => !r.passed);
66
69
  const critical = failed.filter(r => r.impact === 'critical');
67
70
  const high = failed.filter(r => r.impact === 'high');
68
71
  const medium = failed.filter(r => r.impact === 'medium');
69
72
 
70
- // Calculate score
73
+ // Calculate score only from applicable checks
71
74
  const weights = { critical: 15, high: 10, medium: 5 };
72
- const maxScore = results.reduce((sum, r) => sum + (weights[r.impact] || 5), 0);
75
+ const maxScore = applicable.reduce((sum, r) => sum + (weights[r.impact] || 5), 0);
73
76
  const earnedScore = passed.reduce((sum, r) => sum + (weights[r.impact] || 5), 0);
74
- const score = Math.round((earnedScore / maxScore) * 100);
77
+ const score = maxScore > 0 ? Math.round((earnedScore / maxScore) * 100) : 0;
75
78
 
76
79
  // Silent mode: skip all output, just return result
77
80
  if (silent) {
@@ -153,7 +156,7 @@ async function audit(options) {
153
156
 
154
157
  // Summary
155
158
  console.log(colorize(' ─────────────────────────────────────', 'dim'));
156
- console.log(` ${colorize(`${passed.length}/${results.length}`, 'bold')} checks passing`);
159
+ console.log(` ${colorize(`${passed.length}/${applicable.length}`, 'bold')} checks passing${skipped.length > 0 ? colorize(` (${skipped.length} not applicable)`, 'dim') : ''}`);
157
160
 
158
161
  if (failed.length > 0) {
159
162
  console.log(` Run ${colorize('npx claudex-setup setup', 'bold')} to fix automatically`);
package/src/setup.js CHANGED
@@ -25,16 +25,110 @@ function detectScripts(ctx) {
25
25
  return found;
26
26
  }
27
27
 
28
+ // ============================================================
29
+ // Helper: detect key dependencies and generate guidelines
30
+ // ============================================================
31
+ function detectDependencies(ctx) {
32
+ const pkg = ctx.jsonFile('package.json');
33
+ if (!pkg) return [];
34
+ const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
35
+ const guidelines = [];
36
+
37
+ // Data fetching
38
+ if (allDeps['@tanstack/react-query']) {
39
+ guidelines.push('- Use React Query (TanStack Query) for all server data fetching — never raw useEffect + fetch');
40
+ guidelines.push('- Define query keys as constants. Invalidate related queries after mutations');
41
+ }
42
+ if (allDeps['swr']) {
43
+ guidelines.push('- Use SWR for data fetching with automatic revalidation');
44
+ }
45
+
46
+ // Validation
47
+ if (allDeps['zod']) {
48
+ guidelines.push('- Use Zod for all input validation and type inference (z.infer<typeof schema>)');
49
+ guidelines.push('- Define schemas in a shared location. Use .parse() at API boundaries');
50
+ }
51
+
52
+ // ORM / Database
53
+ if (allDeps['prisma'] || allDeps['@prisma/client']) {
54
+ guidelines.push('- Use Prisma for all database operations. Run `npx prisma generate` after schema changes');
55
+ guidelines.push('- Never write raw SQL unless Prisma cannot express the query');
56
+ }
57
+ if (allDeps['drizzle-orm']) {
58
+ guidelines.push('- Use Drizzle ORM for database operations. Schema-first approach');
59
+ }
60
+ if (allDeps['mongoose']) {
61
+ guidelines.push('- Use Mongoose for MongoDB operations. Define schemas with validation');
62
+ }
63
+
64
+ // Auth
65
+ if (allDeps['next-auth'] || allDeps['@auth/core']) {
66
+ guidelines.push('- Use NextAuth.js for authentication. Access session via auth() in Server Components');
67
+ }
68
+ if (allDeps['clerk'] || allDeps['@clerk/nextjs']) {
69
+ guidelines.push('- Use Clerk for authentication. Protect routes with middleware');
70
+ }
71
+
72
+ // State management
73
+ if (allDeps['zustand']) {
74
+ guidelines.push('- Use Zustand for client state. Keep stores small and focused');
75
+ }
76
+ if (allDeps['@reduxjs/toolkit']) {
77
+ guidelines.push('- Use Redux Toolkit for state management. Use createSlice and RTK Query');
78
+ }
79
+
80
+ // Styling
81
+ if (allDeps['tailwindcss']) {
82
+ guidelines.push('- Use Tailwind CSS for all styling. Avoid inline styles and CSS modules');
83
+ }
84
+ if (allDeps['styled-components'] || allDeps['@emotion/react']) {
85
+ guidelines.push('- Use CSS-in-JS for component styling. Colocate styles with components');
86
+ }
87
+
88
+ // Testing
89
+ if (allDeps['vitest']) {
90
+ guidelines.push('- Use Vitest for testing. Colocate test files with source (*.test.ts)');
91
+ }
92
+ if (allDeps['jest']) {
93
+ guidelines.push('- Use Jest for testing. Follow existing test patterns in the codebase');
94
+ }
95
+ if (allDeps['playwright'] || allDeps['@playwright/test']) {
96
+ guidelines.push('- Use Playwright for E2E tests. Keep tests in tests/ or e2e/');
97
+ }
98
+
99
+ // Python
100
+ const reqTxt = ctx.fileContent('requirements.txt') || '';
101
+ if (reqTxt.includes('sqlalchemy')) {
102
+ guidelines.push('- Use SQLAlchemy for all database operations');
103
+ }
104
+ if (reqTxt.includes('pydantic')) {
105
+ guidelines.push('- Use Pydantic for data validation and serialization');
106
+ }
107
+ if (reqTxt.includes('pytest')) {
108
+ guidelines.push('- Use pytest for testing. Run with `python -m pytest`');
109
+ }
110
+
111
+ return guidelines;
112
+ }
113
+
28
114
  // ============================================================
29
115
  // Helper: detect main directories
30
116
  // ============================================================
31
117
  function detectMainDirs(ctx) {
32
- 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'];
118
+ 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'];
119
+ // Also check inside src/ for nested structure (common in Next.js, React)
120
+ 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'];
33
121
  const found = [];
34
- for (const dir of candidates) {
122
+ const seenNames = new Set();
123
+
124
+ for (const dir of [...candidates, ...srcNested]) {
35
125
  if (ctx.hasDir(dir)) {
36
126
  const files = ctx.dirFiles(dir);
37
- found.push({ name: dir, fileCount: files.length, files: files.slice(0, 10) });
127
+ const displayName = dir.includes('/') ? dir : dir;
128
+ if (!seenNames.has(displayName)) {
129
+ found.push({ name: displayName, fileCount: files.length, files: files.slice(0, 10) });
130
+ seenNames.add(displayName);
131
+ }
38
132
  }
39
133
  }
40
134
  return found;
@@ -61,31 +155,63 @@ function generateMermaid(dirs, stacks) {
61
155
  return ` ${id}[${label}]`;
62
156
  }
63
157
 
64
- // Entry point
65
- nodes.push(addNode('Entry Point', 'round'));
158
+ // Detect Next.js App Router specifically
159
+ const hasAppRouter = dirNames.includes('app') || dirNames.includes('src/app');
160
+ const hasPages = dirNames.includes('pages') || dirNames.includes('src/pages');
161
+ const hasAppApi = dirNames.includes('app/api') || dirNames.includes('src/app/api');
162
+ const hasSrcComponents = dirNames.includes('src/components') || dirNames.includes('components');
163
+ const hasSrcHooks = dirNames.includes('src/hooks') || dirNames.includes('hooks');
164
+ const hasSrcLib = dirNames.includes('src/lib') || dirNames.includes('lib');
165
+
166
+ // Smart entry point based on framework
167
+ const isNextJs = stackKeys.includes('nextjs');
168
+ const isDjango = stackKeys.includes('django');
169
+ const isFastApi = stackKeys.includes('fastapi');
170
+
171
+ if (isNextJs) {
172
+ nodes.push(addNode('Next.js', 'round'));
173
+ } else if (isDjango) {
174
+ nodes.push(addNode('Django', 'round'));
175
+ } else if (isFastApi) {
176
+ nodes.push(addNode('FastAPI', 'round'));
177
+ } else {
178
+ nodes.push(addNode('Entry Point', 'round'));
179
+ }
180
+
181
+ const root = ids['Next.js'] || ids['Django'] || ids['FastAPI'] || ids['Entry Point'];
66
182
 
67
183
  // Detect layers
68
- if (dirNames.includes('app') || dirNames.includes('pages')) {
69
- nodes.push(addNode('Pages / Routes', 'default'));
70
- edges.push(` ${ids['Entry Point']} --> ${ids['Pages / Routes']}`);
184
+ if (hasAppRouter || hasPages) {
185
+ const label = hasAppRouter ? 'App Router' : 'Pages';
186
+ nodes.push(addNode(label, 'default'));
187
+ edges.push(` ${root} --> ${ids[label]}`);
188
+ }
189
+
190
+ if (hasAppApi) {
191
+ nodes.push(addNode('API Routes', 'default'));
192
+ const parent = ids['App Router'] || ids['Pages'] || root;
193
+ edges.push(` ${parent} --> ${ids['API Routes']}`);
71
194
  }
72
195
 
73
- if (dirNames.includes('components')) {
196
+ if (hasSrcComponents) {
74
197
  nodes.push(addNode('Components', 'default'));
75
- const parent = ids['Pages / Routes'] || ids['Entry Point'];
198
+ const parent = ids['App Router'] || ids['Pages'] || root;
76
199
  edges.push(` ${parent} --> ${ids['Components']}`);
77
200
  }
78
201
 
79
- if (dirNames.includes('src')) {
80
- nodes.push(addNode('src/', 'default'));
81
- const parent = ids['Pages / Routes'] || ids['Entry Point'];
82
- edges.push(` ${parent} --> ${ids['src/']}`);
202
+ if (hasSrcHooks) {
203
+ nodes.push(addNode('Hooks', 'default'));
204
+ const parent = ids['Components'] || root;
205
+ edges.push(` ${parent} --> ${ids['Hooks']}`);
83
206
  }
84
207
 
85
- if (dirNames.includes('lib')) {
208
+ if (hasSrcLib) {
86
209
  nodes.push(addNode('lib/', 'default'));
87
- const parent = ids['src/'] || ids['Entry Point'];
210
+ const parent = ids['API Routes'] || ids['Hooks'] || ids['Components'] || root;
88
211
  edges.push(` ${parent} --> ${ids['lib/']}`);
212
+ } else if (dirNames.includes('src') && !hasAppRouter && !hasPages) {
213
+ nodes.push(addNode('src/', 'default'));
214
+ edges.push(` ${root} --> ${ids['src/']}`);
89
215
  }
90
216
 
91
217
  if (dirNames.includes('api') || dirNames.includes('routes') || dirNames.includes('controllers')) {
@@ -316,6 +442,13 @@ npm run lint # or: npx eslint .`;
316
442
  }
317
443
  }
318
444
 
445
+ // --- Dependency-specific guidelines ---
446
+ const depGuidelines = detectDependencies(ctx);
447
+ const depSection = depGuidelines.length > 0 ? `
448
+ ## Key Dependencies
449
+ ${depGuidelines.join('\n')}
450
+ ` : '';
451
+
319
452
  // --- Verification criteria based on detected commands ---
320
453
  const verificationSteps = [];
321
454
  verificationSteps.push('1. All existing tests still pass');
@@ -348,7 +481,7 @@ ${mermaid}
348
481
  ${dirDescription}
349
482
  ## Stack
350
483
  ${stackNames}
351
- ${stackSection}${tsSection}
484
+ ${stackSection}${tsSection}${depSection}
352
485
  ## Build & Test
353
486
  \`\`\`bash
354
487
  ${buildSection}
@@ -379,6 +512,9 @@ ${verificationSteps.join('\n')}
379
512
  - Use descriptive commit messages (why, not what)
380
513
  - Create focused PRs — one concern per PR
381
514
  - Document non-obvious decisions in code comments
515
+
516
+ ---
517
+ *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.*
382
518
  `;
383
519
  },
384
520
 
@@ -413,8 +549,9 @@ echo '{"decision": "allow"}'
413
549
  # Appends to .claude/logs/file-changes.log
414
550
 
415
551
  INPUT=$(cat -)
416
- TOOL_NAME=$(echo "$INPUT" | grep -oP '"tool_name"\\s*:\\s*"\\K[^"]+' 2>/dev/null || echo "unknown")
417
- FILE_PATH=$(echo "$INPUT" | grep -oP '"file_path"\\s*:\\s*"\\K[^"]+' 2>/dev/null || echo "")
552
+ TOOL_NAME=$(echo "$INPUT" | sed -n 's/.*"tool_name"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p')
553
+ TOOL_NAME=\${TOOL_NAME:-unknown}
554
+ FILE_PATH=$(echo "$INPUT" | sed -n 's/.*"file_path"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p')
418
555
 
419
556
  if [ -z "$FILE_PATH" ]; then
420
557
  exit 0
@@ -590,7 +727,13 @@ async function setup(options) {
590
727
 
591
728
  if (typeof result === 'string') {
592
729
  // Single file template (like CLAUDE.md)
593
- const filePath = key === 'claudeMd' ? 'CLAUDE.md' : key;
730
+ // Map technique keys to actual file paths
731
+ const filePathMap = {
732
+ 'claudeMd': 'CLAUDE.md',
733
+ 'mermaidArchitecture': 'CLAUDE.md', // mermaid is part of CLAUDE.md, skip separate file
734
+ };
735
+ if (key === 'mermaidArchitecture') continue; // Mermaid is generated inside CLAUDE.md template
736
+ const filePath = filePathMap[key] || key;
594
737
  const fullPath = path.join(options.dir, filePath);
595
738
 
596
739
  if (!fs.existsSync(fullPath)) {
package/src/techniques.js CHANGED
@@ -344,8 +344,8 @@ const TECHNIQUES = {
344
344
  name: 'Default mode is not bypassPermissions',
345
345
  check: (ctx) => {
346
346
  const settings = ctx.jsonFile('.claude/settings.local.json') || ctx.jsonFile('.claude/settings.json');
347
- if (!settings) return true; // no settings = not bypassed
348
- return settings.defaultMode !== 'bypassPermissions';
347
+ if (!settings || !settings.permissions) return null; // no settings = skip (not applicable)
348
+ return settings.permissions.defaultMode !== 'bypassPermissions';
349
349
  },
350
350
  impact: 'critical',
351
351
  rating: 5,
@@ -840,7 +840,7 @@ const TECHNIQUES = {
840
840
  name: 'Hooks use specific matchers (not catch-all)',
841
841
  check: (ctx) => {
842
842
  const settings = ctx.jsonFile('.claude/settings.local.json') || ctx.jsonFile('.claude/settings.json');
843
- if (!settings || !settings.hooks) return true; // no hooks = skip
843
+ if (!settings || !settings.hooks) return null; // no hooks = not applicable
844
844
  const hookStr = JSON.stringify(settings.hooks);
845
845
  // Check that hooks have matchers, not just catch-all
846
846
  return hookStr.includes('matcher');
@@ -852,28 +852,15 @@ const TECHNIQUES = {
852
852
  template: null
853
853
  },
854
854
 
855
- permissionsNotBypassed: {
856
- id: 2005,
857
- name: 'Default mode is not bypassPermissions',
858
- check: (ctx) => {
859
- const settings = ctx.jsonFile('.claude/settings.local.json') || ctx.jsonFile('.claude/settings.json');
860
- if (!settings || !settings.permissions) return true;
861
- return settings.permissions.defaultMode !== 'bypassPermissions';
862
- },
863
- impact: 'high',
864
- rating: 4,
865
- category: 'quality-deep',
866
- fix: 'bypassPermissions skips all safety checks. Use "default" or "auto" mode with targeted allow rules instead.',
867
- template: null
868
- },
855
+ // permissionsNotBypassed removed - duplicate of noBypassPermissions (#24)
869
856
 
870
857
  commandsUseArguments: {
871
858
  id: 2006,
872
859
  name: 'Commands use $ARGUMENTS for flexibility',
873
860
  check: (ctx) => {
874
- if (!ctx.hasDir('.claude/commands')) return true;
861
+ if (!ctx.hasDir('.claude/commands')) return null; // not applicable
875
862
  const files = ctx.dirFiles('.claude/commands');
876
- if (files.length === 0) return true;
863
+ if (files.length === 0) return null;
877
864
  // Check if at least one command uses $ARGUMENTS
878
865
  for (const f of files) {
879
866
  const content = ctx.fileContent(`.claude/commands/${f}`) || '';
@@ -892,9 +879,9 @@ const TECHNIQUES = {
892
879
  id: 2007,
893
880
  name: 'Agents have maxTurns limit',
894
881
  check: (ctx) => {
895
- if (!ctx.hasDir('.claude/agents')) return true;
882
+ if (!ctx.hasDir('.claude/agents')) return null;
896
883
  const files = ctx.dirFiles('.claude/agents');
897
- if (files.length === 0) return true;
884
+ if (files.length === 0) return null;
898
885
  for (const f of files) {
899
886
  const content = ctx.fileContent(`.claude/agents/${f}`) || '';
900
887
  if (!content.includes('maxTurns')) return false;