feat(cli): Add new CLI commands (dev, clean) for lifecycle management and workspace operations during LixB adoption

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Quinn Ftw 2026-02-05 15:00:48 -08:00
parent 341602ae7e
commit acade23ae4
6 changed files with 700 additions and 5 deletions

View file

@ -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<boolean> {
}
/**
* 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<CleanResult> {
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<CommandResult> {
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<CommandResult> {
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)`);

View file

@ -86,7 +86,7 @@ export async function devRestart(ctx: CommandContext): Promise<CommandResult> {
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 {

View file

@ -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<CommandResult> {
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<CommandResult> {
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 });
});
});
}

View file

@ -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';

View file

@ -53,6 +53,7 @@ const lazyCommands: Record<string, [string, string]> = {
'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

478
scripts/audit-lixb-adoption.ts Executable file
View file

@ -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<string, string>;
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<BuildCategory, number>;
migrationNeeded: PackageResult[];
properlyConfigured: PackageResult[];
}
// =============================================================================
// Constants
// =============================================================================
const CATEGORY_LABELS: Record<BuildCategory, string> = {
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<BuildCategory, string> = {
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<BuildCategory, number> = {
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<BuildCategory, PackageResult[]>();
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<BuildCategory, PackageResult[]>();
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<void> {
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);
});