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.
- package/bin/claude-craft.js +85 -0
- package/package.json +39 -0
- package/src/commands/auth.js +43 -0
- package/src/commands/create.js +543 -0
- package/src/commands/install.js +480 -0
- package/src/commands/logout.js +24 -0
- package/src/commands/update.js +339 -0
- package/src/constants.js +299 -0
- package/src/generators/directories.js +30 -0
- package/src/generators/metadata.js +57 -0
- package/src/generators/security.js +39 -0
- package/src/prompts/gather.js +308 -0
- package/src/ui/brand.js +62 -0
- package/src/ui/cards.js +179 -0
- package/src/ui/format.js +55 -0
- package/src/ui/phase-header.js +20 -0
- package/src/ui/prompts.js +56 -0
- package/src/ui/tables.js +89 -0
- package/src/ui/tasks.js +258 -0
- package/src/ui/theme.js +83 -0
- package/src/utils/analysis-cache.js +519 -0
- package/src/utils/api-client.js +253 -0
- package/src/utils/api-file-writer.js +197 -0
- package/src/utils/bootstrap-runner.js +148 -0
- package/src/utils/claude-analyzer.js +255 -0
- package/src/utils/claude-optimizer.js +341 -0
- package/src/utils/claude-rewriter.js +553 -0
- package/src/utils/claude-scorer.js +101 -0
- package/src/utils/description-analyzer.js +116 -0
- package/src/utils/detect-project.js +1276 -0
- package/src/utils/existing-setup.js +341 -0
- package/src/utils/file-writer.js +64 -0
- package/src/utils/json-extract.js +56 -0
- package/src/utils/logger.js +27 -0
- package/src/utils/mcp-setup.js +461 -0
- package/src/utils/preflight.js +112 -0
- package/src/utils/prompt-api-key.js +59 -0
- package/src/utils/run-claude.js +152 -0
- package/src/utils/security.js +82 -0
- 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
|
+
}
|