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/analyze.js ADDED
@@ -0,0 +1,397 @@
1
+ /**
2
+ * Project scanner + recommendation layer for augment and suggest-only modes.
3
+ * Produces a structured repo-aware analysis without writing files.
4
+ */
5
+
6
+ const path = require('path');
7
+ const { audit } = require('./audit');
8
+ const { ProjectContext } = require('./context');
9
+ const { STACKS } = require('./techniques');
10
+
11
+ const COLORS = {
12
+ reset: '\x1b[0m',
13
+ bold: '\x1b[1m',
14
+ dim: '\x1b[2m',
15
+ red: '\x1b[31m',
16
+ green: '\x1b[32m',
17
+ yellow: '\x1b[33m',
18
+ blue: '\x1b[36m',
19
+ magenta: '\x1b[35m',
20
+ };
21
+
22
+ function c(text, color) {
23
+ return `${COLORS[color] || ''}${text}${COLORS.reset}`;
24
+ }
25
+
26
+ function escapeRegex(value) {
27
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
28
+ }
29
+
30
+ function extractTomlSection(content, sectionName) {
31
+ const pattern = new RegExp(`\\[${escapeRegex(sectionName)}\\]([\\s\\S]*?)(?:\\n\\s*\\[|$)`);
32
+ const match = content.match(pattern);
33
+ return match ? match[1] : null;
34
+ }
35
+
36
+ function extractTomlValue(sectionContent, key) {
37
+ if (!sectionContent) return null;
38
+ const pattern = new RegExp(`^\\s*${escapeRegex(key)}\\s*=\\s*["']([^"']+)["']`, 'm');
39
+ const match = sectionContent.match(pattern);
40
+ return match ? match[1].trim() : null;
41
+ }
42
+
43
+ function detectProjectMetadata(ctx) {
44
+ const pkg = ctx.jsonFile('package.json');
45
+ if (pkg && (pkg.name || pkg.description)) {
46
+ return {
47
+ name: pkg.name || path.basename(ctx.dir),
48
+ description: pkg.description || '',
49
+ };
50
+ }
51
+
52
+ const pyproject = ctx.fileContent('pyproject.toml') || '';
53
+ if (pyproject) {
54
+ const projectSection = extractTomlSection(pyproject, 'project');
55
+ const poetrySection = extractTomlSection(pyproject, 'tool.poetry');
56
+ const name = extractTomlValue(projectSection, 'name') ||
57
+ extractTomlValue(poetrySection, 'name');
58
+ const description = extractTomlValue(projectSection, 'description') ||
59
+ extractTomlValue(poetrySection, 'description');
60
+
61
+ if (name || description) {
62
+ return {
63
+ name: name || path.basename(ctx.dir),
64
+ description: description || '',
65
+ };
66
+ }
67
+ }
68
+
69
+ return {
70
+ name: path.basename(ctx.dir),
71
+ description: '',
72
+ };
73
+ }
74
+
75
+ function detectMainDirs(ctx) {
76
+ const candidates = [
77
+ 'src', 'lib', 'app', 'pages', 'components', 'api', 'routes', 'utils', 'helpers',
78
+ 'services', 'models', 'controllers', 'views', 'public', 'assets', 'config', 'tests',
79
+ 'test', '__tests__', 'spec', 'scripts', 'prisma', 'db', 'middleware', 'hooks',
80
+ 'agents', 'chains', 'workers', 'jobs', 'dags', 'macros', 'migrations',
81
+ 'src/components', 'src/app', 'src/pages', 'src/api', 'src/lib', 'src/hooks',
82
+ 'src/utils', 'src/services', 'src/models', 'src/middleware', 'src/agents',
83
+ 'src/chains', 'src/workers', 'src/jobs', 'src/app/api', 'app/api',
84
+ 'models/staging', 'models/marts'
85
+ ];
86
+
87
+ const dirs = [];
88
+ for (const dir of candidates) {
89
+ if (ctx.hasDir(dir)) {
90
+ dirs.push(dir);
91
+ }
92
+ }
93
+ return dirs;
94
+ }
95
+
96
+ function collectClaudeAssets(ctx) {
97
+ const sharedSettings = ctx.jsonFile('.claude/settings.json');
98
+ const localSettings = ctx.jsonFile('.claude/settings.local.json');
99
+ const settings = sharedSettings || localSettings || null;
100
+
101
+ const assetFiles = {
102
+ claudeMd: ctx.fileContent('CLAUDE.md') ? 'CLAUDE.md' : (ctx.fileContent('.claude/CLAUDE.md') ? '.claude/CLAUDE.md' : null),
103
+ settings: sharedSettings ? '.claude/settings.json' : (localSettings ? '.claude/settings.local.json' : null),
104
+ commands: ctx.hasDir('.claude/commands') ? ctx.dirFiles('.claude/commands') : [],
105
+ rules: ctx.hasDir('.claude/rules') ? ctx.dirFiles('.claude/rules') : [],
106
+ hooks: ctx.hasDir('.claude/hooks') ? ctx.dirFiles('.claude/hooks') : [],
107
+ agents: ctx.hasDir('.claude/agents') ? ctx.dirFiles('.claude/agents') : [],
108
+ skills: ctx.hasDir('.claude/skills') ? ctx.dirFiles('.claude/skills') : [],
109
+ };
110
+
111
+ return {
112
+ files: assetFiles,
113
+ counts: {
114
+ commands: assetFiles.commands.length,
115
+ rules: assetFiles.rules.length,
116
+ hooks: assetFiles.hooks.length,
117
+ agents: assetFiles.agents.length,
118
+ skills: assetFiles.skills.length,
119
+ mcpServers: settings && settings.mcpServers ? Object.keys(settings.mcpServers).length : 0,
120
+ },
121
+ permissions: settings && settings.permissions ? {
122
+ defaultMode: settings.permissions.defaultMode || null,
123
+ hasDenyRules: Array.isArray(settings.permissions.deny) && settings.permissions.deny.length > 0,
124
+ } : null,
125
+ settingsSource: assetFiles.settings,
126
+ };
127
+ }
128
+
129
+ function detectMaturity(assets) {
130
+ let score = 0;
131
+ if (assets.files.claudeMd) score += 2;
132
+ if (assets.files.settings) score += 1;
133
+ if (assets.counts.rules > 0) score += 1;
134
+ if (assets.counts.commands > 0) score += 1;
135
+ if (assets.counts.hooks > 0) score += 1;
136
+ if (assets.counts.agents > 0) score += 1;
137
+ if (assets.counts.skills > 0) score += 1;
138
+
139
+ if (score === 0) return 'none';
140
+ if (score <= 2) return 'starter';
141
+ if (score <= 5) return 'developing';
142
+ return 'mature';
143
+ }
144
+
145
+ function riskFromImpact(impact) {
146
+ if (impact === 'critical') return 'high';
147
+ if (impact === 'high') return 'medium';
148
+ return 'low';
149
+ }
150
+
151
+ function moduleFromCategory(category) {
152
+ const map = {
153
+ memory: 'CLAUDE.md',
154
+ quality: 'verification',
155
+ git: 'safety',
156
+ workflow: 'commands-agents-skills',
157
+ security: 'permissions',
158
+ automation: 'hooks',
159
+ design: 'design-rules',
160
+ devops: 'ci-devops',
161
+ hygiene: 'project-hygiene',
162
+ performance: 'context-management',
163
+ tools: 'mcp-tools',
164
+ prompting: 'prompt-structure',
165
+ features: 'modern-claude-features',
166
+ 'quality-deep': 'quality-deep',
167
+ };
168
+ return map[category] || category;
169
+ }
170
+
171
+ function toStrengths(results) {
172
+ return results
173
+ .filter(r => r.passed === true)
174
+ .sort((a, b) => {
175
+ const order = { critical: 3, high: 2, medium: 1, low: 0 };
176
+ return (order[b.impact] || 0) - (order[a.impact] || 0);
177
+ })
178
+ .slice(0, 6)
179
+ .map(r => ({
180
+ key: r.key,
181
+ name: r.name,
182
+ category: r.category,
183
+ note: `Already present and worth preserving: ${r.name}.`,
184
+ }));
185
+ }
186
+
187
+ function toGaps(results) {
188
+ return results
189
+ .filter(r => r.passed === false)
190
+ .sort((a, b) => {
191
+ const order = { critical: 3, high: 2, medium: 1, low: 0 };
192
+ return (order[b.impact] || 0) - (order[a.impact] || 0);
193
+ })
194
+ .slice(0, 8)
195
+ .map(r => ({
196
+ key: r.key,
197
+ name: r.name,
198
+ impact: r.impact,
199
+ category: r.category,
200
+ fix: r.fix,
201
+ }));
202
+ }
203
+
204
+ function toRecommendations(auditResult) {
205
+ const failed = auditResult.results
206
+ .filter(r => r.passed === false)
207
+ .sort((a, b) => {
208
+ const order = { critical: 3, high: 2, medium: 1, low: 0 };
209
+ return (order[b.impact] || 0) - (order[a.impact] || 0);
210
+ });
211
+
212
+ return failed.slice(0, 10).map((r, index) => ({
213
+ priority: index + 1,
214
+ key: r.key,
215
+ name: r.name,
216
+ impact: r.impact,
217
+ module: moduleFromCategory(r.category),
218
+ risk: riskFromImpact(r.impact),
219
+ why: r.fix,
220
+ }));
221
+ }
222
+
223
+ function buildOptionalModules(stacks, assets) {
224
+ const stackKeys = stacks.map(s => s.key);
225
+ const modules = [];
226
+
227
+ if (!assets.files.claudeMd) modules.push('CLAUDE.md baseline');
228
+ if (assets.counts.commands === 0) modules.push('Slash commands');
229
+ if (assets.counts.hooks === 0) modules.push('Hooks automation');
230
+ if (!assets.permissions || !assets.permissions.hasDenyRules) modules.push('Permission safety profile');
231
+ if (assets.counts.rules === 0) modules.push('Path-specific rules');
232
+ if (stackKeys.some(k => ['react', 'nextjs', 'vue', 'angular', 'svelte'].includes(k))) modules.push('Frontend pack');
233
+ if (stackKeys.some(k => ['node', 'python', 'django', 'fastapi', 'go', 'rust', 'java'].includes(k))) modules.push('Backend pack');
234
+ if (stackKeys.some(k => ['docker', 'terraform', 'kubernetes'].includes(k))) modules.push('DevOps pack');
235
+ if (assets.counts.agents === 0) modules.push('Specialized agents');
236
+
237
+ return [...new Set(modules)].slice(0, 8);
238
+ }
239
+
240
+ function buildRiskNotes(auditResult, assets, maturity) {
241
+ const notes = [];
242
+ if (!assets.files.claudeMd) notes.push('No CLAUDE.md exists yet, so Claude has no persistent project-specific guidance.');
243
+ if (assets.permissions && assets.permissions.defaultMode === 'bypassPermissions') {
244
+ notes.push('Current settings use bypassPermissions, which is risky for broader team adoption.');
245
+ }
246
+ if (!assets.permissions || !assets.permissions.hasDenyRules) {
247
+ notes.push('Permissions lack deny rules, so secret access and destructive commands are not strongly guarded.');
248
+ }
249
+ if (maturity === 'mature') {
250
+ notes.push('This repo already has meaningful Claude assets, so augment mode should preserve existing structure instead of overwriting it.');
251
+ }
252
+ if (auditResult.results.some(r => r.key === 'ciPipeline' && r.passed === false)) {
253
+ notes.push('Without CI enforcement, readiness can drift after setup.');
254
+ }
255
+ return notes.slice(0, 5);
256
+ }
257
+
258
+ function buildRolloutOrder(report) {
259
+ const steps = [];
260
+ if (!report.existingClaudeAssets.claudeMd) steps.push('Create a project-specific CLAUDE.md baseline');
261
+ if (report.gapsIdentified.some(g => g.category === 'security')) steps.push('Add safe settings and deny rules');
262
+ if (report.gapsIdentified.some(g => g.category === 'automation')) steps.push('Add hooks and automate verification');
263
+ if (report.gapsIdentified.some(g => g.category === 'workflow')) steps.push('Add commands, rules, and specialization modules');
264
+ if (report.gapsIdentified.some(g => g.category === 'devops')) steps.push('Connect CI threshold enforcement');
265
+ if (steps.length === 0) steps.push('Tighten quality-deep items and preserve the current setup');
266
+ return steps;
267
+ }
268
+
269
+ async function analyzeProject(options) {
270
+ const mode = options.mode || 'augment';
271
+ const ctx = new ProjectContext(options.dir);
272
+ const stacks = ctx.detectStacks(STACKS);
273
+ const auditResult = await audit({ ...options, silent: true });
274
+ const assets = collectClaudeAssets(ctx);
275
+ const metadata = detectProjectMetadata(ctx);
276
+ const maturity = detectMaturity(assets);
277
+ const mainDirs = detectMainDirs(ctx);
278
+
279
+ const report = {
280
+ mode,
281
+ writeBehavior: 'No files are written in this mode.',
282
+ projectSummary: {
283
+ name: metadata.name,
284
+ description: metadata.description,
285
+ directory: options.dir,
286
+ stacks: stacks.map(s => s.label),
287
+ maturity,
288
+ score: auditResult.score,
289
+ organicScore: auditResult.organicScore,
290
+ checkCount: auditResult.checkCount,
291
+ },
292
+ detectedArchitecture: {
293
+ repoType: stacks.length > 0 ? 'stack-detected repo' : 'generic repo',
294
+ mainDirectories: mainDirs,
295
+ stackSignals: stacks.map(s => s.key),
296
+ },
297
+ existingClaudeAssets: {
298
+ claudeMd: assets.files.claudeMd,
299
+ settings: assets.settingsSource,
300
+ commands: assets.files.commands,
301
+ rules: assets.files.rules,
302
+ hooks: assets.files.hooks,
303
+ agents: assets.files.agents,
304
+ skills: assets.files.skills,
305
+ mcpServers: assets.counts.mcpServers,
306
+ },
307
+ strengthsPreserved: toStrengths(auditResult.results),
308
+ gapsIdentified: toGaps(auditResult.results),
309
+ topNextActions: auditResult.quickWins,
310
+ recommendedImprovements: toRecommendations(auditResult),
311
+ riskNotes: buildRiskNotes(auditResult, assets, maturity),
312
+ optionalModules: buildOptionalModules(stacks, assets),
313
+ };
314
+
315
+ report.suggestedRolloutOrder = buildRolloutOrder(report);
316
+ return report;
317
+ }
318
+
319
+ function printAnalysis(report, options = {}) {
320
+ if (options.json) {
321
+ console.log(JSON.stringify(report, null, 2));
322
+ return;
323
+ }
324
+
325
+ const modeLabel = report.mode === 'suggest-only' ? 'suggest-only' : report.mode;
326
+ console.log('');
327
+ console.log(c(` claudex-setup ${modeLabel}`, 'bold'));
328
+ console.log(c(' ═══════════════════════════════════════', 'dim'));
329
+ console.log(c(` ${report.writeBehavior}`, 'dim'));
330
+ console.log('');
331
+
332
+ console.log(c(' Project Summary', 'blue'));
333
+ console.log(` ${report.projectSummary.name}${report.projectSummary.description ? ` — ${report.projectSummary.description}` : ''}`);
334
+ console.log(c(` Stack: ${report.projectSummary.stacks.join(', ') || 'Unknown'}`, 'dim'));
335
+ console.log(c(` Maturity: ${report.projectSummary.maturity} | Score: ${report.projectSummary.score}/100 | Organic: ${report.projectSummary.organicScore}/100`, 'dim'));
336
+ console.log('');
337
+
338
+ console.log(c(' Detected Architecture', 'blue'));
339
+ console.log(c(` Main directories: ${report.detectedArchitecture.mainDirectories.join(', ') || 'No strong structure detected yet'}`, 'dim'));
340
+ console.log('');
341
+
342
+ console.log(c(' Existing Claude Assets', 'blue'));
343
+ console.log(c(` CLAUDE.md: ${report.existingClaudeAssets.claudeMd || 'missing'}`, 'dim'));
344
+ console.log(c(` Settings: ${report.existingClaudeAssets.settings || 'missing'}`, 'dim'));
345
+ console.log(c(` Commands: ${report.existingClaudeAssets.commands.length} | Rules: ${report.existingClaudeAssets.rules.length} | Hooks: ${report.existingClaudeAssets.hooks.length} | Agents: ${report.existingClaudeAssets.agents.length} | Skills: ${report.existingClaudeAssets.skills.length}`, 'dim'));
346
+ console.log('');
347
+
348
+ if (report.strengthsPreserved.length > 0) {
349
+ console.log(c(' Strengths Preserved', 'green'));
350
+ for (const item of report.strengthsPreserved) {
351
+ console.log(` - ${item.name}`);
352
+ }
353
+ console.log('');
354
+ }
355
+
356
+ if (report.gapsIdentified.length > 0) {
357
+ console.log(c(' Gaps Identified', 'yellow'));
358
+ for (const item of report.gapsIdentified.slice(0, 5)) {
359
+ console.log(` - [${item.impact}] ${item.name}`);
360
+ console.log(c(` ${item.fix}`, 'dim'));
361
+ }
362
+ console.log('');
363
+ }
364
+
365
+ if (report.topNextActions.length > 0) {
366
+ console.log(c(' Top 5 Next Actions', 'magenta'));
367
+ report.topNextActions.slice(0, 5).forEach((item, index) => {
368
+ console.log(` ${index + 1}. ${item.name}`);
369
+ console.log(c(` ${item.fix}`, 'dim'));
370
+ });
371
+ console.log('');
372
+ }
373
+
374
+ if (report.riskNotes.length > 0) {
375
+ console.log(c(' Risk Notes', 'red'));
376
+ for (const note of report.riskNotes) {
377
+ console.log(` - ${note}`);
378
+ }
379
+ console.log('');
380
+ }
381
+
382
+ if (report.optionalModules.length > 0) {
383
+ console.log(c(' Optional Modules', 'blue'));
384
+ console.log(c(` ${report.optionalModules.join(' | ')}`, 'dim'));
385
+ console.log('');
386
+ }
387
+
388
+ if (report.suggestedRolloutOrder.length > 0) {
389
+ console.log(c(' Suggested Rollout Order', 'blue'));
390
+ report.suggestedRolloutOrder.forEach((item, index) => {
391
+ console.log(` ${index + 1}. ${item}`);
392
+ });
393
+ console.log('');
394
+ }
395
+ }
396
+
397
+ module.exports = { analyzeProject, printAnalysis };
package/src/audit.js CHANGED
@@ -29,17 +29,17 @@ function progressBar(score, max = 100, width = 20) {
29
29
  return colorize('█'.repeat(filled), color) + colorize('░'.repeat(empty), 'dim');
30
30
  }
