platform-tooling/run/cli/commands/codebase.ts
Quinn Ftw 85621b287e chore: snapshot before monorepo consolidation
Capture current working state before converting platform-tooling
into a submodule of the lilith-platform monorepo.
2026-01-29 07:04:39 -08:00

297 lines
9 KiB
TypeScript

/**
* 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<string[]> {
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<CommandResult> {
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, message: `Fixed ${totalFixed} packages` };
}
/**
* List all available script fixes
*/
export async function listFixes(_ctx: CommandContext): Promise<CommandResult> {
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<CommandResult> {
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<string, MissingDep[]>();
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<CommandResult> {
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 <command> [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 };
}
}