@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
JavaScript
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