claudenv 1.0.1
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/LICENSE +21 -0
- package/README.md +156 -0
- package/bin/cli.js +205 -0
- package/package.json +60 -0
- package/scaffold/.claude/agents/doc-analyzer.md +70 -0
- package/scaffold/.claude/commands/init-docs.md +69 -0
- package/scaffold/.claude/commands/update-docs.md +49 -0
- package/scaffold/.claude/commands/validate-docs.md +40 -0
- package/scaffold/.claude/skills/doc-generator/SKILL.md +56 -0
- package/scaffold/.claude/skills/doc-generator/scripts/validate.sh +108 -0
- package/scaffold/.claude/skills/doc-generator/templates/detection-patterns.md +76 -0
- package/src/constants.js +237 -0
- package/src/detector.js +346 -0
- package/src/generator.js +259 -0
- package/src/index.js +5 -0
- package/src/prompts.js +267 -0
- package/src/validator.js +206 -0
- package/templates/claude-md.ejs +49 -0
- package/templates/rules-code-style.ejs +64 -0
- package/templates/rules-testing.ejs +64 -0
- package/templates/rules-workflow.ejs +49 -0
- package/templates/settings-json.ejs +28 -0
- package/templates/state-md.ejs +23 -0
package/src/detector.js
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join, basename } from 'node:path';
|
|
3
|
+
import { glob } from 'glob';
|
|
4
|
+
import { parse as parseToml } from 'smol-toml';
|
|
5
|
+
import {
|
|
6
|
+
MANIFEST_MAP,
|
|
7
|
+
TYPESCRIPT_INDICATORS,
|
|
8
|
+
FRAMEWORK_MAP,
|
|
9
|
+
PACKAGE_MANAGER_MAP,
|
|
10
|
+
TEST_FRAMEWORK_MAP,
|
|
11
|
+
TEST_DEPENDENCY_MAP,
|
|
12
|
+
CI_PATTERNS,
|
|
13
|
+
LINTER_MAP,
|
|
14
|
+
FORMATTER_MAP,
|
|
15
|
+
MONOREPO_MAP,
|
|
16
|
+
INFRA_MAP,
|
|
17
|
+
SUGGESTED_COMMANDS,
|
|
18
|
+
} from './constants.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Detect the tech stack of a project by scanning its files.
|
|
22
|
+
* @param {string} projectDir - Absolute path to the project root
|
|
23
|
+
* @returns {Promise<object>} Detected tech stack
|
|
24
|
+
*/
|
|
25
|
+
export async function detectTechStack(projectDir) {
|
|
26
|
+
const files = await glob('**/*', {
|
|
27
|
+
cwd: projectDir,
|
|
28
|
+
maxDepth: 3,
|
|
29
|
+
dot: true,
|
|
30
|
+
nodir: true,
|
|
31
|
+
ignore: ['**/node_modules/**', '**/.git/**', '**/vendor/**', '**/__pycache__/**', '**/target/**'],
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const fileSet = new Set(files);
|
|
35
|
+
const fileNames = files.map((f) => basename(f));
|
|
36
|
+
const fileNameSet = new Set(fileNames);
|
|
37
|
+
|
|
38
|
+
const result = {
|
|
39
|
+
language: null,
|
|
40
|
+
runtime: null,
|
|
41
|
+
framework: null,
|
|
42
|
+
packageManager: null,
|
|
43
|
+
buildTool: null,
|
|
44
|
+
testFramework: null,
|
|
45
|
+
linter: null,
|
|
46
|
+
formatter: null,
|
|
47
|
+
ci: null,
|
|
48
|
+
containerized: false,
|
|
49
|
+
monorepo: null,
|
|
50
|
+
suggestedTestCmd: null,
|
|
51
|
+
suggestedBuildCmd: null,
|
|
52
|
+
suggestedDevCmd: null,
|
|
53
|
+
detectedFiles: {
|
|
54
|
+
manifests: [],
|
|
55
|
+
configs: [],
|
|
56
|
+
ci: [],
|
|
57
|
+
infra: [],
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Phase 1: Detect language/runtime from manifest files
|
|
62
|
+
detectLanguage(fileSet, fileNameSet, files, result);
|
|
63
|
+
|
|
64
|
+
// Phase 2: Check for TypeScript
|
|
65
|
+
if (result.language === 'javascript') {
|
|
66
|
+
for (const indicator of TYPESCRIPT_INDICATORS) {
|
|
67
|
+
if (fileNameSet.has(indicator) || fileSet.has(indicator)) {
|
|
68
|
+
result.language = 'typescript';
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Phase 3: Detect framework from config files
|
|
75
|
+
detectFramework(fileSet, fileNameSet, files, result);
|
|
76
|
+
|
|
77
|
+
// Phase 4: Detect package manager from lockfiles
|
|
78
|
+
detectPackageManager(fileNameSet, result);
|
|
79
|
+
|
|
80
|
+
// Phase 5: Detect test framework
|
|
81
|
+
await detectTestFramework(projectDir, fileSet, fileNameSet, result);
|
|
82
|
+
|
|
83
|
+
// Phase 6: Detect CI/CD
|
|
84
|
+
detectCI(fileSet, files, result);
|
|
85
|
+
|
|
86
|
+
// Phase 7: Detect linter and formatter
|
|
87
|
+
detectLinter(fileNameSet, result);
|
|
88
|
+
detectFormatter(fileNameSet, result);
|
|
89
|
+
|
|
90
|
+
// Phase 8: Detect monorepo tooling
|
|
91
|
+
detectMonorepo(fileNameSet, result);
|
|
92
|
+
|
|
93
|
+
// Phase 9: Detect infrastructure
|
|
94
|
+
detectInfra(fileSet, fileNameSet, files, result);
|
|
95
|
+
|
|
96
|
+
// Phase 10: Infer build tool
|
|
97
|
+
inferBuildTool(result);
|
|
98
|
+
|
|
99
|
+
// Phase 11: Suggest commands
|
|
100
|
+
suggestCommands(result);
|
|
101
|
+
|
|
102
|
+
// Phase 12: Parse manifest for deeper insights
|
|
103
|
+
await parseManifestDetails(projectDir, fileSet, result);
|
|
104
|
+
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function detectLanguage(fileSet, fileNameSet, files, result) {
|
|
109
|
+
for (const [filename, info] of Object.entries(MANIFEST_MAP)) {
|
|
110
|
+
if (filename.startsWith('*')) {
|
|
111
|
+
// Glob pattern like *.csproj
|
|
112
|
+
const ext = filename.slice(1);
|
|
113
|
+
if (files.some((f) => f.endsWith(ext))) {
|
|
114
|
+
result.language = info.language;
|
|
115
|
+
result.runtime = info.runtime;
|
|
116
|
+
result.detectedFiles.manifests.push(files.find((f) => f.endsWith(ext)));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
} else if (fileNameSet.has(filename) || fileSet.has(filename)) {
|
|
120
|
+
result.language = info.language;
|
|
121
|
+
result.runtime = info.runtime;
|
|
122
|
+
const match = files.find((f) => f === filename || basename(f) === filename);
|
|
123
|
+
if (match) result.detectedFiles.manifests.push(match);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function detectFramework(fileSet, fileNameSet, files, result) {
|
|
130
|
+
for (const [filename, framework] of Object.entries(FRAMEWORK_MAP)) {
|
|
131
|
+
if (fileNameSet.has(filename) || fileSet.has(filename)) {
|
|
132
|
+
result.framework = framework;
|
|
133
|
+
const match = files.find((f) => f === filename || basename(f) === filename);
|
|
134
|
+
if (match) result.detectedFiles.configs.push(match);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function detectPackageManager(fileNameSet, result) {
|
|
141
|
+
for (const [lockfile, pm] of Object.entries(PACKAGE_MANAGER_MAP)) {
|
|
142
|
+
if (fileNameSet.has(lockfile)) {
|
|
143
|
+
result.packageManager = pm;
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Fallback: if we have a Node project but no lockfile, assume npm
|
|
148
|
+
if (result.runtime === 'node' && !result.packageManager) {
|
|
149
|
+
result.packageManager = 'npm';
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function detectTestFramework(projectDir, fileSet, fileNameSet, result) {
|
|
154
|
+
// Check config files first
|
|
155
|
+
for (const [filename, framework] of Object.entries(TEST_FRAMEWORK_MAP)) {
|
|
156
|
+
if (fileNameSet.has(filename) || fileSet.has(filename)) {
|
|
157
|
+
result.testFramework = framework;
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Check package.json dependencies
|
|
163
|
+
if (fileSet.has('package.json')) {
|
|
164
|
+
try {
|
|
165
|
+
const pkgRaw = await readFile(join(projectDir, 'package.json'), 'utf-8');
|
|
166
|
+
const pkg = JSON.parse(pkgRaw);
|
|
167
|
+
const allDeps = {
|
|
168
|
+
...pkg.dependencies,
|
|
169
|
+
...pkg.devDependencies,
|
|
170
|
+
};
|
|
171
|
+
for (const [dep, framework] of Object.entries(TEST_DEPENDENCY_MAP)) {
|
|
172
|
+
if (allDeps[dep]) {
|
|
173
|
+
result.testFramework = framework;
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} catch {
|
|
178
|
+
// Ignore parse errors
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function detectCI(fileSet, files, result) {
|
|
184
|
+
for (const pattern of CI_PATTERNS) {
|
|
185
|
+
const matching = files.filter((f) => {
|
|
186
|
+
if (pattern.glob.includes('*')) {
|
|
187
|
+
const prefix = pattern.glob.split('*')[0];
|
|
188
|
+
const suffix = pattern.glob.split('*').pop();
|
|
189
|
+
return f.startsWith(prefix) && f.endsWith(suffix);
|
|
190
|
+
}
|
|
191
|
+
return f === pattern.glob;
|
|
192
|
+
});
|
|
193
|
+
if (matching.length > 0) {
|
|
194
|
+
result.ci = pattern.name;
|
|
195
|
+
result.detectedFiles.ci.push(...matching);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function detectLinter(fileNameSet, result) {
|
|
202
|
+
for (const [filename, linter] of Object.entries(LINTER_MAP)) {
|
|
203
|
+
if (fileNameSet.has(filename)) {
|
|
204
|
+
result.linter = linter;
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function detectFormatter(fileNameSet, result) {
|
|
211
|
+
for (const [filename, fmt] of Object.entries(FORMATTER_MAP)) {
|
|
212
|
+
if (fmt && fileNameSet.has(filename)) {
|
|
213
|
+
result.formatter = fmt;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function detectMonorepo(fileNameSet, result) {
|
|
220
|
+
for (const [filename, tool] of Object.entries(MONOREPO_MAP)) {
|
|
221
|
+
if (fileNameSet.has(filename)) {
|
|
222
|
+
result.monorepo = tool;
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function detectInfra(fileSet, fileNameSet, files, result) {
|
|
229
|
+
for (const [indicator, tool] of Object.entries(INFRA_MAP)) {
|
|
230
|
+
if (indicator.startsWith('*')) {
|
|
231
|
+
const ext = indicator.slice(1);
|
|
232
|
+
if (files.some((f) => f.endsWith(ext))) {
|
|
233
|
+
result.detectedFiles.infra.push(tool);
|
|
234
|
+
}
|
|
235
|
+
} else if (indicator.endsWith('/')) {
|
|
236
|
+
const dir = indicator.slice(0, -1);
|
|
237
|
+
if (files.some((f) => f.startsWith(dir + '/'))) {
|
|
238
|
+
result.detectedFiles.infra.push(tool);
|
|
239
|
+
}
|
|
240
|
+
} else if (fileNameSet.has(indicator) || fileSet.has(indicator)) {
|
|
241
|
+
result.detectedFiles.infra.push(tool);
|
|
242
|
+
if (indicator === 'Dockerfile' || indicator.startsWith('docker-compose')) {
|
|
243
|
+
result.containerized = true;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function inferBuildTool(result) {
|
|
250
|
+
if (result.framework) {
|
|
251
|
+
const frameworkBuildTools = {
|
|
252
|
+
'next.js': 'next',
|
|
253
|
+
vite: 'vite',
|
|
254
|
+
angular: 'angular-cli',
|
|
255
|
+
gatsby: 'gatsby',
|
|
256
|
+
astro: 'astro',
|
|
257
|
+
sveltekit: 'vite',
|
|
258
|
+
};
|
|
259
|
+
result.buildTool = frameworkBuildTools[result.framework] || result.framework;
|
|
260
|
+
} else if (result.runtime === 'rust') {
|
|
261
|
+
result.buildTool = 'cargo';
|
|
262
|
+
} else if (result.runtime === 'go') {
|
|
263
|
+
result.buildTool = 'go';
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function suggestCommands(result) {
|
|
268
|
+
const pm = result.packageManager || 'npm';
|
|
269
|
+
const pmRun = pm === 'npm' ? 'npm run' : pm;
|
|
270
|
+
|
|
271
|
+
// Check framework-specific commands
|
|
272
|
+
const fwKey = result.framework || result.runtime;
|
|
273
|
+
if (fwKey && SUGGESTED_COMMANDS[fwKey]) {
|
|
274
|
+
const cmds = SUGGESTED_COMMANDS[fwKey];
|
|
275
|
+
result.suggestedDevCmd = cmds.dev?.replace('{pm}', pmRun) || null;
|
|
276
|
+
result.suggestedBuildCmd = cmds.build?.replace('{pm}', pmRun) || null;
|
|
277
|
+
result.suggestedTestCmd = cmds.test?.replace('{pm}', pmRun) || null;
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Generic Node.js fallbacks
|
|
282
|
+
if (result.runtime === 'node') {
|
|
283
|
+
result.suggestedDevCmd = `${pmRun} dev`;
|
|
284
|
+
result.suggestedBuildCmd = `${pmRun} build`;
|
|
285
|
+
if (result.testFramework) {
|
|
286
|
+
result.suggestedTestCmd = `${pmRun} test`;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function parseManifestDetails(projectDir, fileSet, result) {
|
|
292
|
+
// Extract additional info from package.json scripts
|
|
293
|
+
if (fileSet.has('package.json')) {
|
|
294
|
+
try {
|
|
295
|
+
const pkgRaw = await readFile(join(projectDir, 'package.json'), 'utf-8');
|
|
296
|
+
const pkg = JSON.parse(pkgRaw);
|
|
297
|
+
|
|
298
|
+
// Override suggested commands with actual scripts if they exist
|
|
299
|
+
if (pkg.scripts) {
|
|
300
|
+
const pm = result.packageManager || 'npm';
|
|
301
|
+
const pmRun = pm === 'npm' ? 'npm run' : pm;
|
|
302
|
+
if (pkg.scripts.dev) result.suggestedDevCmd = `${pmRun} dev`;
|
|
303
|
+
if (pkg.scripts.build) result.suggestedBuildCmd = `${pmRun} build`;
|
|
304
|
+
if (pkg.scripts.test) result.suggestedTestCmd = `${pmRun} test`;
|
|
305
|
+
if (pkg.scripts.lint) result.suggestedLintCmd = `${pmRun} lint`;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Check for workspaces (monorepo)
|
|
309
|
+
if (pkg.workspaces && !result.monorepo) {
|
|
310
|
+
result.monorepo = 'npm-workspaces';
|
|
311
|
+
}
|
|
312
|
+
} catch {
|
|
313
|
+
// Ignore
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Parse pyproject.toml for Python projects
|
|
318
|
+
if (fileSet.has('pyproject.toml')) {
|
|
319
|
+
try {
|
|
320
|
+
const tomlRaw = await readFile(join(projectDir, 'pyproject.toml'), 'utf-8');
|
|
321
|
+
const toml = parseToml(tomlRaw);
|
|
322
|
+
|
|
323
|
+
// Check for test framework in dependencies
|
|
324
|
+
if (toml.project?.dependencies) {
|
|
325
|
+
const deps = toml.project.dependencies;
|
|
326
|
+
if (Array.isArray(deps) && deps.some((d) => d.startsWith('pytest'))) {
|
|
327
|
+
result.testFramework = result.testFramework || 'pytest';
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (toml.tool?.pytest) {
|
|
331
|
+
result.testFramework = result.testFramework || 'pytest';
|
|
332
|
+
}
|
|
333
|
+
if (toml.tool?.ruff) {
|
|
334
|
+
result.linter = result.linter || 'ruff';
|
|
335
|
+
}
|
|
336
|
+
if (toml.tool?.black) {
|
|
337
|
+
result.formatter = result.formatter || 'black';
|
|
338
|
+
}
|
|
339
|
+
if (toml.tool?.['ruff']?.format) {
|
|
340
|
+
result.formatter = result.formatter || 'ruff';
|
|
341
|
+
}
|
|
342
|
+
} catch {
|
|
343
|
+
// Ignore
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
package/src/generator.js
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir, readdir, chmod, stat } from 'node:fs/promises';
|
|
2
|
+
import { join, dirname, relative } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import ejs from 'ejs';
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const TEMPLATES_DIR = join(__dirname, '..', 'templates');
|
|
8
|
+
const SCAFFOLD_DIR = join(__dirname, '..', 'scaffold');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generate documentation files from detection results and user config.
|
|
12
|
+
* @param {string} projectDir - Project root directory
|
|
13
|
+
* @param {object} config - Combined detection + user answers
|
|
14
|
+
* @returns {Promise<{files: Array<{path: string, content: string}>}>}
|
|
15
|
+
*/
|
|
16
|
+
export async function generateDocs(projectDir, config) {
|
|
17
|
+
const files = [];
|
|
18
|
+
|
|
19
|
+
// Prepare template data with defaults
|
|
20
|
+
const data = buildTemplateData(config);
|
|
21
|
+
|
|
22
|
+
// Generate CLAUDE.md
|
|
23
|
+
const claudeMd = await renderTemplate('claude-md.ejs', data);
|
|
24
|
+
files.push({ path: 'CLAUDE.md', content: claudeMd });
|
|
25
|
+
|
|
26
|
+
// Generate rules files if requested
|
|
27
|
+
if (config.generateRules !== false) {
|
|
28
|
+
const codeStyle = await renderTemplate('rules-code-style.ejs', data);
|
|
29
|
+
files.push({ path: '.claude/rules/code-style.md', content: codeStyle });
|
|
30
|
+
|
|
31
|
+
const testing = await renderTemplate('rules-testing.ejs', data);
|
|
32
|
+
files.push({ path: '.claude/rules/testing.md', content: testing });
|
|
33
|
+
|
|
34
|
+
const workflow = await renderTemplate('rules-workflow.ejs', data);
|
|
35
|
+
files.push({ path: '.claude/rules/workflow.md', content: workflow });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Generate _state.md for project state tracking
|
|
39
|
+
const stateMd = await renderTemplate('state-md.ejs', data);
|
|
40
|
+
files.push({ path: '_state.md', content: stateMd });
|
|
41
|
+
|
|
42
|
+
// Generate settings.json if hooks requested
|
|
43
|
+
if (config.generateHooks) {
|
|
44
|
+
const settingsData = {
|
|
45
|
+
validationScriptPath: '.claude/skills/doc-generator/scripts/validate.sh',
|
|
46
|
+
enableStopHook: config.enableStopHook !== false,
|
|
47
|
+
};
|
|
48
|
+
const settings = await renderTemplate('settings-json.ejs', settingsData);
|
|
49
|
+
files.push({ path: '.claude/settings.json', content: settings });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return { files };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Write generated files to disk.
|
|
57
|
+
* @param {string} projectDir - Project root directory
|
|
58
|
+
* @param {Array<{path: string, content: string}>} files - Files to write
|
|
59
|
+
* @param {object} [options] - Write options
|
|
60
|
+
* @param {boolean} [options.overwrite=false] - Overwrite existing files
|
|
61
|
+
* @param {boolean} [options.dryRun=false] - Only return what would be written
|
|
62
|
+
*/
|
|
63
|
+
export async function writeDocs(projectDir, files, options = {}) {
|
|
64
|
+
const { overwrite = false, dryRun = false } = options;
|
|
65
|
+
const written = [];
|
|
66
|
+
const skipped = [];
|
|
67
|
+
|
|
68
|
+
for (const file of files) {
|
|
69
|
+
const fullPath = join(projectDir, file.path);
|
|
70
|
+
|
|
71
|
+
if (!overwrite) {
|
|
72
|
+
try {
|
|
73
|
+
await readFile(fullPath);
|
|
74
|
+
skipped.push(file.path);
|
|
75
|
+
continue;
|
|
76
|
+
} catch {
|
|
77
|
+
// File doesn't exist — proceed to write
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!dryRun) {
|
|
82
|
+
await mkdir(dirname(fullPath), { recursive: true });
|
|
83
|
+
await writeFile(fullPath, file.content, 'utf-8');
|
|
84
|
+
}
|
|
85
|
+
written.push(file.path);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { written, skipped };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Install Claude Code infrastructure (commands, skills, agents) into the target project.
|
|
93
|
+
* Copies files from the scaffold/ directory.
|
|
94
|
+
* @param {string} projectDir - Target project root
|
|
95
|
+
* @param {object} [options] - Install options
|
|
96
|
+
* @param {boolean} [options.overwrite=false] - Overwrite existing files
|
|
97
|
+
* @returns {Promise<{written: string[], skipped: string[]}>}
|
|
98
|
+
*/
|
|
99
|
+
export async function installScaffold(projectDir, options = {}) {
|
|
100
|
+
const { overwrite = false } = options;
|
|
101
|
+
const written = [];
|
|
102
|
+
const skipped = [];
|
|
103
|
+
|
|
104
|
+
async function copyRecursive(srcDir, destDir) {
|
|
105
|
+
let entries;
|
|
106
|
+
try {
|
|
107
|
+
entries = await readdir(srcDir, { withFileTypes: true });
|
|
108
|
+
} catch {
|
|
109
|
+
return; // scaffold dir may not exist in test environments
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (const entry of entries) {
|
|
113
|
+
const srcPath = join(srcDir, entry.name);
|
|
114
|
+
const destPath = join(destDir, entry.name);
|
|
115
|
+
const relPath = relative(projectDir, destPath);
|
|
116
|
+
|
|
117
|
+
if (entry.isDirectory()) {
|
|
118
|
+
await mkdir(destPath, { recursive: true });
|
|
119
|
+
await copyRecursive(srcPath, destPath);
|
|
120
|
+
} else {
|
|
121
|
+
// Check if file already exists
|
|
122
|
+
if (!overwrite) {
|
|
123
|
+
try {
|
|
124
|
+
await stat(destPath);
|
|
125
|
+
skipped.push(relPath);
|
|
126
|
+
continue;
|
|
127
|
+
} catch {
|
|
128
|
+
// File doesn't exist — proceed
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
await mkdir(dirname(destPath), { recursive: true });
|
|
133
|
+
const content = await readFile(srcPath);
|
|
134
|
+
await writeFile(destPath, content);
|
|
135
|
+
|
|
136
|
+
// Make .sh files executable
|
|
137
|
+
if (entry.name.endsWith('.sh')) {
|
|
138
|
+
await chmod(destPath, 0o755);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
written.push(relPath);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
await copyRecursive(SCAFFOLD_DIR, projectDir);
|
|
147
|
+
return { written, skipped };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Render an EJS template file with the given data.
|
|
152
|
+
*/
|
|
153
|
+
async function renderTemplate(templateName, data) {
|
|
154
|
+
const templatePath = join(TEMPLATES_DIR, templateName);
|
|
155
|
+
const template = await readFile(templatePath, 'utf-8');
|
|
156
|
+
return ejs.render(template, data, { filename: templatePath });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Build template data from config with sensible defaults.
|
|
161
|
+
*/
|
|
162
|
+
function buildTemplateData(config) {
|
|
163
|
+
const pm = config.packageManager || 'npm';
|
|
164
|
+
const pmRun = pm === 'npm' ? 'npm run' : pm;
|
|
165
|
+
|
|
166
|
+
// Build commands object
|
|
167
|
+
const commands = {
|
|
168
|
+
dev: config.suggestedDevCmd || null,
|
|
169
|
+
build: config.suggestedBuildCmd || null,
|
|
170
|
+
test: config.suggestedTestCmd || null,
|
|
171
|
+
lint: config.suggestedLintCmd || null,
|
|
172
|
+
migrate: null,
|
|
173
|
+
format: null,
|
|
174
|
+
testSingle: null,
|
|
175
|
+
testWatch: null,
|
|
176
|
+
testCoverage: null,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// Add framework-specific commands
|
|
180
|
+
if (config.framework === 'django') {
|
|
181
|
+
commands.migrate = 'python manage.py migrate';
|
|
182
|
+
} else if (config.framework === 'rails') {
|
|
183
|
+
commands.migrate = 'bin/rails db:migrate';
|
|
184
|
+
} else if (config.framework === 'laravel') {
|
|
185
|
+
commands.migrate = 'php artisan migrate';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Add test variations
|
|
189
|
+
if (config.testFramework === 'vitest') {
|
|
190
|
+
commands.testSingle = `${pmRun} vitest run path/to/file`;
|
|
191
|
+
commands.testWatch = `${pmRun} vitest`;
|
|
192
|
+
commands.testCoverage = `${pmRun} vitest run --coverage`;
|
|
193
|
+
} else if (config.testFramework === 'jest') {
|
|
194
|
+
commands.testSingle = `${pmRun} jest -- path/to/file`;
|
|
195
|
+
commands.testWatch = `${pmRun} jest --watch`;
|
|
196
|
+
commands.testCoverage = `${pmRun} jest --coverage`;
|
|
197
|
+
} else if (config.testFramework === 'pytest') {
|
|
198
|
+
commands.testSingle = 'pytest path/to/test_file.py';
|
|
199
|
+
commands.testWatch = 'ptw';
|
|
200
|
+
commands.testCoverage = 'pytest --cov';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Format command — use npx for Node tools since they may not be in scripts
|
|
204
|
+
if (config.formatter === 'prettier') {
|
|
205
|
+
commands.format = 'npx prettier --write .';
|
|
206
|
+
} else if (config.formatter === 'black') {
|
|
207
|
+
commands.format = 'black .';
|
|
208
|
+
} else if (config.formatter === 'ruff') {
|
|
209
|
+
commands.format = 'ruff format .';
|
|
210
|
+
} else if (config.formatter === 'rustfmt') {
|
|
211
|
+
commands.format = 'cargo fmt';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Path globs for conditional rule loading
|
|
215
|
+
const pathGlobs = [];
|
|
216
|
+
const testPathGlobs = [];
|
|
217
|
+
if (config.language === 'typescript' || config.language === 'javascript') {
|
|
218
|
+
pathGlobs.push('src/**/*.ts', 'src/**/*.tsx', 'src/**/*.js', 'src/**/*.jsx');
|
|
219
|
+
testPathGlobs.push('**/*.test.ts', '**/*.test.tsx', '**/*.spec.ts', '**/__tests__/**');
|
|
220
|
+
} else if (config.language === 'python') {
|
|
221
|
+
pathGlobs.push('**/*.py');
|
|
222
|
+
testPathGlobs.push('tests/**/*.py', '**/test_*.py');
|
|
223
|
+
} else if (config.language === 'go') {
|
|
224
|
+
pathGlobs.push('**/*.go');
|
|
225
|
+
testPathGlobs.push('**/*_test.go');
|
|
226
|
+
} else if (config.language === 'rust') {
|
|
227
|
+
pathGlobs.push('src/**/*.rs');
|
|
228
|
+
testPathGlobs.push('tests/**/*.rs');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
// Ensure all template variables have defaults to avoid EJS ReferenceErrors
|
|
233
|
+
language: null,
|
|
234
|
+
runtime: null,
|
|
235
|
+
framework: null,
|
|
236
|
+
packageManager: null,
|
|
237
|
+
testFramework: null,
|
|
238
|
+
linter: null,
|
|
239
|
+
formatter: null,
|
|
240
|
+
ci: null,
|
|
241
|
+
monorepo: null,
|
|
242
|
+
containerized: false,
|
|
243
|
+
projectDescription: null,
|
|
244
|
+
projectType: null,
|
|
245
|
+
deployment: null,
|
|
246
|
+
focusAreas: null,
|
|
247
|
+
// Spread config values over defaults
|
|
248
|
+
...config,
|
|
249
|
+
commands,
|
|
250
|
+
pathGlobs,
|
|
251
|
+
testPathGlobs,
|
|
252
|
+
directories: config.directories || [],
|
|
253
|
+
additionalCommands: config.additionalCommands || [],
|
|
254
|
+
rules: config.rules || [],
|
|
255
|
+
conventions: config.conventions || '',
|
|
256
|
+
testConventions: config.testConventions || '',
|
|
257
|
+
generateRules: config.generateRules !== false,
|
|
258
|
+
};
|
|
259
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { detectTechStack } from './detector.js';
|
|
2
|
+
export { generateDocs, writeDocs } from './generator.js';
|
|
3
|
+
export { validateClaudeMd, validateStructure, crossReferenceCheck } from './validator.js';
|
|
4
|
+
export { runExistingProjectFlow, runColdStartFlow, buildDefaultConfig } from './prompts.js';
|
|
5
|
+
export { installScaffold } from './generator.js';
|