ccraft 1.0.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.
Files changed (40) hide show
  1. package/bin/claude-craft.js +85 -0
  2. package/package.json +39 -0
  3. package/src/commands/auth.js +43 -0
  4. package/src/commands/create.js +543 -0
  5. package/src/commands/install.js +480 -0
  6. package/src/commands/logout.js +24 -0
  7. package/src/commands/update.js +339 -0
  8. package/src/constants.js +299 -0
  9. package/src/generators/directories.js +30 -0
  10. package/src/generators/metadata.js +57 -0
  11. package/src/generators/security.js +39 -0
  12. package/src/prompts/gather.js +308 -0
  13. package/src/ui/brand.js +62 -0
  14. package/src/ui/cards.js +179 -0
  15. package/src/ui/format.js +55 -0
  16. package/src/ui/phase-header.js +20 -0
  17. package/src/ui/prompts.js +56 -0
  18. package/src/ui/tables.js +89 -0
  19. package/src/ui/tasks.js +258 -0
  20. package/src/ui/theme.js +83 -0
  21. package/src/utils/analysis-cache.js +519 -0
  22. package/src/utils/api-client.js +253 -0
  23. package/src/utils/api-file-writer.js +197 -0
  24. package/src/utils/bootstrap-runner.js +148 -0
  25. package/src/utils/claude-analyzer.js +255 -0
  26. package/src/utils/claude-optimizer.js +341 -0
  27. package/src/utils/claude-rewriter.js +553 -0
  28. package/src/utils/claude-scorer.js +101 -0
  29. package/src/utils/description-analyzer.js +116 -0
  30. package/src/utils/detect-project.js +1276 -0
  31. package/src/utils/existing-setup.js +341 -0
  32. package/src/utils/file-writer.js +64 -0
  33. package/src/utils/json-extract.js +56 -0
  34. package/src/utils/logger.js +27 -0
  35. package/src/utils/mcp-setup.js +461 -0
  36. package/src/utils/preflight.js +112 -0
  37. package/src/utils/prompt-api-key.js +59 -0
  38. package/src/utils/run-claude.js +152 -0
  39. package/src/utils/security.js +82 -0
  40. package/src/utils/toolkit-rule-generator.js +364 -0
