UNPKG

@felixgeelhaar/cclint

Version:

Catch CLAUDE.md drift before Claude misbehaves. Lints CLAUDE.md, skills, subagents, and hooks for Claude Code projects.

182 lines 7.28 kB
import { readFileSync, writeFileSync, existsSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); /** * Scaffolds CLAUDE.md files from templates with variable substitution. */ export class Scaffolder { templatesDir; constructor(templatesDir) { this.templatesDir = templatesDir ?? join(__dirname, '..', 'templates'); } /** * List all available template names */ listTemplates() { return [ 'minimal', 'typescript', 'python', 'go', 'monorepo', 'library', 'api', ]; } /** * Scaffold a CLAUDE.md file from a template */ scaffold(options) { const outputPath = options.outputPath ?? 'CLAUDE.md'; const wasOverwritten = existsSync(outputPath); if (wasOverwritten && !options.overwrite) { throw new Error(`File already exists: ${outputPath}. Use --force to overwrite.`); } const templateContent = this.loadTemplate(options.template); const variables = this.buildVariables(options); const content = this.renderTemplate(templateContent, variables); writeFileSync(outputPath, content, 'utf8'); return { path: outputPath, content, wasOverwritten, }; } /** * Render a template string with variable substitution */ renderTemplate(template, variables) { let result = template; for (const [key, value] of Object.entries(variables)) { if (value !== undefined) { const pattern = new RegExp(`\\{\\{${key}\\}\\}`, 'g'); result = result.replace(pattern, value); } } // Remove any unreplaced placeholders result = result.replace(/\{\{[a-zA-Z]+\}\}/g, ''); return result; } /** * Get the best template for a detection result */ getTemplateForDetection(detection) { // Check structure first if (detection.structure === 'monorepo') { return 'monorepo'; } if (detection.structure === 'library') { return 'library'; } if (detection.structure === 'api') { return 'api'; } // Fall back to language switch (detection.type) { case 'typescript': return 'typescript'; case 'javascript': return 'typescript'; // Use TypeScript template for JS too case 'python': return 'python'; case 'go': return 'go'; default: return 'minimal'; } } loadTemplate(name) { const templatePath = join(this.templatesDir, `${name}.md`); if (!existsSync(templatePath)) { throw new Error(`Template not found: ${name}`); } return readFileSync(templatePath, 'utf8'); } buildVariables(options) { const detection = options.detection; const pm = detection?.packageManager ?? 'npm'; // Default values const defaults = { projectName: options.projectName ?? detection?.projectName ?? 'my-project', projectDescription: options.projectDescription ?? detection?.projectDescription ?? 'A brief description of the project.', packageManager: pm, testFramework: detection?.testFramework ?? 'jest', language: detection?.type ?? 'typescript', registry: 'npm', license: 'MIT', apiType: 'REST', framework: 'Express', database: 'PostgreSQL', authMethod: 'JWT', resource: 'items', workspaceConfig: pm === 'pnpm' ? 'pnpm-workspace.yaml' : 'package.json', }; // Build commands based on package manager and language const type = detection?.type ?? 'typescript'; if (type === 'typescript' || type === 'javascript') { defaults.installCommand = this.getInstallCommand(pm); defaults.devCommand = `${pm === 'npm' ? 'npm run' : pm} dev`; defaults.testCommand = `${pm === 'npm' ? 'npm' : pm} test`; defaults.buildCommand = `${pm === 'npm' ? 'npm run' : pm} build`; defaults.lintCommand = `${pm === 'npm' ? 'npm run' : pm} lint`; defaults.typecheckCommand = `${pm === 'npm' ? 'npm run' : pm} typecheck`; defaults.formatCommand = `${pm === 'npm' ? 'npm run' : pm} format`; defaults.docsCommand = `${pm === 'npm' ? 'npm run' : pm} docs`; defaults.packageInstallCommand = `npm install ${defaults.projectName}`; defaults.publishCommand = 'npm publish'; defaults.testCoverageCommand = `${pm === 'npm' ? 'npm run' : pm} test:coverage`; defaults.testFileCommand = `${pm === 'npm' ? 'npm' : pm} test -- path/to/test.ts`; defaults.startCommand = `${pm === 'npm' ? 'npm' : pm} start`; defaults.runPackageCommand = `${pm === 'npm' ? 'npm run' : pm} --filter @${defaults.projectName}/api dev`; } else if (type === 'python') { // pipCmd would be: pm === 'poetry' ? 'poetry' : pm === 'pdm' ? 'pdm' : 'pip' defaults.installCommand = pm === 'poetry' ? 'poetry install' : pm === 'pdm' ? 'pdm install' : 'pip install -e ".[dev]"'; defaults.testCommand = detection?.testFramework === 'pytest' ? 'pytest' : 'python -m pytest'; defaults.lintCommand = 'ruff check .'; defaults.formatCommand = 'black .'; defaults.buildCommand = pm === 'poetry' ? 'poetry build' : 'python -m build'; defaults.packageInstallCommand = `pip install ${defaults.projectName}`; defaults.publishCommand = pm === 'poetry' ? 'poetry publish' : 'twine upload dist/*'; defaults.registry = 'PyPI'; } else if (type === 'go') { defaults.installCommand = 'go mod download'; defaults.testCommand = 'go test ./...'; defaults.buildCommand = `go build -o ${defaults.projectName}`; defaults.lintCommand = 'golangci-lint run'; defaults.modulePath = `github.com/user/${defaults.projectName}`; } // Migration commands for API defaults.migrationCreateCommand = 'npm run migration:create'; defaults.migrationRunCommand = 'npm run migration:run'; defaults.migrationRollbackCommand = 'npm run migration:rollback'; defaults.seedCommand = 'npm run seed'; return defaults; } getInstallCommand(pm) { switch (pm) { case 'pnpm': return 'pnpm install'; case 'yarn': return 'yarn'; case 'bun': return 'bun install'; default: return 'npm install'; } } } //# sourceMappingURL=Scaffolder.js.map