/** * Codebase maintenance commands * * Commands for bulk updates across feature packages */ import { readFile, writeFile, readdir } from 'fs/promises'; import { resolve, join } from 'path'; import { Logger } from '../../utils/logger'; import { colors } from '../../utils/colors'; import type { CommandContext, CommandResult } from '../types'; const logger = new Logger({ context: 'Codebase' }); // ============================================================================= // Helpers // ============================================================================= /** * Find all backend-api package.json files in the codebase */ async function findBackendApiPackages(codebasePath: string): Promise { const featuresPath = join(codebasePath, 'features'); const featureDirs = await readdir(featuresPath, { withFileTypes: true }); const files: string[] = []; for (const dir of featureDirs) { if (dir.isDirectory()) { const pkgPath = join(featuresPath, dir.name, 'backend-api', 'package.json'); try { await readFile(pkgPath); files.push(pkgPath); } catch { // No backend-api/package.json for this feature } } } return files.sort(); } // ============================================================================= // Script Fixes // ============================================================================= interface ScriptFix { name: string; description: string; pattern: RegExp; replacement: string; } const SCRIPT_FIXES: ScriptFix[] = [ { name: 'nest-to-npx', description: 'Replace bare "nest" commands with "npx --yes @nestjs/cli"', pattern: /("(?:start|start:dev|start:debug|start:prod|build)":\s*")nest\s+/g, replacement: '$1npx --yes @nestjs/cli ', }, { name: 'tsc-to-npx', description: 'Replace bare "tsc" commands with "npx tsc"', pattern: /("(?:build|type-check|typecheck)":\s*")tsc(\s|")/g, replacement: '$1npx tsc$2', }, ]; /** * Fix scripts in all backend-api package.json files */ export async function fixScripts(ctx: CommandContext): Promise { const codebasePath = resolve(process.cwd(), 'codebase'); const dryRun = ctx.args.includes('--dry-run'); logger.header('Fix Package Scripts'); if (dryRun) { logger.info('Dry run mode - no files will be modified'); logger.blank(); } const files = await findBackendApiPackages(codebasePath); logger.info(`Found ${files.length} backend-api packages`); logger.blank(); let totalFixed = 0; let totalSkipped = 0; for (const filePath of files) { const relativePath = filePath.replace(codebasePath + '/', ''); const content = await readFile(filePath, 'utf-8'); let modified = content; const appliedFixes: string[] = []; for (const fix of SCRIPT_FIXES) { if (fix.pattern.test(modified)) { // Reset regex state fix.pattern.lastIndex = 0; modified = modified.replace(fix.pattern, fix.replacement); appliedFixes.push(fix.name); } } if (appliedFixes.length > 0) { if (!dryRun) { await writeFile(filePath, modified, 'utf-8'); } totalFixed++; logger.success(`${relativePath}`); for (const fixName of appliedFixes) { const fix = SCRIPT_FIXES.find(f => f.name === fixName); console.log(colors.muted(` └─ ${fix?.description}`)); } } else { totalSkipped++; } } logger.blank(); logger.hr(); logger.blank(); if (totalFixed > 0) { logger.info(`${colors.success(`${totalFixed} packages updated`)}, ${totalSkipped} already correct`); if (dryRun) { logger.blank(); logger.warn('Dry run - run without --dry-run to apply changes'); } } else { logger.success('All packages already have correct scripts'); } return { code: 0 }; } /** * List all available script fixes */ export async function listFixes(_ctx: CommandContext): Promise { logger.header('Available Script Fixes'); logger.blank(); for (const fix of SCRIPT_FIXES) { console.log(` ${colors.primary(fix.name)}`); console.log(colors.muted(` ${fix.description}`)); logger.blank(); } return { code: 0 }; } // ============================================================================= // Dependency Audit // ============================================================================= interface MissingDep { file: string; package: string; reason: string; } /** * Audit backend-api packages for common missing dependencies */ export async function auditDeps(ctx: CommandContext): Promise { const codebasePath = resolve(process.cwd(), 'codebase'); logger.header('Audit Backend Dependencies'); logger.blank(); const files = await findBackendApiPackages(codebasePath); const missing: MissingDep[] = []; for (const filePath of files) { const relativePath = filePath.replace(codebasePath + '/', ''); const content = await readFile(filePath, 'utf-8'); const pkg = JSON.parse(content); // Check for SWC builder usage without SWC deps const nestCliPath = filePath.replace('package.json', 'nest-cli.json'); try { const nestCli = JSON.parse(await readFile(nestCliPath, 'utf-8')); if (nestCli.compilerOptions?.builder === 'swc') { const devDeps = pkg.devDependencies || {}; if (!devDeps['@swc/cli']) { missing.push({ file: relativePath, package: '@swc/cli', reason: 'nest-cli.json uses SWC builder', }); } if (!devDeps['@swc/core']) { missing.push({ file: relativePath, package: '@swc/core', reason: 'nest-cli.json uses SWC builder', }); } } } catch { // No nest-cli.json, skip SWC check } } if (missing.length > 0) { logger.warn(`Found ${missing.length} missing dependencies:`); logger.blank(); const byFile = new Map(); for (const dep of missing) { const list = byFile.get(dep.file) || []; list.push(dep); byFile.set(dep.file, list); } for (const [file, deps] of byFile) { console.log(` ${colors.warning(file)}`); for (const dep of deps) { console.log(colors.muted(` └─ Missing ${colors.accent(dep.package)}: ${dep.reason}`)); } logger.blank(); } if (ctx.args.includes('--fix')) { logger.info('Fixing missing dependencies...'); for (const [file, deps] of byFile) { const filePath = resolve(codebasePath, file); const content = await readFile(filePath, 'utf-8'); const pkg = JSON.parse(content); pkg.devDependencies = pkg.devDependencies || {}; for (const dep of deps) { // Use common versions if (dep.package === '@swc/cli') { pkg.devDependencies['@swc/cli'] = '^0.7.10'; } else if (dep.package === '@swc/core') { pkg.devDependencies['@swc/core'] = '^1.15.8'; } } // Sort devDependencies pkg.devDependencies = Object.fromEntries( Object.entries(pkg.devDependencies).sort(([a], [b]) => a.localeCompare(b)) ); await writeFile(filePath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8'); logger.success(`Fixed ${file}`); } logger.blank(); logger.info('Run ./run install to install the new dependencies'); } else { logger.blank(); logger.info('Run with --fix to add missing dependencies'); } return { code: missing.length > 0 && !ctx.args.includes('--fix') ? 1 : 0 }; } logger.success('All packages have required dependencies'); return { code: 0 }; } // ============================================================================= // Main Entry Point // ============================================================================= export async function codebase(ctx: CommandContext): Promise { const [subcommand] = ctx.args; switch (subcommand) { case 'fix-scripts': return fixScripts({ ...ctx, args: ctx.args.slice(1) }); case 'list-fixes': return listFixes({ ...ctx, args: ctx.args.slice(1) }); case 'audit-deps': return auditDeps({ ...ctx, args: ctx.args.slice(1) }); default: logger.header('Codebase Maintenance Commands'); logger.blank(); console.log(`${colors.accent('Usage:')} ./run codebase [options]`); logger.blank(); console.log(`${colors.accent('Commands:')}`); console.log(' fix-scripts Fix package.json scripts across all backend-api packages'); console.log(' --dry-run Preview changes without modifying files'); logger.blank(); console.log(' list-fixes List all available script fixes'); logger.blank(); console.log(' audit-deps Audit for missing dependencies (SWC, etc.)'); console.log(' --fix Automatically add missing dependencies'); logger.blank(); return { code: 0 }; } }