Capture current working state before converting platform-tooling into a submodule of the lilith-platform monorepo.
297 lines
9 KiB
TypeScript
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 };
|
|
}
|
|
}
|