31
31
 
32
- const EFFORT_ORDER = { critical: 0, high: 1, medium: 2 };
32
+ const IMPACT_ORDER = { critical: 3, high: 2, medium: 1, low: 0 };
33
33
 
34
34
  function getQuickWins(failed) {
35
- // Quick wins = medium impact items first (easiest), then high, sorted by name length (shorter = simpler)
36
- return [...failed]
35
+ const prioritized = failed.filter(r => !(r.category === 'hygiene' && r.impact === 'low'));
36
+ const pool = prioritized.length > 0 ? prioritized : failed;
37
+
38
+ return [...pool]
37
39
  .sort((a, b) => {
38
- const effortA = EFFORT_ORDER[a.impact] ?? 3;
39
- const effortB = EFFORT_ORDER[b.impact] ?? 3;
40
- // Prefer medium (easiest to fix), then high, then critical
41
- if (effortA !== effortB) return effortB - effortA;
42
- // Tie-break by fix length (shorter fix description = likely simpler)
40
+ const impactA = IMPACT_ORDER[a.impact] ?? 0;
41
+ const impactB = IMPACT_ORDER[b.impact] ?? 0;
42
+ if (impactA !== impactB) return impactB - impactA;
43
43
  return (a.fix || '').length - (b.fix || '').length;
44
44
  })
45
45
  .slice(0, 3);
@@ -90,10 +90,23 @@ async function audit(options) {
90
90
  const scaffoldedPassed = passed.filter(r => scaffoldedKeys.has(r.key));
91
91
  const organicEarned = organicPassed.reduce((sum, r) => sum + (weights[r.impact] || 5), 0);
92
92
  const organicScore = maxScore > 0 ? Math.round((organicEarned / maxScore) * 100) : 0;
93
+ const quickWins = getQuickWins(failed);
94
+ const result = {
95
+ score,
96
+ organicScore,
97
+ isScaffolded,
98
+ passed: passed.length,
99
+ failed: failed.length,
100
+ skipped: skipped.length,
101
+ checkCount: applicable.length,
102
+ stacks,
103
+ results,
104
+ quickWins: quickWins.map(({ key, name, impact, fix, category }) => ({ key, name, impact, category, fix })),
105
+ };
93
106
 
94
107
  // Silent mode: skip all output, just return result
95
108
  if (silent) {
96
- return { score, passed: passed.length, failed: failed.length, stacks, results };
109
+ return result;
97
110
  }
98
111
 
99
112
  if (options.json) {
@@ -101,15 +114,9 @@ async function audit(options) {
101
114
  console.log(JSON.stringify({
102
115
  version,
103
116
  timestamp: new Date().toISOString(),
104
- score,
105
- stacks,
106
- passed: passed.length,
107
- failed: failed.length,
108
- skipped: skipped.length,
109
- checkCount: applicable.length,
110
- results
117
+ ...result
111
118
  }, null, 2));
112
- return { score, passed: passed.length, failed: failed.length, stacks, results };
119
+ return result;
113
120
  }
114
121
 
115
122
  // Display results
@@ -173,8 +180,7 @@ async function audit(options) {
173
180
 
174
181
  // Quick wins
175
182
  if (failed.length > 0) {
176
- const quickWins = getQuickWins(failed);
177
- console.log(colorize(' ⚡ Quick wins (easiest fixes first)', 'magenta'));
183
+ console.log(colorize(' ⚡ Best next fixes', 'magenta'));
178
184
  for (let i = 0; i < quickWins.length; i++) {
179
185
  const r = quickWins[i];
180
186
  console.log(` ${i + 1}. ${colorize(r.name, 'bold')}`);
@@ -211,7 +217,6 @@ async function audit(options) {
211
217
  console.log('');
212
218
 
213
219
  // Send anonymous insights (opt-in, privacy-first, fire-and-forget)
214
- const result = { score, passed: passed.length, failed: failed.length, stacks, results };
215
220
  sendInsights(result);
216
221
 
217
222
  return result;
@@ -0,0 +1,176 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+
5
+ const { version } = require('../package.json');
6
+ const { audit } = require('./audit');
7
+ const { setup } = require('./setup');
8
+
9
+ function copyProject(sourceDir, targetDir) {
10
+ fs.mkdirSync(targetDir, { recursive: true });
11
+ const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
12
+ for (const entry of entries) {
13
+ if (entry.name === '.git' || entry.name === 'node_modules' || entry.name === '__pycache__') {
14
+ continue;
15
+ }
16
+ const from = path.join(sourceDir, entry.name);
17
+ const to = path.join(targetDir, entry.name);
18
+ if (entry.isDirectory()) {
19
+ copyProject(from, to);
20
+ } else if (entry.isFile()) {
21
+ fs.copyFileSync(from, to);
22
+ }
23
+ }
24
+ }
25
+
26
+ function summarizeAudit(result) {
27
+ return {
28
+ score: result.score,
29
+ organicScore: result.organicScore,
30
+ passed: result.passed,
31
+ failed: result.failed,
32
+ checkCount: result.checkCount,
33
+ quickWins: result.quickWins,
34
+ };
35
+ }
36
+
37
+ function buildExecutiveSummary(before, after) {
38
+ const scoreDelta = after.score - before.score;
39
+ const organicDelta = after.organicScore - before.organicScore;
40
+ return {
41
+ headline: scoreDelta > 0
42
+ ? `Benchmark improved readiness by ${scoreDelta} points without touching the original repo.`
43
+ : 'Benchmark did not improve the score in this run.',
44
+ scoreDelta,
45
+ organicDelta,
46
+ decisionGuidance: scoreDelta >= 20
47
+ ? 'Strong pilot candidate'
48
+ : scoreDelta >= 10
49
+ ? 'Promising but needs manual review'
50
+ : 'Use suggest-only mode before rollout',
51
+ };
52
+ }
53
+
54
+ function buildCaseStudy(before, after, applyResult) {
55
+ return {
56
+ initialState: `Baseline score ${before.score}/100, organic ${before.organicScore}/100.`,
57
+ chosenMode: 'benchmark-on-isolated-copy',
58
+ whatChanged: applyResult.writtenFiles,
59
+ whatWasPreserved: applyResult.preservedFiles,
60
+ measuredResults: {
61
+ scoreDelta: after.score - before.score,
62
+ organicDelta: after.organicScore - before.organicScore,
63
+ passedDelta: after.passed - before.passed,
64
+ },
65
+ };
66
+ }
67
+
68
+ function renderBenchmarkMarkdown(report) {
69
+ return [
70
+ '# Claudex Setup Benchmark Report',
71
+ '',
72
+ `- Generated by: ${report.generatedBy}`,
73
+ `- Created at: ${report.createdAt}`,
74
+ `- Source repo: ${report.directory}`,
75
+ '',
76
+ '## Methodology',
77
+ ...report.methodology.map(item => `- ${item}`),
78
+ '',
79
+ '## Before',
80
+ `- Score: ${report.before.score}/100`,
81
+ `- Organic score: ${report.before.organicScore}/100`,
82
+ `- Passing checks: ${report.before.passed}/${report.before.checkCount}`,
83
+ '',
84
+ '## After',
85
+ `- Score: ${report.after.score}/100`,
86
+ `- Organic score: ${report.after.organicScore}/100`,
87
+ `- Passing checks: ${report.after.passed}/${report.after.checkCount}`,
88
+ '',
89
+ '## Delta',
90
+ `- Score delta: ${report.delta.score}`,
91
+ `- Organic score delta: ${report.delta.organicScore}`,
92
+ `- Passed checks delta: ${report.delta.passed}`,
93
+ '',
94
+ '## Executive Summary',
95
+ `- ${report.executiveSummary.headline}`,
96
+ `- Recommendation: ${report.executiveSummary.decisionGuidance}`,
97
+ '',
98
+ '## Case Study',
99
+ `- Initial state: ${report.caseStudy.initialState}`,
100
+ `- Chosen mode: ${report.caseStudy.chosenMode}`,
101
+ `- What changed: ${report.caseStudy.whatChanged.join(', ') || 'none'}`,
102
+ `- What was preserved: ${report.caseStudy.whatWasPreserved.join(', ') || 'none'}`,
103
+ '',
104
+ ].join('\n');
105
+ }
106
+
107
+ async function runBenchmark(options) {
108
+ const before = await audit({ dir: options.dir, silent: true });
109
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'claudex-benchmark-'));
110
+ const sandboxDir = path.join(tempRoot, 'repo');
111
+
112
+ try {
113
+ copyProject(options.dir, sandboxDir);
114
+ const applyResult = await setup({ dir: sandboxDir, auto: true, silent: true });
115
+ const after = await audit({ dir: sandboxDir, silent: true });
116
+
117
+ return {
118
+ schemaVersion: 1,
119
+ generatedBy: `claudex-setup@${version}`,
120
+ createdAt: new Date().toISOString(),
121
+ directory: options.dir,
122
+ methodology: [
123
+ 'Run a baseline audit on the source repo.',
124
+ 'Copy the repo into a temporary isolated workspace.',
125
+ 'Apply starter-safe Claude artifacts only on the isolated copy.',
126
+ 'Re-run the audit and compare the results.',
127
+ ],
128
+ before: summarizeAudit(before),
129
+ after: summarizeAudit(after),
130
+ delta: {
131
+ score: after.score - before.score,
132
+ organicScore: after.organicScore - before.organicScore,
133
+ passed: after.passed - before.passed,
134
+ failed: after.failed - before.failed,
135
+ },
136
+ executiveSummary: buildExecutiveSummary(before, after),
137
+ caseStudy: buildCaseStudy(before, after, applyResult),
138
+ };
139
+ } finally {
140
+ fs.rmSync(tempRoot, { recursive: true, force: true });
141
+ }
142
+ }
143
+
144
+ function printBenchmark(report, options = {}) {
145
+ if (options.json) {
146
+ console.log(JSON.stringify(report, null, 2));
147
+ return;
148
+ }
149
+
150
+ console.log('');
151
+ console.log(' claudex-setup benchmark');
152
+ console.log(' ═══════════════════════════════════════');
153
+ console.log(' Runs in an isolated temp copy. Your current repo is not modified.');
154
+ console.log('');
155
+ console.log(` Before: ${report.before.score}/100 (organic ${report.before.organicScore}/100)`);
156
+ console.log(` After: ${report.after.score}/100 (organic ${report.after.organicScore}/100)`);
157
+ console.log(` Delta: score ${report.delta.score >= 0 ? '+' : ''}${report.delta.score}, organic ${report.delta.organicScore >= 0 ? '+' : ''}${report.delta.organicScore}`);
158
+ console.log('');
159
+ console.log(` ${report.executiveSummary.headline}`);
160
+ console.log(` Recommendation: ${report.executiveSummary.decisionGuidance}`);
161
+ console.log('');
162
+ }
163
+
164
+ function writeBenchmarkReport(report, outFile) {
165
+ fs.mkdirSync(path.dirname(outFile), { recursive: true });
166
+ const content = path.extname(outFile).toLowerCase() === '.md'
167
+ ? renderBenchmarkMarkdown(report)
168
+ : JSON.stringify(report, null, 2);
169
+ fs.writeFileSync(outFile, content, 'utf8');
170
+ }
171
+
172
+ module.exports = {
173
+ runBenchmark,
174
+ printBenchmark,
175
+ writeBenchmarkReport,
176
+ };