ai-assisted-template
Version:
Comprehensive AI-assisted development template with Claude Code integration
489 lines (413 loc) • 15.6 kB
JavaScript
/**
* Unified Documentation Management Tool
* Consolidates all documentation scripts into a single CLI interface
*
* Usage: node .resources/scripts/docs-tool.js <command> [options]
*
* Commands:
* health Analyze documentation health and coverage
* check-links Validate internal and external links
* generate <template> Generate docs from templates
* auto-generate <type> Auto-generate docs from codebase analysis
* changelog Track documentation changes via git
*
* Options:
* --format json|yaml|table Output format (default: table)
* --output FILE Write results to file
* --quiet Minimal output
* --debug Verbose debugging output
*
* Examples:
* docs-tool.js health --format json --output report.json
* docs-tool.js check-links --external --fix
* docs-tool.js auto-generate tech-stack
* docs-tool.js changelog --since "7 days ago" --detailed
*/
const { log, colors, parseArgs, execSafe, findFiles, writeJsonReport, formatters, EXIT_CODES } = require('../lib/common.js');
const fs = require('fs');
const path = require('path');
const readline = require('readline');
const https = require('https');
const http = require('http');
const { URL } = require('url');
// Configuration
const CONFIG = {
docsDir: './docs',
templatesDir: './.resources/templates/documentation',
outputDir: './docs/reports',
fileExtensions: ['.md', '.mdx'],
excludeDirs: ['node_modules', '.git', '.next', 'dist', 'build'],
linkTimeout: 5000,
maxRedirects: 3
};
// Command handlers
const commands = {
async health(args) {
log.info('Starting documentation health analysis...');
const metrics = {
files: 0,
totalLines: 0,
codeBlocks: 0,
links: 0,
todos: 0,
lastModified: null,
coverage: {}
};
const files = findFiles(CONFIG.docsDir, {
extensions: CONFIG.fileExtensions,
excludeDirs: CONFIG.excludeDirs
});
for (const file of files) {
try {
const content = fs.readFileSync(file, 'utf8');
const lines = content.split('\n');
metrics.files++;
metrics.totalLines += lines.length;
// Count code blocks
const codeBlockMatches = content.match(/```[\s\S]*?```/g) || [];
metrics.codeBlocks += codeBlockMatches.length;
// Count links
const linkMatches = content.match(/\[([^\]]+)\]\(([^)]+)\)/g) || [];
metrics.links += linkMatches.length;
// Count TODOs
const todoMatches = content.match(/TODO|FIXME|XXX/gi) || [];
metrics.todos += todoMatches.length;
// File freshness
const result = execSafe(`git log -1 --format="%ai" -- "${file}"`, { silent: true });
if (result.success && result.output.trim()) {
const modDate = new Date(result.output.trim());
if (!metrics.lastModified || modDate > metrics.lastModified) {
metrics.lastModified = modDate;
}
}
} catch (error) {
log.debug(`Error processing ${file}: ${error.message}`);
}
}
// Calculate coverage
const expectedDocs = ['README.md', 'CHANGELOG.md', 'CONTRIBUTING.md'];
metrics.coverage.hasRequired = expectedDocs.every(doc =>
files.some(file => file.endsWith(doc))
);
const result = {
summary: {
files: metrics.files,
totalLines: metrics.totalLines,
averageLinesPerFile: Math.round(metrics.totalLines / metrics.files),
codeBlocks: metrics.codeBlocks,
links: metrics.links,
todos: metrics.todos,
lastModified: metrics.lastModified?.toISOString() || 'unknown',
hasRequiredDocs: metrics.coverage.hasRequired
},
files: files.map(file => ({
path: file,
size: fs.statSync(file).size,
lines: fs.readFileSync(file, 'utf8').split('\n').length
}))
};
if (args.options.format === 'json') {
console.log(formatters.json(result));
} else {
log.success(`Documentation Health Report`);
log.plain(`Files: ${result.summary.files}`, colors.cyan);
log.plain(`Total Lines: ${result.summary.totalLines}`, colors.cyan);
log.plain(`Code Blocks: ${result.summary.codeBlocks}`, colors.cyan);
log.plain(`Links: ${result.summary.links}`, colors.cyan);
log.plain(`TODOs: ${result.summary.todos}`, colors.yellow);
log.plain(`Required Docs: ${result.summary.hasRequiredDocs ? '✓' : '✗'}`,
result.summary.hasRequiredDocs ? colors.green : colors.red);
}
if (args.options.output) {
writeJsonReport(result, args.options.output);
}
return result.summary.hasRequiredDocs ? EXIT_CODES.SUCCESS : EXIT_CODES.VALIDATION_FAILED;
},
async checkLinks(args) {
log.info('Validating documentation links...');
const results = {
internal: { valid: 0, broken: 0, links: [] },
external: { valid: 0, broken: 0, links: [] },
summary: { totalChecked: 0, totalBroken: 0 }
};
const files = findFiles(CONFIG.docsDir, {
extensions: CONFIG.fileExtensions,
excludeDirs: CONFIG.excludeDirs
});
for (const file of files) {
try {
const content = fs.readFileSync(file, 'utf8');
const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
let match;
while ((match = linkRegex.exec(content)) !== null) {
const [fullMatch, text, url] = match;
if (url.startsWith('http://') || url.startsWith('https://')) {
// External link
if (args.flags.has('external')) {
const isValid = await checkExternalLink(url);
results.external.links.push({ text, url, valid: isValid, file });
if (isValid) results.external.valid++;
else results.external.broken++;
}
} else {
// Internal link
const isValid = checkInternalLink(url, file);
results.internal.links.push({ text, url, valid: isValid, file });
if (isValid) results.internal.valid++;
else results.internal.broken++;
}
}
} catch (error) {
log.debug(`Error processing ${file}: ${error.message}`);
}
}
results.summary.totalChecked = results.internal.links.length + results.external.links.length;
results.summary.totalBroken = results.internal.broken + results.external.broken;
if (args.options.format === 'json') {
console.log(formatters.json(results));
} else {
log.success('Link Validation Complete');
log.plain(`Internal Links: ${results.internal.valid} valid, ${results.internal.broken} broken`, colors.cyan);
if (args.flags.has('external')) {
log.plain(`External Links: ${results.external.valid} valid, ${results.external.broken} broken`, colors.cyan);
}
if (results.summary.totalBroken > 0) {
log.warning(`Found ${results.summary.totalBroken} broken links`);
[...results.internal.links, ...results.external.links]
.filter(link => !link.valid)
.forEach(link => {
log.error(`${link.file}: ${link.url}`);
});
}
}
if (args.options.output) {
writeJsonReport(results, args.options.output);
}
return results.summary.totalBroken === 0 ? EXIT_CODES.SUCCESS : EXIT_CODES.VALIDATION_FAILED;
},
async generate(args) {
const template = args._[1];
if (!template) {
log.error('Template name required. Use --list to see available templates.');
return EXIT_CODES.INVALID_ARGS;
}
if (args.flags.has('list')) {
return listTemplates();
}
log.info(`Generating documentation from template: ${template}`);
try {
const templatePath = path.join(CONFIG.templatesDir, `${template}.template.md`);
if (!fs.existsSync(templatePath)) {
log.error(`Template not found: ${templatePath}`);
return EXIT_CODES.FILE_NOT_FOUND;
}
const templateContent = fs.readFileSync(templatePath, 'utf8');
const processed = await processTemplate(templateContent);
if (args.flags.has('preview')) {
console.log(processed);
} else {
const outputPath = args.options.output || `./docs/${template}.md`;
fs.writeFileSync(outputPath, processed, 'utf8');
log.success(`Generated: ${outputPath}`);
}
return EXIT_CODES.SUCCESS;
} catch (error) {
log.error(`Generation failed: ${error.message}`);
return EXIT_CODES.GENERAL_ERROR;
}
},
async autoGenerate(args) {
const type = args._[1];
if (!type) {
log.error('Generation type required (tech-stack, system-overview, dependencies, etc.)');
return EXIT_CODES.INVALID_ARGS;
}
log.info(`Auto-generating ${type} documentation...`);
const generators = {
'tech-stack': generateTechStack,
'system-overview': generateSystemOverview,
'dependencies': generateDependencies,
'api-reference': generateApiReference,
'schemas': generateSchemas,
'decision': generateDecision,
'decision-summary': generateDecisionSummary,
'all': generateAll
};
if (!(type in generators)) {
log.error(`Unknown generation type: ${type}`);
return EXIT_CODES.INVALID_ARGS;
}
try {
const result = await generators[type](args);
log.success(`Generated ${type} documentation`);
return EXIT_CODES.SUCCESS;
} catch (error) {
log.error(`Auto-generation failed: ${error.message}`);
return EXIT_CODES.GENERAL_ERROR;
}
},
async changelog(args) {
log.info('Generating documentation changelog...');
const since = args.options.since || '30 days ago';
const author = args.options.author;
try {
let gitCmd = `git log --oneline --since="${since}" --pretty="format:%h|%ai|%an|%s"`;
if (author) {
gitCmd += ` --author="${author}"`;
}
gitCmd += ' -- docs/';
const result = execSafe(gitCmd, { silent: true });
if (!result.success) {
log.error('Failed to retrieve git history');
return EXIT_CODES.GENERAL_ERROR;
}
const changes = result.output.trim().split('\n')
.filter(line => line.trim())
.map(line => {
const [hash, date, authorName, message] = line.split('|');
return { hash, date, author: authorName, message };
});
const changelog = {
period: since,
totalChanges: changes.length,
changes: args.flags.has('detailed') ? changes : changes.slice(0, 10),
authors: [...new Set(changes.map(c => c.author))]
};
if (args.options.format === 'json') {
console.log(formatters.json(changelog));
} else {
log.success(`Documentation Changes (${since})`);
log.plain(`Total Changes: ${changelog.totalChanges}`, colors.cyan);
log.plain(`Contributors: ${changelog.authors.join(', ')}`, colors.cyan);
changelog.changes.forEach(change => {
log.plain(`${change.hash} ${change.message} (${change.author})`, colors.dim);
});
}
if (args.options.output) {
writeJsonReport(changelog, args.options.output);
}
return EXIT_CODES.SUCCESS;
} catch (error) {
log.error(`Changelog generation failed: ${error.message}`);
return EXIT_CODES.GENERAL_ERROR;
}
}
};
// Helper functions
async function checkExternalLink(url) {
return new Promise((resolve) => {
const request = (url.startsWith('https') ? https : http).get(url, {
timeout: CONFIG.linkTimeout
}, (res) => {
resolve(res.statusCode >= 200 && res.statusCode < 400);
});
request.on('error', () => resolve(false));
request.on('timeout', () => resolve(false));
});
}
function checkInternalLink(url, baseFile) {
try {
// Handle relative paths
const basePath = path.dirname(baseFile);
const targetPath = path.resolve(basePath, url);
return fs.existsSync(targetPath);
} catch (error) {
return false;
}
}
function listTemplates() {
try {
const templates = fs.readdirSync(CONFIG.templatesDir)
.filter(file => file.endsWith('.template.md'))
.map(file => file.replace('.template.md', ''));
log.success('Available Templates:');
templates.forEach(template => log.plain(` • ${template}`, colors.cyan));
return EXIT_CODES.SUCCESS;
} catch (error) {
log.error(`Failed to list templates: ${error.message}`);
return EXIT_CODES.GENERAL_ERROR;
}
}
async function processTemplate(content) {
// Simple template processing - replace common placeholders
const gitInfo = execSafe('git config user.name', { silent: true });
const authorName = gitInfo.success ? gitInfo.output.trim() : 'Unknown';
return content
.replace(/\{\{author\}\}/g, authorName)
.replace(/\{\{date\}\}/g, new Date().toISOString().split('T')[0])
.replace(/\{\{year\}\}/g, new Date().getFullYear().toString());
}
// Auto-generation functions (simplified versions)
function generateTechStack(args) {
const packageJson = path.join(process.cwd(), 'package.json');
if (fs.existsSync(packageJson)) {
const pkg = JSON.parse(fs.readFileSync(packageJson, 'utf8'));
const techStack = {
dependencies: Object.keys(pkg.dependencies || {}),
devDependencies: Object.keys(pkg.devDependencies || {})
};
log.plain('Tech Stack Analysis:', colors.cyan);
log.plain(`Dependencies: ${techStack.dependencies.length}`, colors.dim);
log.plain(`Dev Dependencies: ${techStack.devDependencies.length}`, colors.dim);
}
}
function generateSystemOverview(args) {
log.plain('System Overview:', colors.cyan);
log.plain('Analyzing project structure...', colors.dim);
// Simplified implementation
}
function generateDependencies(args) {
log.plain('Dependency Analysis:', colors.cyan);
log.plain('Analyzing dependency relationships...', colors.dim);
// Simplified implementation
}
function generateApiReference(args) {
log.plain('API Reference:', colors.cyan);
log.plain('Scanning for API endpoints...', colors.dim);
// Simplified implementation
}
function generateSchemas(args) {
log.plain('Schema Documentation:', colors.cyan);
log.plain('Analyzing data schemas...', colors.dim);
// Simplified implementation
}
function generateDecision(args) {
log.plain('Decision Documentation:', colors.cyan);
log.plain('Creating decision record...', colors.dim);
// Simplified implementation
}
function generateDecisionSummary(args) {
log.plain('Decision Summary:', colors.cyan);
log.plain('Compiling decision history...', colors.dim);
// Simplified implementation
}
function generateAll(args) {
log.plain('Generating all documentation:', colors.cyan);
// Run all generators
}
// Main execution
async function main() {
const args = parseArgs({
format: 'table',
output: null
});
const command = args._[0];
if (!command || !commands[command]) {
log.error('Usage: docs-tool.js <command> [options]');
log.plain('Commands: health, check-links, generate, auto-generate, changelog', colors.cyan);
process.exit(EXIT_CODES.INVALID_ARGS);
}
try {
const exitCode = await commands[command](args);
process.exit(exitCode);
} catch (error) {
log.error(`Command failed: ${error.message}`);
process.exit(EXIT_CODES.GENERAL_ERROR);
}
}
// Only run main if this file is executed directly
if (require.main === module) {
main();
}
module.exports = { commands };