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.
@@ -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
+ }
@@ -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';