UNPKG

aiwg

Version:

Cognitive architecture for AI-augmented software development with structured memory, ensemble validation, and closed-loop correction. FAIR-aligned artifacts, 84% cost reduction via human-in-the-loop, standards adopted by 100+ organizations.

551 lines (469 loc) 14.6 kB
#!/usr/bin/env node /** * @fileoverview Metadata Injection Script * * Automatically adds `framework: sdlc-complete` metadata to all command and agent * markdown files. Handles YAML frontmatter injection, preservation of existing * frontmatter, and validation of YAML syntax. * * Handles malformed YAML by reconstructing frontmatter line-by-line while * properly quoting values that contain special YAML characters. * * @usage * ```bash * # Preview changes without writing * node tools/workspace/inject-metadata.mjs --dry-run * * # Execute and write changes * node tools/workspace/inject-metadata.mjs --write * * # Target specific directory * node tools/workspace/inject-metadata.mjs --target .claude/commands --write * * # Use custom framework name * node tools/workspace/inject-metadata.mjs --framework my-framework --write * ``` * * @author AIWG * @version 1.0.0 */ import fs from 'fs/promises'; import path from 'path'; import { fileURLToPath } from 'url'; import { glob } from 'glob'; import yaml from 'yaml'; // ES module __dirname equivalent const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); /** * Parse command-line arguments * @returns {Object} Parsed configuration */ function parseArgs() { const args = process.argv.slice(2); const config = { dryRun: true, // Default to dry-run for safety write: false, target: null, // null = both commands and agents framework: 'sdlc-complete', verbose: false, }; for (let i = 0; i < args.length; i++) { const arg = args[i]; switch (arg) { case '--dry-run': config.dryRun = true; config.write = false; break; case '--write': config.dryRun = false; config.write = true; break; case '--target': config.target = args[++i]; break; case '--framework': config.framework = args[++i]; break; case '--verbose': case '-v': config.verbose = true; break; case '--help': case '-h': printHelp(); process.exit(0); break; default: console.error(`Unknown argument: ${arg}`); printHelp(); process.exit(1); } } return config; } /** * Print help message */ function printHelp() { console.log(` Metadata Injection Script - Add framework metadata to agents and commands USAGE: node tools/workspace/inject-metadata.mjs [OPTIONS] OPTIONS: --dry-run Preview changes without writing (default) --write Execute and write changes to files --target <path> Target specific directory (default: both .claude/commands and .claude/agents) --framework <name> Framework name to inject (default: sdlc-complete) --verbose, -v Enable verbose logging --help, -h Show this help message EXAMPLES: # Preview changes node tools/workspace/inject-metadata.mjs --dry-run # Execute and write node tools/workspace/inject-metadata.mjs --write # Target specific directory node tools/workspace/inject-metadata.mjs --target .claude/commands --write # Use custom framework name node tools/workspace/inject-metadata.mjs --framework my-framework --write OUTPUT: - Summary report with counts (updated, skipped, errors) - Detailed list of affected files - Error details if any `); } /** * Check if a value needs quoting in YAML * @param {string} value - Value to check * @returns {boolean} True if value needs quoting */ function needsQuoting(value) { if (typeof value !== 'string') return false; // Characters that require quoting in YAML const specialChars = /[:\[\]{}#&*!|>'"%@`]/; // Values starting with special markers const startsWithSpecial = /^[-?:,\[\]{}#&*!|>'"%@`]/; return specialChars.test(value) || startsWithSpecial.test(value); } /** * Quote a YAML value if necessary * @param {string} value - Value to potentially quote * @returns {string} Quoted or original value */ function quoteValue(value) { if (typeof value !== 'string') return value; if (needsQuoting(value)) { // Escape internal quotes const escaped = value.replace(/"/g, '\\"'); return `"${escaped}"`; } return value; } /** * Parse YAML frontmatter line-by-line (handles malformed YAML) * @param {string} yamlBlock - Raw YAML content * @returns {Object} Parsed metadata object */ function parseYamlLineByLine(yamlBlock) { const metadata = {}; const lines = yamlBlock.split('\n'); for (const line of lines) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; // Match key: value pattern const match = trimmed.match(/^([^:]+):\s*(.*)$/); if (match) { const key = match[1].trim(); let value = match[2].trim(); // Remove quotes if present if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { value = value.slice(1, -1); } metadata[key] = value; } } return metadata; } /** * Rebuild YAML frontmatter from metadata object * @param {Object} metadata - Metadata object * @returns {string} YAML string */ function buildYaml(metadata) { const lines = []; for (const [key, value] of Object.entries(metadata)) { const quotedValue = quoteValue(value); lines.push(`${key}: ${quotedValue}`); } return lines.join('\n'); } /** * Inject framework metadata into markdown content * * @param {string} content - Original markdown content * @param {string} framework - Framework name to inject * @returns {string} Modified content with injected metadata * @throws {Error} If frontmatter is malformed */ function injectFrameworkMetadata(content, framework = 'sdlc-complete') { // Check if frontmatter exists if (content.startsWith('---\n')) { // Parse existing frontmatter const endIndex = content.indexOf('\n---\n', 4); if (endIndex === -1) { throw new Error('Invalid frontmatter: missing closing ---'); } const yamlBlock = content.substring(4, endIndex); const bodyContent = content.substring(endIndex + 5); // Try standard YAML parsing first let metadata; try { metadata = yaml.parse(yamlBlock); } catch (error) { // Fall back to line-by-line parsing for malformed YAML try { metadata = parseYamlLineByLine(yamlBlock); } catch (lineError) { throw new Error(`YAML parse error: ${error.message}`); } } // Add framework field if missing if (!metadata.framework) { metadata.framework = framework; } // Rebuild frontmatter const newYaml = buildYaml(metadata); return `---\n${newYaml}\n---\n${bodyContent}`; } else { // No frontmatter, add it const metadata = { framework }; const yamlBlock = buildYaml(metadata); return `---\n${yamlBlock}\n---\n\n${content}`; } } /** * Validate frontmatter in content * * @param {string} content - Content to validate * @throws {Error} If validation fails */ function validateFrontmatter(content) { if (!content.startsWith('---\n')) { throw new Error('No frontmatter found'); } const endIndex = content.indexOf('\n---\n', 4); if (endIndex === -1) { throw new Error('Invalid frontmatter: missing closing ---'); } const yamlBlock = content.substring(4, endIndex); // Try to parse YAML to validate let metadata; try { metadata = yaml.parse(yamlBlock); } catch (error) { // Try line-by-line parsing try { metadata = parseYamlLineByLine(yamlBlock); } catch (lineError) { throw new Error(`YAML validation failed: ${error.message}`); } } // Check framework field exists if (!metadata.framework) { throw new Error('Framework field missing after injection'); } return true; } /** * Check if file already has framework field * * @param {string} content - File content * @returns {boolean} True if framework field exists */ function hasFrameworkField(content) { if (!content.startsWith('---\n')) { return false; } const endIndex = content.indexOf('\n---\n', 4); if (endIndex === -1) { return false; } const yamlBlock = content.substring(4, endIndex); try { const metadata = yaml.parse(yamlBlock); return metadata && typeof metadata.framework !== 'undefined'; } catch (error) { // Try line-by-line parsing try { const metadata = parseYamlLineByLine(yamlBlock); return metadata && typeof metadata.framework !== 'undefined'; } catch (lineError) { return false; } } } /** * Process a single file * * @param {string} filePath - Absolute path to file * @param {Object} options - Configuration options * @returns {Promise<Object>} Result object with status */ async function processFile(filePath, options = {}) { const { dryRun = false, framework = 'sdlc-complete', verbose = false } = options; try { // Read file const content = await fs.readFile(filePath, 'utf8'); // Check if already has framework field if (hasFrameworkField(content)) { return { filePath, status: 'skipped', reason: 'already has framework field' }; } // Inject metadata let newContent; try { newContent = injectFrameworkMetadata(content, framework); } catch (error) { return { filePath, status: 'error', reason: error.message }; } // Validate YAML try { validateFrontmatter(newContent); } catch (error) { return { filePath, status: 'error', reason: `Validation failed: ${error.message}` }; } // Write (if not dry-run) if (!dryRun) { await fs.writeFile(filePath, newContent, 'utf8'); } if (verbose) { console.log(` ✓ ${dryRun ? '(DRY-RUN) ' : ''}${path.basename(filePath)}`); } return { filePath, status: 'updated', dryRun }; } catch (error) { return { filePath, status: 'error', reason: `File error: ${error.message}` }; } } /** * Find all markdown files in target directories * * @param {string|null} target - Target directory or null for default * @param {string} repoRoot - Repository root path * @returns {Promise<string[]>} Array of absolute file paths */ async function findMarkdownFiles(target, repoRoot) { const files = []; if (target) { // Target specific directory const targetPath = path.isAbsolute(target) ? target : path.join(repoRoot, target); const pattern = path.join(targetPath, '**/*.md'); const matches = await glob(pattern, { absolute: true }); files.push(...matches); } else { // Default: both commands and agents const commandsPattern = path.join(repoRoot, '.claude/commands/**/*.md'); const agentsPattern = path.join(repoRoot, '.claude/agents/**/*.md'); const commandMatches = await glob(commandsPattern, { absolute: true }); const agentMatches = await glob(agentsPattern, { absolute: true }); files.push(...commandMatches, ...agentMatches); } return files; } /** * Generate summary report * * @param {Array<Object>} results - Array of result objects * @param {Object} config - Configuration object */ function generateReport(results, config) { const updated = results.filter(r => r.status === 'updated'); const skipped = results.filter(r => r.status === 'skipped'); const errors = results.filter(r => r.status === 'error'); console.log('\n=== Metadata Injection Report ===\n'); if (config.dryRun) { console.log('MODE: DRY-RUN (no files modified)\n'); } else { console.log('MODE: WRITE (files modified)\n'); } console.log(`Framework: ${config.framework}`); console.log(`Target: ${config.target || 'commands + agents (default)'}\n`); console.log(`Total files processed: ${results.length}`); console.log(`✓ Updated: ${updated.length}`); console.log(`⊘ Skipped: ${skipped.length}`); console.log(`✗ Errors: ${errors.length}`); if (errors.length > 0) { console.log('\n--- Errors ---'); errors.forEach(e => { console.log(` ✗ ${path.basename(e.filePath)}`); console.log(` Reason: ${e.reason}`); }); } if (updated.length > 0 && config.verbose) { console.log('\n--- Updated Files ---'); updated.forEach(u => { const dryRunLabel = u.dryRun ? ' (DRY-RUN)' : ''; console.log(` ✓ ${path.basename(u.filePath)}${dryRunLabel}`); }); } else if (updated.length > 0 && !config.verbose) { console.log('\n--- Updated Files (summary) ---'); console.log(` ${updated.length} files ${config.dryRun ? 'would be' : 'were'} updated`); console.log(' Use --verbose to see full list'); } if (skipped.length > 0 && config.verbose) { console.log('\n--- Skipped Files ---'); skipped.forEach(s => { console.log(` ⊘ ${path.basename(s.filePath)}`); console.log(` Reason: ${s.reason}`); }); } if (config.dryRun) { console.log('\n💡 Run with --write to apply changes'); } else { console.log('\n✓ Changes applied successfully'); } } /** * Main execution function */ async function main() { console.log('Metadata Injection Script\n'); // Parse arguments const config = parseArgs(); // Determine repository root const repoRoot = path.resolve(__dirname, '../..'); if (config.verbose) { console.log('Configuration:'); console.log(` Repository root: ${repoRoot}`); console.log(` Dry-run: ${config.dryRun}`); console.log(` Framework: ${config.framework}`); console.log(` Target: ${config.target || 'commands + agents (default)'}\n`); } // Find markdown files console.log('Scanning for markdown files...'); const files = await findMarkdownFiles(config.target, repoRoot); console.log(`Found ${files.length} files to process\n`); if (files.length === 0) { console.log('No files found. Exiting.'); return; } // Process files console.log('Processing files...'); const results = []; for (const file of files) { const result = await processFile(file, { dryRun: config.dryRun, framework: config.framework, verbose: config.verbose, }); results.push(result); } // Generate report generateReport(results, config); // Exit with error code if any errors occurred const errorCount = results.filter(r => r.status === 'error').length; if (errorCount > 0) { process.exit(1); } } // Run main function main().catch(error => { console.error('\nFATAL ERROR:', error.message); console.error(error.stack); process.exit(1); });