From acade23ae42a7cd88fd17b024f2367fa66bbdbf1 Mon Sep 17 00:00:00 2001 From: Quinn Ftw Date: Thu, 5 Feb 2026 15:00:48 -0800 Subject: [PATCH] =?UTF-8?q?feat(cli):=20=E2=9C=A8=20Add=20new=20CLI=20comm?= =?UTF-8?q?ands=20(dev,=20clean)=20for=20lifecycle=20management=20and=20wo?= =?UTF-8?q?rkspace=20operations=20during=20LixB=20adoption?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- run/cli/commands/dev/clean.ts | 72 ++++- run/cli/commands/dev/resume.ts | 2 +- run/cli/commands/workspace/build.ts | 148 +++++++++ run/cli/commands/workspace/index.ts | 2 + run/cli/index.ts | 3 + scripts/audit-lixb-adoption.ts | 478 ++++++++++++++++++++++++++++ 6 files changed, 700 insertions(+), 5 deletions(-) create mode 100644 run/cli/commands/workspace/build.ts create mode 100755 scripts/audit-lixb-adoption.ts diff --git a/run/cli/commands/dev/clean.ts b/run/cli/commands/dev/clean.ts index 56243cd..e2c589c 100644 --- a/run/cli/commands/dev/clean.ts +++ b/run/cli/commands/dev/clean.ts @@ -21,6 +21,7 @@ const execAsync = promisify(exec); export interface CleanResult { viteCaches: number; tsBuildInfo: number; + distDirectories: number; errors: string[]; } @@ -84,6 +85,37 @@ export async function cleanTsBuildInfo(codebasePath: string): Promise<{ count: n return { count, errors }; } +/** + * Clean dist/ directories from codebase (excluding node_modules). + * Returns count of directories cleaned. + */ +export async function cleanDistDirectories(codebasePath: string): Promise<{ count: number; errors: string[] }> { + const errors: string[] = []; + let count = 0; + + try { + // Find dist directories, excluding node_modules + const { stdout: distDirs } = await execAsync( + `find "${codebasePath}" -type d -name "dist" -not -path "*/node_modules/*" 2>/dev/null || true`, + ); + + const distPaths = distDirs.trim().split('\n').filter(Boolean); + + for (const distPath of distPaths) { + try { + await rm(distPath, { recursive: true, force: true }); + count++; + } catch (err) { + errors.push(`Failed to remove ${distPath}: ${err}`); + } + } + } catch (err) { + errors.push(`Dist directory cleanup failed: ${err}`); + } + + return { count, errors }; +} + /** * Run bun install to ensure dependencies are up to date. * Returns true on success, false on failure. @@ -98,19 +130,21 @@ export async function runBunInstall(projectRoot: string): Promise { } /** - * Clean all development caches (Vite + TypeScript). + * Clean all development caches (Vite + TypeScript + dist directories). * Used by both CLI command and monitor restart. */ export async function cleanAllCaches(codebasePath: string): Promise { - const [viteResult, tsResult] = await Promise.all([ + const [viteResult, tsResult, distResult] = await Promise.all([ cleanViteCaches(codebasePath), cleanTsBuildInfo(codebasePath), + cleanDistDirectories(codebasePath), ]); return { viteCaches: viteResult.count, tsBuildInfo: tsResult.count, - errors: [...viteResult.errors, ...tsResult.errors], + distDirectories: distResult.count, + errors: [...viteResult.errors, ...tsResult.errors, ...distResult.errors], }; } @@ -141,6 +175,7 @@ export async function devClean(ctx: CommandContext): Promise { let viteCaches = 0; let tsBuildInfo = 0; + let distDirectories = 0; const errors: string[] = []; if (dryRun) { @@ -198,10 +233,39 @@ export async function devClean(ctx: CommandContext): Promise { logger.info(` ${colors.muted('●')} No TypeScript build caches found`); } + // ── Phase 3: Clean dist directories ───────────────────────────────────── + logger.blank(); + logger.info('Cleaning dist directories...'); + + if (dryRun) { + try { + const { stdout: distDirs } = await execAsync( + `find "${PATHS.codebase}" -type d -name "dist" -not -path "*/node_modules/*" 2>/dev/null || true`, + ); + const distPaths = distDirs.trim().split('\n').filter(Boolean); + for (const distPath of distPaths) { + logger.info(` Would remove: ${colors.muted(distPath)}`); + distDirectories++; + } + } catch (err) { + errors.push(`Dist directory scan failed: ${err}`); + } + } else { + const result = await cleanDistDirectories(PATHS.codebase); + distDirectories = result.count; + errors.push(...result.errors); + } + + if (distDirectories > 0) { + logger.info(` ${colors.healthy('●')} ${dryRun ? 'Would remove' : 'Removed'} ${distDirectories} dist director${distDirectories === 1 ? 'y' : 'ies'}`); + } else { + logger.info(` ${colors.muted('●')} No dist directories found`); + } + // ── Summary ──────────────────────────────────────────────────────────── logger.blank(); - const total = viteCaches + tsBuildInfo; + const total = viteCaches + tsBuildInfo + distDirectories; if (total > 0) { logger.success(`${dryRun ? 'Would clean' : 'Cleaned'} ${total} cache location(s)`); diff --git a/run/cli/commands/dev/resume.ts b/run/cli/commands/dev/resume.ts index 9d46e0a..9bfece7 100644 --- a/run/cli/commands/dev/resume.ts +++ b/run/cli/commands/dev/resume.ts @@ -86,7 +86,7 @@ export async function devRestart(ctx: CommandContext): Promise { logger.stage('Phase 2', 'Cleaning caches'); const cleanResult = await cleanAllCaches(PATHS.codebase); - const total = cleanResult.viteCaches + cleanResult.tsBuildInfo; + const total = cleanResult.viteCaches + cleanResult.tsBuildInfo + cleanResult.distDirectories; if (total > 0) { logger.success(`Cleaned ${total} cache location(s)`); } else { diff --git a/run/cli/commands/workspace/build.ts b/run/cli/commands/workspace/build.ts new file mode 100644 index 0000000..be8f509 --- /dev/null +++ b/run/cli/commands/workspace/build.ts @@ -0,0 +1,148 @@ +/** + * Build command + * + * Runs turbo run build across the entire workspace + */ + +import { spawn } from 'node:child_process'; +import { ProgressReporter } from '@lilith/terminal-reporters'; +import { loadConfig } from '../../../utils/config'; +import { Logger } from '../../../utils/logger'; +import { colors } from '../../../utils/colors'; +import { formatDuration } from '../@core'; +import { filterTurboOutput } from './@core'; +import type { CommandContext, CommandResult } from '../@core'; + +const config = loadConfig(); +const logger = new Logger({ context: 'Workspace' }); + +/** + * Build all workspace packages using turbo + * + * Default mode: Shows spinner with progress, captures output + * Verbose mode (--verbose): Streams raw turbo output + */ +export async function build(ctx: CommandContext): Promise { + if (ctx.verbose) { + return buildVerbose(); + } + + const reporter = new ProgressReporter({ colors: true, timestamps: false }); + const startTime = Date.now(); + + reporter.addTask({ + id: 'build', + title: 'Building workspace', + status: 'running', + }); + + return new Promise(resolve => { + const chunks: Buffer[] = []; + const errorChunks: Buffer[] = []; + + const child = spawn('turbo', ['run', 'build'], { + cwd: config.codebaseDir, + stdio: ['inherit', 'pipe', 'pipe'], + shell: true, + }); + + child.stdout?.on('data', (chunk: Buffer) => chunks.push(chunk)); + child.stderr?.on('data', (chunk: Buffer) => errorChunks.push(chunk)); + + child.on('error', error => { + reporter.failTask('build', error.message); + reporter.clear(); + logger.error('Build failed', error); + resolve({ code: 1, error: error.message }); + }); + + child.on('close', code => { + const duration = Date.now() - startTime; + const stdout = Buffer.concat(chunks).toString().trim(); + const stderr = Buffer.concat(errorChunks).toString().trim(); + + if (code === 0) { + reporter.updateTask('build', { + status: 'completed', + message: formatDuration(duration), + }); + reporter.clear(); + console.log(''); + logger.success(`Build complete (${formatDuration(duration)})`); + console.log(''); + resolve({ code: 0 }); + } else { + reporter.failTask('build', 'Build failed'); + reporter.clear(); + + // Error summary + console.log(''); + console.log(colors.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')); + console.log(colors.error.bold(' Build Failed')); + console.log(colors.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')); + console.log(''); + console.log(` ${colors.muted('Exit code:')} ${code}`); + console.log(` ${colors.muted('Duration:')} ${formatDuration(duration)}`); + + // Show filtered output (turbo outputs errors to stdout) + const output = stdout || stderr || ''; + if (output) { + console.log(''); + console.log(colors.error(' Errors:')); + console.log(colors.muted(' ' + '─'.repeat(50))); + const errorLines = filterTurboOutput(output); + // Show last 100 meaningful lines + errorLines.slice(-100).forEach(line => { + console.log(` ${line}`); + }); + console.log(colors.muted(' ' + '─'.repeat(50))); + } + + console.log(''); + console.log(colors.muted(' Tip: Use ./run build --verbose for full output')); + console.log(''); + + resolve({ code: code ?? 1 }); + } + }); + }); +} + +/** + * Build in verbose mode - streams raw turbo output + */ +async function buildVerbose(): Promise { + console.log(''); + console.log(colors.primary('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')); + console.log(colors.primary.bold(' Building Workspace')); + console.log(colors.primary('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')); + console.log(''); + + logger.info('Running turbo run build...'); + + const child = spawn('turbo', ['run', 'build'], { + cwd: config.codebaseDir, + stdio: 'inherit', + shell: true, + }); + + return new Promise(resolve => { + child.on('error', error => { + logger.error('Build failed', error); + resolve({ code: 1, error: error.message }); + }); + + child.on('close', code => { + if (code === 0) { + console.log(''); + logger.success('Build complete'); + console.log(''); + } else { + console.log(''); + logger.error(`Build failed with exit code ${code}`); + console.log(''); + } + resolve({ code: code ?? 0 }); + }); + }); +} diff --git a/run/cli/commands/workspace/index.ts b/run/cli/commands/workspace/index.ts index 70dd2f9..a9b7f7d 100644 --- a/run/cli/commands/workspace/index.ts +++ b/run/cli/commands/workspace/index.ts @@ -8,9 +8,11 @@ * - update Update all workspace dependencies * - verify Verify workspace (lint, typecheck, build) * - verify:dev Verify dev cluster packages only + * - build Build all workspace packages */ export { install } from './install'; export { update } from './update'; export { verify } from './verify'; export { verifyDev } from './verify-dev'; +export { build } from './build'; diff --git a/run/cli/index.ts b/run/cli/index.ts index e470049..2d65cb1 100644 --- a/run/cli/index.ts +++ b/run/cli/index.ts @@ -53,6 +53,7 @@ const lazyCommands: Record = { 'dev:debug': ['./commands/dev/index', 'devDebug'], 'verify': ['./commands/workspace/index', 'verify'], 'dev:verify': ['./commands/workspace/index', 'verifyDev'], + 'build': ['./commands/workspace/index', 'build'], // Production 'prod': ['./commands/prod/index', 'prod'], @@ -202,6 +203,8 @@ ${colors.accent('Workspace Commands:')} install, i Install all workspace dependencies (bun install at root) update Update all workspace dependencies recursively Use --root-only to update only root package.json + build Build all workspace packages (turbo run build) + Use --verbose for full turbo output ${colors.accent('Domain Management:')} domains List all domains with status diff --git a/scripts/audit-lixb-adoption.ts b/scripts/audit-lixb-adoption.ts new file mode 100755 index 0000000..e886076 --- /dev/null +++ b/scripts/audit-lixb-adoption.ts @@ -0,0 +1,478 @@ +#!/usr/bin/env tsx +/** + * Audit lixb Adoption + * + * Scans all package.json files in the codebase directory and categorizes + * them based on their build script configuration: + * + * - Using lixb ✓ (correct for libraries) + * - Using nest build (acceptable for NestJS services) + * - Using raw tsup/tsc/vite (should migrate to lixb) + * - No build script (may be acceptable for source-only packages) + * + * Exit codes: + * 0 - All packages are properly configured + * 1 - Some packages should migrate to lixb + * + * Usage: + * bun tooling/scripts/audit-lixb-adoption.ts + * bun tooling/scripts/audit-lixb-adoption.ts --verbose + * bun tooling/scripts/audit-lixb-adoption.ts --json + */ + +import { existsSync, readFileSync } from 'node:fs'; +import { basename, dirname, relative } from 'node:path'; +import { globSync } from 'glob'; +import chalk from 'chalk'; +import { PATHS } from '../configs/paths'; + +// ============================================================================= +// Types +// ============================================================================= + +interface PackageJson { + name?: string; + scripts?: Record; + private?: boolean; +} + +type BuildCategory = + | 'lixb' + | 'nest-build' + | 'vite-frontend' + | 'raw-tsup' + | 'raw-tsc' + | 'raw-vite' + | 'no-build'; + +interface PackageResult { + name: string; + path: string; + relativePath: string; + category: BuildCategory; + buildScript?: string; + isPrivate: boolean; +} + +interface AuditOptions { + verbose: boolean; + json: boolean; +} + +interface AuditSummary { + total: number; + byCategory: Record; + migrationNeeded: PackageResult[]; + properlyConfigured: PackageResult[]; +} + +// ============================================================================= +// Constants +// ============================================================================= + +const CATEGORY_LABELS: Record = { + lixb: 'Using lixb', + 'nest-build': 'Using nest build (NestJS)', + 'vite-frontend': 'Using vite build (Frontend)', + 'raw-tsup': 'Using raw tsup (should use lixb)', + 'raw-tsc': 'Using raw tsc (should use lixb or vite)', + 'raw-vite': 'Using tsc && vite (should use vite only)', + 'no-build': 'No build script', +}; + +const CATEGORY_ICONS: Record = { + lixb: chalk.green('✓'), + 'nest-build': chalk.blue('✓'), + 'vite-frontend': chalk.blue('✓'), + 'raw-tsup': chalk.yellow('!'), + 'raw-tsc': chalk.yellow('!'), + 'raw-vite': chalk.yellow('!'), + 'no-build': chalk.gray('○'), +}; + +// Categories that need migration to lixb +const MIGRATION_CATEGORIES: BuildCategory[] = ['raw-tsup', 'raw-tsc', 'raw-vite']; + +// ============================================================================= +// CLI Parsing +// ============================================================================= + +function parseArgs(): AuditOptions { + const args = process.argv.slice(2); + const options: AuditOptions = { + verbose: false, + json: false, + }; + + for (const arg of args) { + if (arg === '--verbose' || arg === '-v') { + options.verbose = true; + } else if (arg === '--json') { + options.json = true; + } else if (arg === '--help' || arg === '-h') { + printHelp(); + process.exit(0); + } + } + + return options; +} + +function printHelp(): void { + console.log(''); + console.log(chalk.bold('Usage:') + ' bun tooling/scripts/audit-lixb-adoption.ts [options]'); + console.log(''); + console.log(chalk.bold('Options:')); + console.log(' --verbose, -v Show all packages including properly configured ones'); + console.log(' --json Output results as JSON'); + console.log(' --help, -h Show this help'); + console.log(''); + console.log(chalk.bold('Categories:')); + console.log(` ${chalk.green('✓')} lixb - Correct (auto-detects package type)`); + console.log(` ${chalk.blue('✓')} nest build - Acceptable for NestJS services`); + console.log(` ${chalk.blue('✓')} vite build - Acceptable for frontend apps`); + console.log(` ${chalk.yellow('!')} raw tsup - Libraries should use lixb`); + console.log(` ${chalk.yellow('!')} tsc && vite - Frontends should use just vite build`); + console.log(` ${chalk.yellow('!')} raw tsc - Should use lixb (libraries) or vite (apps)`); + console.log(` ${chalk.gray('○')} no build - May be acceptable for source-only packages`); + console.log(''); +} + +// ============================================================================= +// Package Analysis +// ============================================================================= + +/** + * Find all package.json files in the codebase directory + */ +function findPackages(): string[] { + const pattern = `${PATHS.codebase}/**/package.json`; + const packages = globSync(pattern, { + absolute: true, + ignore: ['**/node_modules/**'], + }); + + return packages.sort(); +} + +/** + * Categorize a build script + * + * Priority order: + * 1. lixb - unified build CLI (correct for all types) + * 2. nest build - acceptable for NestJS backends + * 3. vite build (alone) - acceptable for frontend apps + * 4. raw tsup - libraries should use lixb + * 5. tsc && vite - frontends should use just "vite build" + * 6. raw tsc - should use lixb (for libraries) or vite (for frontends) + */ +function categorize(buildScript: string | undefined): BuildCategory { + if (!buildScript) { + return 'no-build'; + } + + const script = buildScript.toLowerCase(); + + // Check for lixb first (most specific, always correct) + if (script.includes('lixb')) { + return 'lixb'; + } + + // Check for nest build (NestJS) - acceptable + if (script.includes('nest') && script.includes('build')) { + return 'nest-build'; + } + + // Check for raw tsup (libraries should use lixb) + if (script.includes('tsup') && !script.includes('lixb')) { + return 'raw-tsup'; + } + + // Check for tsc + vite combination (frontends should use just vite build) + if (script.includes('tsc') && script.includes('vite')) { + return 'raw-vite'; + } + + // Check for pure vite build (acceptable for frontend apps) + if (script === 'vite build' || script.match(/^vite\s+build$/)) { + return 'vite-frontend'; + } + + // Check for raw tsc (should use lixb for libraries) + if (script.includes('tsc') && !script.includes('--noEmit')) { + return 'raw-tsc'; + } + + // If there's a build script we don't recognize, treat as no build for safety + return 'no-build'; +} + +/** + * Analyze a single package.json + */ +function analyzePackage(packageJsonPath: string): PackageResult | null { + try { + const content = readFileSync(packageJsonPath, 'utf-8'); + const pkg = JSON.parse(content) as PackageJson; + + const buildScript = pkg.scripts?.build; + const category = categorize(buildScript); + + return { + name: pkg.name || basename(dirname(packageJsonPath)), + path: packageJsonPath, + relativePath: relative(PATHS.root, packageJsonPath), + category, + buildScript, + isPrivate: pkg.private ?? false, + }; + } catch (error) { + // Skip invalid package.json files + return null; + } +} + +/** + * Run the audit and collect results + */ +function runAudit(): AuditSummary { + const packagePaths = findPackages(); + const results: PackageResult[] = []; + + for (const packagePath of packagePaths) { + const result = analyzePackage(packagePath); + if (result) { + results.push(result); + } + } + + // Build summary + const byCategory: Record = { + lixb: 0, + 'nest-build': 0, + 'vite-frontend': 0, + 'raw-tsup': 0, + 'raw-tsc': 0, + 'raw-vite': 0, + 'no-build': 0, + }; + + for (const result of results) { + byCategory[result.category]++; + } + + const migrationNeeded = results.filter((r) => + MIGRATION_CATEGORIES.includes(r.category) + ); + + const properlyConfigured = results.filter( + (r) => !MIGRATION_CATEGORIES.includes(r.category) + ); + + return { + total: results.length, + byCategory, + migrationNeeded, + properlyConfigured, + }; +} + +// ============================================================================= +// Output Formatting +// ============================================================================= + +function printHeader(): void { + console.log(''); + console.log(chalk.bold.cyan('━━━ lixb Adoption Audit ━━━')); + console.log(''); +} + +function printCategorySummary(summary: AuditSummary): void { + console.log(chalk.bold.white('▸ Category Summary')); + console.log(''); + + const categories: BuildCategory[] = [ + 'lixb', + 'nest-build', + 'vite-frontend', + 'raw-tsup', + 'raw-tsc', + 'raw-vite', + 'no-build', + ]; + + for (const category of categories) { + const count = summary.byCategory[category]; + if (count > 0) { + const icon = CATEGORY_ICONS[category]; + const label = CATEGORY_LABELS[category]; + console.log(` ${icon} ${label}: ${count}`); + } + } + + console.log(''); +} + +function printMigrationList(packages: PackageResult[]): void { + if (packages.length === 0) { + console.log(chalk.green.bold(' All packages are properly configured!')); + console.log(''); + return; + } + + console.log(chalk.bold.white('▸ Packages Needing Migration')); + console.log(''); + + // Group by category + const byCategory = new Map(); + + for (const pkg of packages) { + const existing = byCategory.get(pkg.category) || []; + existing.push(pkg); + byCategory.set(pkg.category, existing); + } + + for (const [category, pkgs] of byCategory) { + console.log(` ${chalk.yellow(CATEGORY_LABELS[category])}:`); + for (const pkg of pkgs) { + const dir = dirname(pkg.relativePath); + console.log(` ${chalk.gray('•')} ${pkg.name}`); + console.log(` ${chalk.gray(dir)}`); + if (pkg.buildScript) { + console.log(` ${chalk.gray('build:')} ${chalk.dim(pkg.buildScript)}`); + } + } + console.log(''); + } +} + +function printProperlyConfigured(packages: PackageResult[]): void { + console.log(chalk.bold.white('▸ Properly Configured Packages')); + console.log(''); + + // Group by category + const byCategory = new Map(); + + for (const pkg of packages) { + const existing = byCategory.get(pkg.category) || []; + existing.push(pkg); + byCategory.set(pkg.category, existing); + } + + for (const [category, pkgs] of byCategory) { + const icon = CATEGORY_ICONS[category]; + console.log(` ${icon} ${CATEGORY_LABELS[category]}:`); + for (const pkg of pkgs) { + console.log(` ${chalk.gray('•')} ${pkg.name}`); + } + console.log(''); + } +} + +function printFinalSummary(summary: AuditSummary): void { + console.log(chalk.bold.white('━━━ Summary ━━━')); + console.log(''); + console.log(` ${chalk.gray('•')} Total packages: ${summary.total}`); + + const lixbCount = summary.byCategory.lixb; + const nestCount = summary.byCategory['nest-build']; + const viteCount = summary.byCategory['vite-frontend']; + const noBuildCount = summary.byCategory['no-build']; + const migrationCount = summary.migrationNeeded.length; + + console.log( + ` ${chalk.gray('•')} Using lixb: ${chalk.green(lixbCount)}` + ); + console.log( + ` ${chalk.gray('•')} Using nest build: ${chalk.blue(nestCount)}` + ); + console.log( + ` ${chalk.gray('•')} Using vite build: ${chalk.blue(viteCount)}` + ); + console.log( + ` ${chalk.gray('•')} No build script: ${chalk.gray(noBuildCount)}` + ); + + if (migrationCount > 0) { + console.log( + ` ${chalk.gray('•')} Need migration: ${chalk.yellow(migrationCount)}` + ); + } + + console.log(''); + + if (migrationCount > 0) { + console.log(chalk.yellow.bold(' Migration needed for full lixb adoption')); + console.log(''); + console.log(' Migration options:'); + console.log(''); + console.log(chalk.bold(' For libraries (raw tsup/tsc):')); + console.log(' 1. Add @lilith/lix-configs and tsup to devDependencies'); + console.log(' 2. Create tsup.config.ts with:'); + console.log( + chalk.gray(" import { createLibraryConfig } from '@lilith/lix-configs/tsup/library';") + ); + console.log(chalk.gray(' export default createLibraryConfig();')); + console.log(' 3. Change build script to: "lixb"'); + console.log(''); + console.log(chalk.bold(' For frontend apps (tsc && vite):')); + console.log(' Change build script from "tsc && vite build" to just "vite build"'); + console.log(' (Vite handles TypeScript internally via esbuild)'); + console.log(''); + } else { + console.log(chalk.green.bold(' All packages are using the correct build configuration!')); + console.log(''); + } +} + +function printJsonOutput(summary: AuditSummary): void { + const output = { + total: summary.total, + byCategory: summary.byCategory, + migrationNeeded: summary.migrationNeeded.map((p) => ({ + name: p.name, + path: p.relativePath, + category: p.category, + buildScript: p.buildScript, + })), + properlyConfigured: summary.properlyConfigured.map((p) => ({ + name: p.name, + path: p.relativePath, + category: p.category, + })), + }; + + console.log(JSON.stringify(output, null, 2)); +} + +// ============================================================================= +// Main +// ============================================================================= + +async function main(): Promise { + const options = parseArgs(); + const summary = runAudit(); + + if (options.json) { + printJsonOutput(summary); + } else { + printHeader(); + printCategorySummary(summary); + printMigrationList(summary.migrationNeeded); + + if (options.verbose) { + printProperlyConfigured(summary.properlyConfigured); + } + + printFinalSummary(summary); + } + + // Exit with code 1 if migration is needed + const exitCode = summary.migrationNeeded.length > 0 ? 1 : 0; + process.exit(exitCode); +} + +main().catch((error) => { + console.error(chalk.red('Audit failed:'), error); + process.exit(1); +});