diff --git a/packages/quality-rails/eslint.config.js b/packages/quality-rails/eslint.config.js new file mode 100644 index 0000000..b9b92c4 --- /dev/null +++ b/packages/quality-rails/eslint.config.js @@ -0,0 +1,20 @@ +import tsEslintPlugin from '@typescript-eslint/eslint-plugin'; +import tsEslintParser from '@typescript-eslint/parser'; + +export default [ + { + files: ['src/**/*.ts'], + languageOptions: { + parser: tsEslintParser, + sourceType: 'module', + ecmaVersion: 'latest', + }, + plugins: { + '@typescript-eslint': tsEslintPlugin, + }, + rules: { + 'no-console': 'off', + '@typescript-eslint/no-explicit-any': 'off', + }, + }, +]; diff --git a/packages/quality-rails/package.json b/packages/quality-rails/package.json new file mode 100644 index 0000000..5f508ac --- /dev/null +++ b/packages/quality-rails/package.json @@ -0,0 +1,30 @@ +{ + "name": "@mosaic/quality-rails", + "version": "0.1.0", + "type": "module", + "description": "Mosaic quality rails - TypeScript code quality scaffolder", + "scripts": { + "build": "tsc -p tsconfig.json", + "typecheck": "tsc --noEmit", + "lint": "eslint src/", + "test": "vitest run" + }, + "dependencies": { + "@mosaic/types": "workspace:*", + "commander": "^13", + "js-yaml": "^4" + }, + "devDependencies": { + "@types/node": "^22", + "@types/js-yaml": "^4", + "@typescript-eslint/eslint-plugin": "^8", + "@typescript-eslint/parser": "^8", + "eslint": "^9", + "typescript": "^5", + "vitest": "^2" + }, + "publishConfig": { + "registry": "https://git.mosaicstack.dev/api/packages/mosaic/npm", + "access": "public" + } +} diff --git a/packages/quality-rails/src/cli.ts b/packages/quality-rails/src/cli.ts new file mode 100644 index 0000000..c610bda --- /dev/null +++ b/packages/quality-rails/src/cli.ts @@ -0,0 +1,193 @@ +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); + }); +} diff --git a/packages/quality-rails/src/detect.ts b/packages/quality-rails/src/detect.ts new file mode 100644 index 0000000..d7d5393 --- /dev/null +++ b/packages/quality-rails/src/detect.ts @@ -0,0 +1,30 @@ +import { constants } from 'node:fs'; +import { access } from 'node:fs/promises'; +import { join } from 'node:path'; + +import type { ProjectKind } from './types.js'; + +async function fileExists(filePath: string): Promise { + try { + await access(filePath, constants.F_OK); + return true; + } catch { + return false; + } +} + +export async function detectProjectKind(projectPath: string): Promise { + if (await fileExists(join(projectPath, 'package.json'))) { + return 'node'; + } + + if (await fileExists(join(projectPath, 'pyproject.toml'))) { + return 'python'; + } + + if (await fileExists(join(projectPath, 'Cargo.toml'))) { + return 'rust'; + } + + return 'unknown'; +} diff --git a/packages/quality-rails/src/index.ts b/packages/quality-rails/src/index.ts new file mode 100644 index 0000000..3f61eb1 --- /dev/null +++ b/packages/quality-rails/src/index.ts @@ -0,0 +1,5 @@ +export * from './cli.js'; +export * from './detect.js'; +export * from './scaffolder.js'; +export * from './templates.js'; +export * from './types.js'; diff --git a/packages/quality-rails/src/scaffolder.ts b/packages/quality-rails/src/scaffolder.ts new file mode 100644 index 0000000..b9a9527 --- /dev/null +++ b/packages/quality-rails/src/scaffolder.ts @@ -0,0 +1,201 @@ +import { spawn } from 'node:child_process'; +import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; + +import { + biomeTemplate, + eslintTemplate, + prChecklistTemplate, + preCommitHookTemplate, + pyprojectSection, + rustfmtTemplate, +} from './templates.js'; +import type { RailsConfig, ScaffoldResult } from './types.js'; + +const PYPROJECT_START_MARKER = '# >>> mosaic-quality-rails >>>'; +const PYPROJECT_END_MARKER = '# <<< mosaic-quality-rails <<<'; + +async function ensureDirectory(filePath: string): Promise { + await mkdir(dirname(filePath), { recursive: true }); +} + +async function writeRelativeFile( + projectPath: string, + relativePath: string, + contents: string, + result: ScaffoldResult, +): Promise { + const absolutePath = join(projectPath, relativePath); + await ensureDirectory(absolutePath); + await writeFile(absolutePath, contents, { encoding: 'utf8', mode: 0o644 }); + result.filesWritten.push(relativePath); +} + +async function upsertPyproject( + projectPath: string, + profile: RailsConfig['profile'], + result: ScaffoldResult, +): Promise { + const pyprojectPath = join(projectPath, 'pyproject.toml'); + const nextSection = pyprojectSection(profile); + + let previous = ''; + try { + previous = await readFile(pyprojectPath, 'utf8'); + } catch { + previous = ''; + } + + const existingStart = previous.indexOf(PYPROJECT_START_MARKER); + const existingEnd = previous.indexOf(PYPROJECT_END_MARKER); + + if (existingStart >= 0 && existingEnd > existingStart) { + const before = previous.slice(0, existingStart).trimEnd(); + const after = previous.slice(existingEnd + PYPROJECT_END_MARKER.length).trimStart(); + const rebuilt = [before, nextSection.trim(), after] + .filter((segment) => segment.length > 0) + .join('\n\n'); + await writeRelativeFile(projectPath, 'pyproject.toml', `${rebuilt}\n`, result); + return; + } + + const separator = previous.trim().length > 0 ? '\n\n' : ''; + await writeRelativeFile(projectPath, 'pyproject.toml', `${previous.trimEnd()}${separator}${nextSection}`, result); +} + +function runCommand(command: string, args: string[], cwd: string): Promise { + return new Promise((resolve, reject) => { + const child = spawn(command, args, { + cwd, + stdio: 'ignore', + env: process.env, + }); + + child.on('error', reject); + child.on('exit', (code) => { + if (code === 0) { + resolve(); + return; + } + reject(new Error(`Command failed (${code ?? 'unknown'}): ${command} ${args.join(' ')}`)); + }); + }); +} + +function buildNodeDevDependencies(config: RailsConfig): string[] { + const dependencies = new Set(); + + if (config.linters.some((linter) => linter.toLowerCase() === 'eslint')) { + dependencies.add('eslint'); + dependencies.add('@typescript-eslint/parser'); + dependencies.add('@typescript-eslint/eslint-plugin'); + } + + if (config.linters.some((linter) => linter.toLowerCase() === 'biome')) { + dependencies.add('@biomejs/biome'); + } + + if (config.formatters.some((formatter) => formatter.toLowerCase() === 'prettier')) { + dependencies.add('prettier'); + } + + if (config.hooks) { + dependencies.add('husky'); + } + + return [...dependencies]; +} + +async function installNodeDependencies(config: RailsConfig, result: ScaffoldResult): Promise { + const dependencies = buildNodeDevDependencies(config); + if (dependencies.length === 0) { + return; + } + + const commandLine = `pnpm add -D ${dependencies.join(' ')}`; + + if (process.env.MOSAIC_QUALITY_RAILS_SKIP_INSTALL === '1') { + result.commandsToRun.push(commandLine); + return; + } + + try { + await runCommand('pnpm', ['add', '-D', ...dependencies], config.projectPath); + } catch (error) { + result.warnings.push( + `Failed to auto-install Node dependencies: ${error instanceof Error ? error.message : String(error)}`, + ); + result.commandsToRun.push(commandLine); + } +} + +export async function scaffoldQualityRails(config: RailsConfig): Promise { + const result: ScaffoldResult = { + filesWritten: [], + commandsToRun: [], + warnings: [], + }; + + const normalizedLinters = new Set(config.linters.map((linter) => linter.toLowerCase())); + + if (config.kind === 'node') { + if (normalizedLinters.has('eslint')) { + await writeRelativeFile( + config.projectPath, + '.eslintrc', + eslintTemplate(config.profile), + result, + ); + } + + if (normalizedLinters.has('biome')) { + await writeRelativeFile( + config.projectPath, + 'biome.json', + biomeTemplate(config.profile), + result, + ); + } + + await installNodeDependencies(config, result); + } + + if (config.kind === 'python') { + await upsertPyproject(config.projectPath, config.profile, result); + } + + if (config.kind === 'rust') { + await writeRelativeFile( + config.projectPath, + 'rustfmt.toml', + rustfmtTemplate(config.profile), + result, + ); + } + + if (config.hooks) { + await writeRelativeFile( + config.projectPath, + '.githooks/pre-commit', + preCommitHookTemplate(config), + result, + ); + await chmod(join(config.projectPath, '.githooks/pre-commit'), 0o755); + result.commandsToRun.push('git config core.hooksPath .githooks'); + } + + await writeRelativeFile( + config.projectPath, + 'PR-CHECKLIST.md', + prChecklistTemplate(config.profile), + result, + ); + + if (config.kind === 'unknown') { + result.warnings.push( + 'Unable to detect project kind. Generated generic rails only (hooks + PR checklist).', + ); + } + + return result; +} diff --git a/packages/quality-rails/src/templates.ts b/packages/quality-rails/src/templates.ts new file mode 100644 index 0000000..2f52f53 --- /dev/null +++ b/packages/quality-rails/src/templates.ts @@ -0,0 +1,182 @@ +import type { QualityProfile, RailsConfig } from './types.js'; + +const PROFILE_TO_MAX_WARNINGS: Record = { + strict: 0, + standard: 10, + minimal: 50, +}; + +const PROFILE_TO_LINE_LENGTH: Record = { + strict: 100, + standard: 110, + minimal: 120, +}; + +export function eslintTemplate(profile: QualityProfile): string { + return `${JSON.stringify( + { + root: true, + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], + extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], + env: { + node: true, + es2022: true, + }, + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + rules: { + 'no-console': profile === 'strict' ? 'error' : 'warn', + '@typescript-eslint/no-explicit-any': + profile === 'minimal' ? 'off' : profile === 'strict' ? 'error' : 'warn', + '@typescript-eslint/explicit-function-return-type': + profile === 'strict' ? 'warn' : 'off', + 'max-lines-per-function': [ + profile === 'minimal' ? 'off' : 'warn', + { + max: profile === 'strict' ? 60 : 100, + skipBlankLines: true, + skipComments: true, + }, + ], + }, + }, + null, + 2, + )}\n`; +} + +export function biomeTemplate(profile: QualityProfile): string { + return `${JSON.stringify( + { + '$schema': 'https://biomejs.dev/schemas/1.8.3/schema.json', + formatter: { + enabled: true, + indentStyle: 'space', + indentWidth: 2, + lineWidth: PROFILE_TO_LINE_LENGTH[profile], + }, + linter: { + enabled: true, + rules: { + recommended: true, + suspicious: { + noConsole: profile === 'strict' ? 'error' : 'warn', + }, + complexity: { + noExcessiveCognitiveComplexity: + profile === 'strict' ? 'warn' : profile === 'standard' ? 'info' : 'off', + }, + }, + }, + javascript: { + formatter: { + quoteStyle: 'single', + trailingCommas: 'all', + }, + }, + }, + null, + 2, + )}\n`; +} + +export function pyprojectSection(profile: QualityProfile): string { + const lineLength = PROFILE_TO_LINE_LENGTH[profile]; + return [ + '# >>> mosaic-quality-rails >>>', + '[tool.ruff]', + `line-length = ${lineLength}`, + 'target-version = "py311"', + '', + '[tool.ruff.lint]', + 'select = ["E", "F", "I", "UP", "B"]', + `ignore = ${profile === 'minimal' ? '[]' : '["E501"]'}`, + '', + '[tool.black]', + `line-length = ${lineLength}`, + '', + '# <<< mosaic-quality-rails <<<', + '', + ].join('\n'); +} + +export function rustfmtTemplate(profile: QualityProfile): string { + const maxWidth = PROFILE_TO_LINE_LENGTH[profile]; + const useSmallHeuristics = profile === 'strict' ? 'Max' : 'Default'; + + return [ + `max_width = ${maxWidth}`, + `use_small_heuristics = "${useSmallHeuristics}"`, + `imports_granularity = "${profile === 'minimal' ? 'Crate' : 'Module'}"`, + `group_imports = "${profile === 'strict' ? 'StdExternalCrate' : 'Preserve'}"`, + '', + ].join('\n'); +} + +function resolveHookCommands(config: RailsConfig): string[] { + const commands: string[] = []; + + if (config.kind === 'node') { + if (config.linters.some((linter) => linter.toLowerCase() === 'eslint')) { + commands.push('pnpm lint'); + } + if (config.linters.some((linter) => linter.toLowerCase() === 'biome')) { + commands.push('pnpm biome check .'); + } + if (config.formatters.some((formatter) => formatter.toLowerCase() === 'prettier')) { + commands.push('pnpm prettier --check .'); + } + commands.push('pnpm test --if-present'); + } + + if (config.kind === 'python') { + commands.push('ruff check .'); + commands.push('black --check .'); + } + + if (config.kind === 'rust') { + commands.push('cargo fmt --check'); + commands.push('cargo clippy --all-targets --all-features -- -D warnings'); + } + + if (commands.length === 0) { + commands.push('echo "No quality commands configured for this project kind"'); + } + + return commands; +} + +export function preCommitHookTemplate(config: RailsConfig): string { + const commands = resolveHookCommands(config) + .map((command) => `${command} || exit 1`) + .join('\n'); + + return [ + '#!/usr/bin/env sh', + 'set -eu', + '', + 'echo "[quality-rails] Running pre-commit checks..."', + commands, + 'echo "[quality-rails] Checks passed."', + '', + ].join('\n'); +} + +export function prChecklistTemplate(profile: QualityProfile): string { + return [ + '# Code Review Checklist', + '', + `Profile: **${profile}**`, + '', + '- [ ] Requirements mapped to tests', + '- [ ] Error handling covers unhappy paths', + '- [ ] Lint and typecheck are clean', + '- [ ] Test suite passes', + '- [ ] Security-sensitive paths reviewed', + `- [ ] Warnings count <= ${PROFILE_TO_MAX_WARNINGS[profile]}`, + '', + ].join('\n'); +} diff --git a/packages/quality-rails/src/types.ts b/packages/quality-rails/src/types.ts new file mode 100644 index 0000000..b487c5d --- /dev/null +++ b/packages/quality-rails/src/types.ts @@ -0,0 +1,18 @@ +export type ProjectKind = 'node' | 'python' | 'rust' | 'unknown'; + +export type QualityProfile = 'strict' | 'standard' | 'minimal'; + +export interface RailsConfig { + projectPath: string; + kind: ProjectKind; + profile: QualityProfile; + linters: string[]; + formatters: string[]; + hooks: boolean; +} + +export interface ScaffoldResult { + filesWritten: string[]; + commandsToRun: string[]; + warnings: string[]; +} diff --git a/packages/quality-rails/templates/.gitkeep b/packages/quality-rails/templates/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/packages/quality-rails/tests/detect.test.ts b/packages/quality-rails/tests/detect.test.ts new file mode 100644 index 0000000..389be32 --- /dev/null +++ b/packages/quality-rails/tests/detect.test.ts @@ -0,0 +1,40 @@ +import { mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +import { detectProjectKind } from '../src/detect.js'; + +async function withTempDir(run: (directory: string) => Promise): Promise { + const directory = await mkdtemp(join(tmpdir(), 'quality-rails-detect-')); + try { + await run(directory); + } finally { + await rm(directory, { recursive: true, force: true }); + } +} + +describe('detectProjectKind', () => { + it('returns node when package.json exists', async () => { + await withTempDir(async (directory) => { + await writeFile(join(directory, 'package.json'), '{"name":"fixture"}\n', 'utf8'); + + await expect(detectProjectKind(directory)).resolves.toBe('node'); + }); + }); + + it('returns python when pyproject.toml exists and package.json does not', async () => { + await withTempDir(async (directory) => { + await writeFile(join(directory, 'pyproject.toml'), '[project]\nname = "fixture"\n', 'utf8'); + + await expect(detectProjectKind(directory)).resolves.toBe('python'); + }); + }); + + it('returns unknown when no known project files exist', async () => { + await withTempDir(async (directory) => { + await expect(detectProjectKind(directory)).resolves.toBe('unknown'); + }); + }); +}); diff --git a/packages/quality-rails/tests/scaffolder.test.ts b/packages/quality-rails/tests/scaffolder.test.ts new file mode 100644 index 0000000..ab2bd8c --- /dev/null +++ b/packages/quality-rails/tests/scaffolder.test.ts @@ -0,0 +1,57 @@ +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +import { scaffoldQualityRails } from '../src/scaffolder.js'; +import type { RailsConfig } from '../src/types.js'; + +async function withTempDir(run: (directory: string) => Promise): Promise { + const directory = await mkdtemp(join(tmpdir(), 'quality-rails-scaffold-')); + try { + await run(directory); + } finally { + await rm(directory, { recursive: true, force: true }); + } +} + +describe('scaffoldQualityRails', () => { + it('writes expected node quality rails files', async () => { + await withTempDir(async (directory) => { + await writeFile(join(directory, 'package.json'), '{"name":"fixture"}\n', 'utf8'); + + const previous = process.env.MOSAIC_QUALITY_RAILS_SKIP_INSTALL; + process.env.MOSAIC_QUALITY_RAILS_SKIP_INSTALL = '1'; + + const config: RailsConfig = { + projectPath: directory, + kind: 'node', + profile: 'strict', + linters: ['eslint', 'biome'], + formatters: ['prettier'], + hooks: true, + }; + + const result = await scaffoldQualityRails(config); + + process.env.MOSAIC_QUALITY_RAILS_SKIP_INSTALL = previous; + + await expect(readFile(join(directory, '.eslintrc'), 'utf8')).resolves.toContain('parser'); + await expect(readFile(join(directory, 'biome.json'), 'utf8')).resolves.toContain('"formatter"'); + await expect(readFile(join(directory, '.githooks', 'pre-commit'), 'utf8')).resolves.toContain('pnpm lint'); + await expect(readFile(join(directory, 'PR-CHECKLIST.md'), 'utf8')).resolves.toContain('Code Review Checklist'); + + expect(result.filesWritten).toEqual( + expect.arrayContaining([ + '.eslintrc', + 'biome.json', + '.githooks/pre-commit', + 'PR-CHECKLIST.md', + ]), + ); + expect(result.commandsToRun).toContain('git config core.hooksPath .githooks'); + expect(result.warnings).toHaveLength(0); + }); + }); +}); diff --git a/packages/quality-rails/tsconfig.json b/packages/quality-rails/tsconfig.json new file mode 100644 index 0000000..972facb --- /dev/null +++ b/packages/quality-rails/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { "outDir": "dist", "rootDir": "src" }, + "include": ["src"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dcb92fe..d4f57ce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -148,6 +148,40 @@ importers: specifier: ^2 version: 2.1.9(@types/node@22.19.15) + packages/quality-rails: + dependencies: + '@mosaic/types': + specifier: workspace:* + version: link:../types + commander: + specifier: ^13 + version: 13.1.0 + js-yaml: + specifier: ^4 + version: 4.1.1 + devDependencies: + '@types/js-yaml': + specifier: ^4 + version: 4.0.9 + '@types/node': + specifier: ^22 + version: 22.19.15 + '@typescript-eslint/eslint-plugin': + specifier: ^8 + version: 8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8 + version: 8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + eslint: + specifier: ^9 + version: 9.39.4(jiti@2.6.1) + typescript: + specifier: ^5 + version: 5.9.3 + vitest: + specifier: ^2 + version: 2.1.9(@types/node@22.19.15) + packages/queue: dependencies: '@modelcontextprotocol/sdk': @@ -946,6 +980,14 @@ packages: '@types/node@22.19.15': resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==} + '@typescript-eslint/eslint-plugin@8.56.1': + resolution: {integrity: sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.56.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/parser@8.56.1': resolution: {integrity: sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -969,6 +1011,13 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/type-utils@8.56.1': + resolution: {integrity: sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/types@8.56.1': resolution: {integrity: sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -979,6 +1028,13 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/utils@8.56.1': + resolution: {integrity: sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/visitor-keys@8.56.1': resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1547,6 +1603,10 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -3004,6 +3064,22 @@ snapshots: dependencies: undici-types: 6.21.0 + '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.56.1 + '@typescript-eslint/type-utils': 8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.56.1 + eslint: 9.39.4(jiti@2.6.1) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.56.1 @@ -3034,6 +3110,18 @@ snapshots: dependencies: typescript: 5.9.3 + '@typescript-eslint/type-utils@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.4(jiti@2.6.1) + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/types@8.56.1': {} '@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)': @@ -3051,6 +3139,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/utils@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.56.1 + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + eslint: 9.39.4(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/visitor-keys@8.56.1': dependencies: '@typescript-eslint/types': 8.56.1 @@ -3689,6 +3788,8 @@ snapshots: ignore@5.3.2: {} + ignore@7.0.5: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1