feat(quality-rails): migrate @mosaic/quality-rails from v0 to v1 (#100)
Some checks failed
ci/woodpecker/push/ci Pipeline failed
Some checks failed
ci/woodpecker/push/ci Pipeline failed
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #100.
This commit is contained in:
202
packages/quality-rails/src/cli.ts
Normal file
202
packages/quality-rails/src/cli.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
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<boolean> {
|
||||
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 <path>', 'Project path')
|
||||
.option('--profile <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 <path>', '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 <path>', '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<void> {
|
||||
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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user