import { constants } from 'node:fs'; import { access } from 'node:fs/promises'; import { resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { Command } from 'commander'; import { detectProjectKind } from './detect.js'; import { scaffoldQualityRails } from './scaffolder.js'; import type { ProjectKind, QualityProfile, RailsConfig } from './types.js'; const VALID_PROFILES: readonly QualityProfile[] = ['strict', 'standard', 'minimal']; async function fileExists(filePath: string): Promise { try { await access(filePath, constants.F_OK); return true; } catch { return false; } } function parseProfile(rawProfile: string): QualityProfile { if (VALID_PROFILES.includes(rawProfile as QualityProfile)) { return rawProfile as QualityProfile; } throw new Error(`Invalid profile: ${rawProfile}. Use one of ${VALID_PROFILES.join(', ')}.`); } function defaultLinters(kind: ProjectKind): string[] { if (kind === 'node') { return ['eslint', 'biome']; } if (kind === 'python') { return ['ruff']; } if (kind === 'rust') { return ['clippy']; } return []; } function defaultFormatters(kind: ProjectKind): string[] { if (kind === 'node') { return ['prettier']; } if (kind === 'python') { return ['black']; } if (kind === 'rust') { return ['rustfmt']; } return []; } function expectedFilesForKind(kind: ProjectKind): string[] { if (kind === 'node') { return ['.eslintrc', 'biome.json', '.githooks/pre-commit', 'PR-CHECKLIST.md']; } if (kind === 'python') { return ['pyproject.toml', '.githooks/pre-commit', 'PR-CHECKLIST.md']; } if (kind === 'rust') { return ['rustfmt.toml', '.githooks/pre-commit', 'PR-CHECKLIST.md']; } return ['.githooks/pre-commit', 'PR-CHECKLIST.md']; } function printScaffoldResult(config: RailsConfig, filesWritten: string[], warnings: string[], commandsToRun: string[]): void { console.log(`[quality-rails] initialized at ${config.projectPath}`); console.log(`kind=${config.kind} profile=${config.profile}`); if (filesWritten.length > 0) { console.log('files written:'); for (const filePath of filesWritten) { console.log(` - ${filePath}`); } } if (commandsToRun.length > 0) { console.log('run next:'); for (const command of commandsToRun) { console.log(` - ${command}`); } } if (warnings.length > 0) { console.log('warnings:'); for (const warning of warnings) { console.log(` - ${warning}`); } } } export function createQualityRailsCli(): Command { const program = new Command('mosaic'); const qualityRails = program.command('quality-rails').description('Manage quality rails scaffolding'); qualityRails .command('init') .requiredOption('--project ', 'Project path') .option('--profile ', 'strict|standard|minimal', 'standard') .action(async (options: { project: string; profile: string }) => { const profile = parseProfile(options.profile); const projectPath = resolve(options.project); const kind = await detectProjectKind(projectPath); const config: RailsConfig = { projectPath, kind, profile, linters: defaultLinters(kind), formatters: defaultFormatters(kind), hooks: true, }; const result = await scaffoldQualityRails(config); printScaffoldResult(config, result.filesWritten, result.warnings, result.commandsToRun); }); qualityRails .command('check') .requiredOption('--project ', 'Project path') .action(async (options: { project: string }) => { const projectPath = resolve(options.project); const kind = await detectProjectKind(projectPath); const expected = expectedFilesForKind(kind); const missing: string[] = []; for (const relativePath of expected) { const exists = await fileExists(resolve(projectPath, relativePath)); if (!exists) { missing.push(relativePath); } } if (missing.length > 0) { console.error('[quality-rails] missing files:'); for (const relativePath of missing) { console.error(` - ${relativePath}`); } process.exitCode = 1; return; } console.log(`[quality-rails] all expected files present for ${kind} project`); }); qualityRails .command('doctor') .requiredOption('--project ', 'Project path') .action(async (options: { project: string }) => { const projectPath = resolve(options.project); const kind = await detectProjectKind(projectPath); const expected = expectedFilesForKind(kind); console.log(`[quality-rails] doctor for ${projectPath}`); console.log(`detected project kind: ${kind}`); for (const relativePath of expected) { const exists = await fileExists(resolve(projectPath, relativePath)); console.log(` - ${exists ? 'ok' : 'missing'}: ${relativePath}`); } if (kind === 'unknown') { console.log('recommendation: add package.json, pyproject.toml, or Cargo.toml for better defaults.'); } }); return program; } export async function runQualityRailsCli(argv: string[] = process.argv): Promise { const program = createQualityRailsCli(); await program.parseAsync(argv); } const entryPath = process.argv[1] ? resolve(process.argv[1]) : ''; if (entryPath.length > 0 && entryPath === fileURLToPath(import.meta.url)) { runQualityRailsCli().catch((error: unknown) => { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); }); }