@@ -0,0 +1,255 @@
1
+ import { isClaudeAvailable, runClaude } from './run-claude.js';
2
+ import { extractJsonObject } from './json-extract.js';
3
+ import { formatContextForAnalyzer } from './existing-setup.js';
4
+ import * as logger from './logger.js';
5
+
6
+ /**
7
+ * Claude-powered project analysis prompt.
8
+ *
9
+ * Inspired by SuperClaude's repo-index agent — uses 5-category parallel
10
+ * discovery to build a structured project index, identifies entry points,
11
+ * measures complexity, and detects architecture patterns.
12
+ */
13
+ const ANALYSIS_PROMPT = `You are a project analysis agent. Your job is to deeply understand this codebase
14
+ and produce a structured JSON index. Be thorough — this index will be used to configure
15
+ the development environment and will save thousands of tokens in future sessions.
16
+
17
+ ## Discovery Process (do all 5 in parallel where possible)
18
+
19
+ ### 1. CODE — Glob for source files
20
+ - Glob for: **/*.{js,jsx,ts,tsx,py,go,rs,java,kt,cs,rb,php,swift,html,cshtml,razor,css,scss,vue,svelte,dart,sql,hcl,tf}
21
+ - Count total files and estimate lines of code per language
22
+ - Calculate language distribution as percentages of the codebase (must sum to ~100)
23
+ - Identify entry points: main files, CLI entry, server start, index files
24
+
25
+ ### 2. CONFIG — Read manifest and config files
26
+ - Read: package.json, go.mod, Cargo.toml, pyproject.toml, *.csproj, pom.xml, build.gradle, composer.json, Gemfile
27
+ - Read: tsconfig.json, .babelrc, webpack.config.*, vite.config.*, next.config.*
28
+ - Read: Dockerfile, docker-compose.yml/yaml
29
+ - Extract: name, description, dependencies, scripts/commands
30
+
31
+ ### 3. DOCS — Check documentation
32
+ - Read first 80 lines of README.md or README
33
+ - Check for: docs/, doc/, wiki/, ARCHITECTURE.md, CONTRIBUTING.md, ADR/
34
+ - Summarize what the project does and its purpose
35
+
36
+ ### 4. TESTS — Identify test infrastructure
37
+ - Glob for: **/*.test.*, **/*.spec.*, **/test_*.*, tests/, __tests__/, spec/
38
+ - Identify test framework: jest, vitest, pytest, go test, cargo test, junit, xunit, rspec
39
+ - Estimate test coverage based on test file count vs source file count
40
+
41
+ ### 5. INFRASTRUCTURE — Check CI/CD and deployment
42
+ - Check: .github/workflows/, .gitlab-ci.yml, Jenkinsfile, .circleci/, bitbucket-pipelines.yml
43
+ - Check: Dockerfile, docker-compose.yml, k8s/, terraform/, pulumi/, serverless.yml
44
+ - Check for code style: .eslintrc*, .prettierrc*, .editorconfig, [tool.ruff], .golangci.yml, rustfmt.toml
45
+
46
+ ## Architecture Detection
47
+
48
+ Identify the architecture pattern from directory structure:
49
+ - **Layered/MVC**: controllers/, models/, views/, services/, routes/
50
+ - **Clean Architecture**: domain/, application/, infrastructure/, presentation/
51
+ - **Hexagonal**: ports/, adapters/, core/
52
+ - **Microservices**: multiple services/ with own configs, docker-compose with multiple services
53
+ - **Monorepo**: packages/, apps/, libs/ with workspace config
54
+ - **Serverless**: functions/, lambda/, serverless.yml
55
+ - **Event-driven**: events/, handlers/, queues/, subscribers/
56
+
57
+ ## Complexity Assessment
58
+
59
+ Estimate project complexity on a 0.0 to 1.0 scale based on:
60
+ - File count: <20 files = 0.1, 20-100 = 0.3, 100-500 = 0.5, 500-2000 = 0.7, 2000+ = 0.9
61
+ - Directory depth: max nesting level
62
+ - Dependency count: total production dependencies
63
+ - Language count: multiple languages = higher complexity
64
+ - Subproject count: monorepo with many packages = higher
65
+ Average these factors.
66
+
67
+ ## Output Format
68
+
69
+ Return ONLY a valid JSON object. No markdown fences, no explanation — just the JSON:
70
+ {
71
+ "name": "project-name",
72
+ "description": "One-two sentence description of what this project does and its purpose",
73
+ "projectType": "monorepo|microservice|monolith|library|cli",
74
+ "languages": ["TypeScript"],
75
+ "languageDistribution": { "TypeScript": 85, "CSS": 10, "HTML": 5 },
76
+ "frameworks": ["Next.js", "Express"],
77
+ "codeStyle": ["eslint", "prettier"],
78
+ "cicd": ["GitHub Actions"],
79
+ "architecture": "Layered MVC with service layer",
80
+ "complexity": 0.5,
81
+ "metrics": {
82
+ "totalFiles": 150,
83
+ "totalDirs": 25,
84
+ "maxDepth": 5,
85
+ "dependencyCount": 42,
86
+ "testFileCount": 30,
87
+ "sourceFileCount": 120,
88
+ "estimatedTestCoverage": "25%"
89
+ },
90
+ "entryPoints": [
91
+ { "type": "server", "path": "src/index.ts", "command": "npm start" },
92
+ { "type": "cli", "path": "bin/cli.js", "command": "node bin/cli.js" }
93
+ ],
94
+ "coreModules": [
95
+ { "path": "src/services/", "purpose": "Business logic layer", "key": true },
96
+ { "path": "src/routes/", "purpose": "API route definitions", "key": true }
97
+ ],
98
+ "subprojects": [
99
+ { "name": "web", "path": "apps/web", "languages": ["TypeScript"], "frameworks": ["Next.js"] }
100
+ ],
101
+ "buildCommands": {
102
+ "install": "npm install",
103
+ "dev": "npm run dev",
104
+ "build": "npm run build",
105
+ "test": "npm test",
106
+ "lint": "npm run lint"
107
+ },
108
+ "databases": ["postgresql", "redis"],
109
+ "testFramework": "jest",
110
+ "packageManager": "npm"
111
+ }
112
+
113
+ Rules:
114
+ - projectType must be one of: "monorepo", "microservice", "monolith", "library", "cli"
115
+ - Language names: "JavaScript", "TypeScript", "Python", "Go", "Rust", "Java", "Kotlin", "C#", "Ruby", "PHP", "Swift", "HTML", "CSS", "SQL", "Dart", "HCL"
116
+ - Framework names: "Next.js", "React", "Vue", "Angular", "Express", "Fastify", "Django", "Flask", "FastAPI", "Gin", "Echo", "Actix", "Spring Boot", "ASP.NET Core", "Razor", "Blazor", "Rails", "Laravel", "Tailwind CSS"
117
+ - Database names: "postgresql", "sql-server", "mysql", "sqlite", "mongodb", "redis", "cosmosdb", "dynamodb", "neo4j", "elasticsearch", "mariadb", "firestore", "cockroachdb". Detect from dependency manifests (NuGet PackageReference, npm packages, pip packages, etc.)
118
+ - languageDistribution values are integers representing percentages, must sum to approximately 100
119
+ - Only include languages with >= 2% of the codebase in languageDistribution. If exact LOC is unavailable, estimate from file counts
120
+ - complexity must be a float between 0.0 and 1.0
121
+ - subprojects should be [] if not a monorepo
122
+ - buildCommands values should be null if unknown
123
+ - Return ONLY the JSON, nothing else`;
124
+
125
+ /**
126
+ * Run Claude CLI to analyze the project directory.
127
+ * Returns { analysis, failReason } where analysis is the parsed object or null,
128
+ * and failReason is null on success or a string describing the failure.
129
+ *
130
+ * @param {string} targetDir - Project root
131
+ * @param {object|null} existingContext - Context from a previous Claude setup (optional)
132
+ */
133
+ export async function analyzeWithClaude(targetDir, existingContext = null) {
134
+ if (!isClaudeAvailable()) {
135
+ return { analysis: null, failReason: 'cli-unavailable' };
136
+ }
137
+
138
+ try {
139
+ const contextBlock = formatContextForAnalyzer(existingContext);
140
+ const fullPrompt = ANALYSIS_PROMPT + contextBlock;
141
+
142
+ const output = await runClaude([
143
+ '-p',
144
+ '--max-turns', '8',
145
+ '--allowedTools', 'Read,Glob,Grep,Bash(ls:*),Bash(find:*),Bash(head:*),Bash(wc:*),Bash(cat:*)',
146
+ ], { cwd: targetDir, stdinInput: fullPrompt });
147
+
148
+ // Extract the JSON object from response using brace-balanced parsing
149
+ const analysis = extractJsonObject(output);
150
+ if (!analysis) {
151
+ logger.warn('Claude analysis did not return valid JSON — falling back.');
152
+ return { analysis: null, failReason: 'invalid-json' };
153
+ }
154
+
155
+ if (!analysis.name && !analysis.languages) {
156
+ logger.warn('Claude analysis returned incomplete data — falling back.');
157
+ return { analysis: null, failReason: 'incomplete-data' };
158
+ }
159
+
160
+ return { analysis: sanitizeAnalysis(analysis), failReason: null };
161
+ } catch (err) {
162
+ if (err.killed) {
163
+ logger.warn('Claude analysis timed out — falling back to filesystem detection.');
164
+ return { analysis: null, failReason: 'timeout' };
165
+ } else {
166
+ logger.warn('Claude analysis failed — falling back to filesystem detection.');
167
+ return { analysis: null, failReason: 'error' };
168
+ }
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Sanitize and normalize a language distribution object.
174
+ * Returns null if the data is missing or invalid.
175
+ */
176
+ function sanitizeDistribution(dist) {
177
+ if (!dist || typeof dist !== 'object' || Array.isArray(dist)) return null;
178
+ const result = {};
179
+ let total = 0;
180
+ for (const [lang, pct] of Object.entries(dist)) {
181
+ if (typeof lang === 'string' && typeof pct === 'number' && pct > 0) {
182
+ const clamped = Math.max(0, Math.min(100, Math.round(pct)));
183
+ if (clamped >= 1) {
184
+ result[lang] = clamped;
185
+ total += clamped;
186
+ }
187
+ }
188
+ }
189
+ if (Object.keys(result).length === 0) return null;
190
+ // Normalize if total is wildly off (allow +/- 10% tolerance)
191
+ if (total > 0 && (total < 90 || total > 110)) {
192
+ for (const lang of Object.keys(result)) {
193
+ result[lang] = Math.round((result[lang] / total) * 100);
194
+ }
195
+ }
196
+ return result;
197
+ }
198
+
199
+ /**
200
+ * Sanitize and normalize Claude's analysis output.
201
+ */
202
+ export function sanitizeAnalysis(raw) {
203
+ const safeArray = (arr) => Array.isArray(arr) ? arr.filter((v) => typeof v === 'string') : [];
204
+ const safeStr = (val, fallback = '') => typeof val === 'string' ? val : fallback;
205
+ const safeNum = (val, fallback = 0) => typeof val === 'number' ? val : fallback;
206
+
207
+ return {
208
+ name: safeStr(raw.name),
209
+ description: safeStr(raw.description),
210
+ projectType: ['monorepo', 'microservice', 'monolith', 'library', 'cli'].includes(raw.projectType)
211
+ ? raw.projectType : 'monolith',
212
+ languages: safeArray(raw.languages),
213
+ languageDistribution: sanitizeDistribution(raw.languageDistribution),
214
+ frameworks: safeArray(raw.frameworks),
215
+ databases: safeArray(raw.databases),
216
+ codeStyle: safeArray(raw.codeStyle),
217
+ cicd: safeArray(raw.cicd),
218
+ architecture: safeStr(raw.architecture),
219
+ complexity: Math.max(0, Math.min(1, safeNum(raw.complexity, 0.5))),
220
+ metrics: raw.metrics && typeof raw.metrics === 'object' ? {
221
+ totalFiles: safeNum(raw.metrics.totalFiles),
222
+ totalDirs: safeNum(raw.metrics.totalDirs),
223
+ maxDepth: safeNum(raw.metrics.maxDepth),
224
+ dependencyCount: safeNum(raw.metrics.dependencyCount),
225
+ testFileCount: safeNum(raw.metrics.testFileCount),
226
+ sourceFileCount: safeNum(raw.metrics.sourceFileCount),
227
+ estimatedTestCoverage: safeStr(raw.metrics.estimatedTestCoverage, 'unknown'),
228
+ } : { totalFiles: 0, totalDirs: 0, maxDepth: 0, dependencyCount: 0, testFileCount: 0, sourceFileCount: 0, estimatedTestCoverage: 'unknown' },
229
+ entryPoints: Array.isArray(raw.entryPoints)
230
+ ? raw.entryPoints.map((e) => ({ type: safeStr(e.type), path: safeStr(e.path), command: safeStr(e.command) }))
231
+ : [],
232
+ coreModules: Array.isArray(raw.coreModules)
233
+ ? raw.coreModules.map((m) => ({ path: safeStr(m.path), purpose: safeStr(m.purpose), key: !!m.key }))
234
+ : [],
235
+ subprojects: Array.isArray(raw.subprojects)
236
+ ? raw.subprojects.map((s) => ({
237
+ name: safeStr(s.name),
238
+ path: safeStr(s.path),
239
+ languages: safeArray(s.languages),
240
+ frameworks: safeArray(s.frameworks),
241
+ }))
242
+ : [],
243
+ buildCommands: raw.buildCommands && typeof raw.buildCommands === 'object'
244
+ ? {
245
+ install: raw.buildCommands.install || null,
246
+ dev: raw.buildCommands.dev || null,
247
+ build: raw.buildCommands.build || null,
248
+ test: raw.buildCommands.test || null,
249
+ lint: raw.buildCommands.lint || null,
250
+ }
251
+ : { install: null, dev: null, build: null, test: null, lint: null },
252
+ testFramework: safeStr(raw.testFramework),
253
+ packageManager: safeStr(raw.packageManager),
254
+ };
255
+ }
@@ -0,0 +1,341 @@
1
+ /**
2
+ * Lightweight settings post-processor.
3
+ *
4
+ * Replaces generic multi-tool references in installed .claude/ config files
5
+ * with the actual project commands detected from the analysis cache or filesystem.
6
+ *
7
+ * Pure filesystem operations — no Claude CLI needed. Runs in <1s.
8
+ */
9
+ import { readFileSync, writeFileSync, existsSync, readdirSync } from 'fs';
10
+ import { join, relative } from 'path';
11
+ import { readAnalysisCache } from './analysis-cache.js';
12
+ import * as logger from './logger.js';
13
+
14
+ // ── Public API ──────────────────────────────────────────────────────────
15
+
16
+ /**
17
+ * Replace generic command references with project-specific ones.
18
+ *
19
+ * @param {string} targetDir - Project root (cwd)
20
+ * @returns {{ status: string, applied: number, replacements: string[] }}
21
+ */
22
+ export function optimizeSettings(targetDir) {
23
+ const commands = _resolveCommands(targetDir);
24
+
25
+ if (!commands) {
26
+ return { status: 'no-commands', applied: 0, replacements: [] };
27
+ }
28
+
29
+ const claudeDir = join(targetDir, '.claude');
30
+ if (!existsSync(claudeDir)) {
31
+ return { status: 'ok', applied: 0, replacements: [] };
32
+ }
33
+
34
+ const mdFiles = _collectMarkdownFiles(claudeDir);
35
+ if (mdFiles.length === 0) {
36
+ return { status: 'ok', applied: 0, replacements: [] };
37
+ }
38
+
39
+ let applied = 0;
40
+ const labels = new Set();
41
+
42
+ for (const filePath of mdFiles) {
43
+ let content;
44
+ try {
45
+ content = readFileSync(filePath, 'utf8');
46
+ } catch {
47
+ continue;
48
+ }
49
+
50
+ const { text, changed } = _applyReplacements(content, commands);
51
+
52
+ if (text !== content) {
53
+ writeFileSync(filePath, text, 'utf8');
54
+ applied++;
55
+ for (const c of changed) labels.add(c);
56
+ logger.debug(`Optimized ${relative(targetDir, filePath)}`);
57
+ }
58
+ }
59
+
60
+ return { status: 'ok', applied, replacements: [...labels] };
61
+ }
62
+
63
+ // ── Command resolution ──────────────────────────────────────────────────
64
+
65
+ /**
66
+ * Build a commands object from the analysis cache or direct filesystem reads.
67
+ * Returns null if no meaningful commands could be detected.
68
+ */
69
+ function _resolveCommands(targetDir) {
70
+ // 1. Try the analysis cache (Claude-extracted, most accurate)
71
+ const cache = readAnalysisCache(targetDir);
72
+ const pi = cache?.projectInfo;
73
+
74
+ if (pi?.buildCommands) {
75
+ const bc = pi.buildCommands;
76
+ if (bc.test || bc.build || bc.lint) {
77
+ return {
78
+ test: bc.test || null,
79
+ build: bc.build || null,
80
+ lint: bc.lint || null,
81
+ dev: bc.dev || null,
82
+ audit: _deriveAuditCommand(pi.packageManager, pi.languages),
83
+ packageManager: pi.packageManager || null,
84
+ testFramework: pi.testFramework || null,
85
+ languages: pi.languages || [],
86
+ };
87
+ }
88
+ }
89
+
90
+ // 2. Fallback: read config files directly from the project
91
+ return _detectFromFilesystem(targetDir);
92
+ }
93
+
94
+ /**
95
+ * Detect commands by reading package.json, Cargo.toml, go.mod, etc.
96
+ */
97
+ function _detectFromFilesystem(targetDir) {
98
+ const result = {
99
+ test: null,
100
+ build: null,
101
+ lint: null,
102
+ dev: null,
103
+ audit: null,
104
+ packageManager: null,
105
+ testFramework: null,
106
+ languages: [],
107
+ };
108
+
109
+ // Detect JS/TS package manager
110
+ if (existsSync(join(targetDir, 'pnpm-lock.yaml'))) result.packageManager = 'pnpm';
111
+ else if (existsSync(join(targetDir, 'yarn.lock'))) result.packageManager = 'yarn';
112
+ else if (existsSync(join(targetDir, 'bun.lockb')) || existsSync(join(targetDir, 'bun.lock'))) result.packageManager = 'bun';
113
+ else if (existsSync(join(targetDir, 'package-lock.json'))) result.packageManager = 'npm';
114
+
115
+ // ── Node.js ───────────────────────────────────────────────────────
116
+ const pkgPath = join(targetDir, 'package.json');
117
+ if (existsSync(pkgPath)) {
118
+ try {
119
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
120
+ const scripts = pkg.scripts || {};
121
+ const pm = result.packageManager || 'npm';
122
+ const run = pm === 'npm' ? 'npm run' : pm;
123
+
124
+ if (scripts.test && !scripts.test.includes('no test specified')) {
125
+ result.test = pm === 'npm' ? 'npm test' : `${pm} test`;
126
+ }
127
+ if (scripts.build) result.build = `${run} build`;
128
+ if (scripts.lint) result.lint = `${run} lint`;
129
+ if (scripts.dev) result.dev = `${run} dev`;
130
+
131
+ // Test framework from devDependencies
132
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
133
+ if (deps?.vitest) result.testFramework = 'Vitest';
134
+ else if (deps?.jest) result.testFramework = 'Jest';
135
+ else if (deps?.mocha) result.testFramework = 'Mocha';
136
+
137
+ result.languages.push('javascript');
138
+ } catch {
139
+ // Ignore parse errors
140
+ }
141
+ }
142
+
143
+ // ── Python ────────────────────────────────────────────────────────
144
+ const hasPython =
145
+ existsSync(join(targetDir, 'pyproject.toml')) ||
146
+ existsSync(join(targetDir, 'setup.py')) ||
147
+ existsSync(join(targetDir, 'requirements.txt'));
148
+ if (hasPython) {
149
+ result.languages.push('python');
150
+ if (
151
+ existsSync(join(targetDir, 'conftest.py')) ||
152
+ existsSync(join(targetDir, 'pytest.ini')) ||
153
+ existsSync(join(targetDir, 'setup.cfg'))
154
+ ) {
155
+ result.test = result.test || 'pytest';
156
+ result.testFramework = result.testFramework || 'pytest';
157
+ }
158
+ result.audit = result.audit || 'pip audit';
159
+ }
160
+
161
+ // ── Rust ──────────────────────────────────────────────────────────
162
+ if (existsSync(join(targetDir, 'Cargo.toml'))) {
163
+ result.languages.push('rust');
164
+ result.test = result.test || 'cargo test';
165
+ result.build = result.build || 'cargo build';
166
+ result.lint = result.lint || 'cargo clippy';
167
+ result.audit = result.audit || 'cargo audit';
168
+ }
169
+
170
+ // ── Go ────────────────────────────────────────────────────────────
171
+ if (existsSync(join(targetDir, 'go.mod'))) {
172
+ result.languages.push('go');
173
+ result.test = result.test || 'go test ./...';
174
+ result.build = result.build || 'go build ./...';
175
+ result.lint = result.lint || 'golangci-lint run';
176
+ result.audit = result.audit || 'govulncheck ./...';
177
+ }
178
+
179
+ // Derive audit for JS if not already set
180
+ if (!result.audit && result.packageManager) {
181
+ result.audit = `${result.packageManager} audit`;
182
+ }
183
+
184
+ // Return null if nothing useful detected
185
+ if (!result.test && !result.build && !result.lint) return null;
186
+ return result;
187
+ }
188
+
189
+ /**
190
+ * Derive the audit command from package manager / languages.
191
+ */
192
+ function _deriveAuditCommand(packageManager, languages) {
193
+ if (packageManager && ['npm', 'pnpm', 'yarn'].includes(packageManager)) {
194
+ return `${packageManager} audit`;
195
+ }
196
+ if (languages?.includes('python') || languages?.includes('Python')) return 'pip audit';
197
+ if (languages?.includes('rust') || languages?.includes('Rust')) return 'cargo audit';
198
+ if (languages?.includes('go') || languages?.includes('Go')) return 'govulncheck ./...';
199
+ return null;
200
+ }
201
+
202
+ // ── Replacement engine ──────────────────────────────────────────────────
203
+
204
+ /**
205
+ * Apply all replacement patterns to content.
206
+ * Returns { text, changed: string[] }.
207
+ */
208
+ function _applyReplacements(content, commands) {
209
+ let text = content;
210
+ const changed = [];
211
+
212
+ // 1. Backtick slash lists: `cmd1` / `cmd2` / `cmd3` [/ etc.]
213
+ // → narrow to the single matching command
214
+ text = text.replace(
215
+ /`([^`\n]+)`(?:\s*\/\s*`[^`\n]+`)+(?:\s*\/?\s*etc\.)?/g,
216
+ (match) => {
217
+ const cmds = [...match.matchAll(/`([^`\n]+)`/g)].map((m) => m[1]);
218
+ const category = _classifyAll(cmds);
219
+ const replacement = category && commands[category];
220
+ if (replacement) {
221
+ changed.push(`${category}-commands`);
222
+ return `\`${replacement}\``;
223
+ }
224
+ return match;
225
+ },
226
+ );
227
+
228
+ // 2. Parenthesized test-framework lists: (Jest, Vitest, Mocha, etc.)
229
+ if (commands.testFramework) {
230
+ text = text.replace(
231
+ /\((?:(?:Jest|Vitest|Mocha|pytest|unittest)(?:,\s*)?){2,}(?:,?\s*etc\.?)?\)/gi,
232
+ () => {
233
+ changed.push('test-framework');
234
+ return `(${commands.testFramework})`;
235
+ },
236
+ );
237
+ }
238
+
239
+ // 3. Parenthesized linter lists: (ESLint, Biome, etc.)
240
+ if (commands.lint) {
241
+ text = text.replace(
242
+ /\((?:(?:ESLint|Biome|Prettier|ruff|pylint)(?:,\s*)?){2,}(?:,?\s*etc\.?)?\)/gi,
243
+ (match) => {
244
+ // Extract the tool name from the lint command
245
+ const tool = _extractToolName(commands.lint);
246
+ if (tool) {
247
+ changed.push('linter-tool');
248
+ return `(${tool})`;
249
+ }
250
+ return match;
251
+ },
252
+ );
253
+ }
254
+
255
+ // 4. Wrong package manager in inline backtick commands
256
+ if (commands.packageManager && commands.packageManager !== 'npm') {
257
+ const pm = commands.packageManager;
258
+ const before = text;
259
+ text = text.replace(/`npm (run \w+|test|install|audit)`/g, (match, sub) => {
260
+ return `\`${pm} ${sub}\``;
261
+ });
262
+ if (text !== before) {
263
+ changed.push(`npm → ${pm}`);
264
+ }
265
+ }
266
+
267
+ return { text, changed };
268
+ }
269
+
270
+ // ── Classification ──────────────────────────────────────────────────────
271
+
272
+ const CATEGORY_PATTERNS = {
273
+ test: /\btest\b|\bjest\b|\bvitest\b|\bpytest\b|\bmocha\b/i,
274
+ lint: /\blint\b|\beslint\b|\bruff\b|\bclippy\b|\bpylint\b|\bbiome\s*(check|lint)\b/i,
275
+ audit: /\baudit\b|\bsafety\b|\bgovulncheck\b|\bsnyk\b/i,
276
+ build: /\bbuild\b|\bcompile\b|\btsc\b/i,
277
+ };
278
+
279
+ /**
280
+ * Return the shared category of all commands, or null if mixed/unknown.
281
+ */
282
+ function _classifyAll(cmds) {
283
+ let shared = null;
284
+ for (const cmd of cmds) {
285
+ const cat = _classify(cmd);
286
+ if (!cat) return null;
287
+ if (shared && cat !== shared) return null;
288
+ shared = cat;
289
+ }
290
+ return shared;
291
+ }
292
+
293
+ function _classify(cmd) {
294
+ for (const [cat, re] of Object.entries(CATEGORY_PATTERNS)) {
295
+ if (re.test(cmd)) return cat;
296
+ }
297
+ return null;
298
+ }
299
+
300
+ /**
301
+ * Extract a human-readable tool name from a command string.
302
+ * e.g. "npm run lint" → "ESLint" (heuristic), "ruff check" → "ruff"
303
+ */
304
+ function _extractToolName(lintCmd) {
305
+ const lower = lintCmd.toLowerCase();
306
+ if (lower.includes('eslint') || lower.includes('npm run lint') || lower.includes('npx eslint')) return 'ESLint';
307
+ if (lower.includes('biome')) return 'Biome';
308
+ if (lower.includes('ruff')) return 'ruff';
309
+ if (lower.includes('pylint')) return 'pylint';
310
+ if (lower.includes('clippy')) return 'clippy';
311
+ if (lower.includes('golangci')) return 'golangci-lint';
312
+ return null;
313
+ }
314
+
315
+ // ── File collection ─────────────────────────────────────────────────────
316
+
317
+ /**
318
+ * Recursively find all .md files under a directory.
319
+ * Skips the temp dir and node_modules.
320
+ */
321
+ function _collectMarkdownFiles(dir) {
322
+ const results = [];
323
+ let entries;
324
+ try {
325
+ entries = readdirSync(dir, { withFileTypes: true });
326
+ } catch {
327
+ return results;
328
+ }
329
+
330
+ for (const entry of entries) {
331
+ const fullPath = join(dir, entry.name);
332
+ if (entry.isDirectory()) {
333
+ if (entry.name === '.claude-craft-temp' || entry.name === 'node_modules') continue;
334
+ results.push(..._collectMarkdownFiles(fullPath));
335
+ } else if (entry.name.endsWith('.md')) {
336
+ results.push(fullPath);
337
+ }
338
+ }
339
+
340
+ return results;
341
+ }