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 +1 -1
- package/src/audit.js +9 -6
- package/src/setup.js +163 -20
- package/src/techniques.js +8 -21
package/package.json
CHANGED
package/src/audit.js
CHANGED
|
@@ -61,17 +61,20 @@ async function audit(options) {
|
|
|
61
61
|
});
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
const
|
|
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 =
|
|
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}/${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
65
|
-
|
|
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 (
|
|
69
|
-
|
|
70
|
-
|
|
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 (
|
|
196
|
+
if (hasSrcComponents) {
|
|
74
197
|
nodes.push(addNode('Components', 'default'));
|
|
75
|
-
const parent = ids['
|
|
198
|
+
const parent = ids['App Router'] || ids['Pages'] || root;
|
|
76
199
|
edges.push(` ${parent} --> ${ids['Components']}`);
|
|
77
200
|
}
|
|
78
201
|
|
|
79
|
-
if (
|
|
80
|
-
nodes.push(addNode('
|
|
81
|
-
const parent = ids['
|
|
82
|
-
edges.push(` ${parent} --> ${ids['
|
|
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 (
|
|
208
|
+
if (hasSrcLib) {
|
|
86
209
|
nodes.push(addNode('lib/', 'default'));
|
|
87
|
-
const parent = ids['
|
|
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" |
|
|
417
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
861
|
+
if (!ctx.hasDir('.claude/commands')) return null; // not applicable
|
|
875
862
|
const files = ctx.dirFiles('.claude/commands');
|
|
876
|
-
if (files.length === 0) return
|
|
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
|
|
882
|
+
if (!ctx.hasDir('.claude/agents')) return null;
|
|
896
883
|
const files = ctx.dirFiles('.claude/agents');
|
|
897
|
-
if (files.length === 0) return
|
|
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;
|