diff --git a/packages/quality-rails/package.json b/packages/quality-rails/package.json index 6f1b827..613154b 100644 --- a/packages/quality-rails/package.json +++ b/packages/quality-rails/package.json @@ -1,6 +1,7 @@ { "name": "@mosaic/quality-rails", "version": "0.0.0", + "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", "exports": { @@ -15,7 +16,11 @@ "typecheck": "tsc --noEmit", "test": "vitest run --passWithNoTests" }, + "dependencies": { + "commander": "^12.0.0" + }, "devDependencies": { + "@types/node": "^22.0.0", "typescript": "^5.8.0", "vitest": "^2.0.0" } diff --git a/packages/quality-rails/src/cli.ts b/packages/quality-rails/src/cli.ts new file mode 100644 index 0000000..938a963 --- /dev/null +++ b/packages/quality-rails/src/cli.ts @@ -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 { + 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 index 0c18d5d..3f61eb1 100644 --- a/packages/quality-rails/src/index.ts +++ b/packages/quality-rails/src/index.ts @@ -1 +1,5 @@ -export const VERSION = '0.0.0'; +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..e498d34 --- /dev/null +++ b/packages/quality-rails/src/scaffolder.ts @@ -0,0 +1,206 @@ +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..4366b2c --- /dev/null +++ b/packages/quality-rails/src/templates.ts @@ -0,0 +1,181 @@ +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/pnpm-lock.yaml b/pnpm-lock.yaml index 947de64..1dc3b20 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,10 +46,10 @@ importers: version: 13.0.2 '@mariozechner/pi-ai': specifier: ~0.57.1 - version: 0.57.1(ws@8.19.0)(zod@4.3.6) + version: 0.57.1(ws@8.19.0)(zod@3.25.76) '@mariozechner/pi-coding-agent': specifier: ~0.57.1 - version: 0.57.1(ws@8.19.0)(zod@4.3.6) + version: 0.57.1(ws@8.19.0)(zod@3.25.76) '@mosaic/auth': specifier: workspace:^ version: link:../../packages/auth @@ -395,7 +395,26 @@ importers: version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1) packages/prdy: + dependencies: + '@clack/prompts': + specifier: ^0.9.0 + version: 0.9.1 + commander: + specifier: ^12.0.0 + version: 12.1.0 + js-yaml: + specifier: ^4.1.0 + version: 4.1.1 + zod: + specifier: ^3.22.0 + version: 3.25.76 devDependencies: + '@types/js-yaml': + specifier: ^4.0.9 + version: 4.0.9 + '@types/node': + specifier: ^22.0.0 + version: 22.19.15 typescript: specifier: ^5.8.0 version: 5.9.3 @@ -404,7 +423,14 @@ importers: version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1) packages/quality-rails: + dependencies: + commander: + specifier: ^12.0.0 + version: 12.1.0 devDependencies: + '@types/node': + specifier: ^22.0.0 + version: 22.19.15 typescript: specifier: ^5.8.0 version: 5.9.3 @@ -706,6 +732,12 @@ packages: '@borewit/text-codec@0.2.2': resolution: {integrity: sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==} + '@clack/core@0.4.1': + resolution: {integrity: sha512-Pxhij4UXg8KSr7rPek6Zowm+5M22rbd2g1nfojHJkxp5YkFqiZ2+YLEM/XGVIzvGOcM0nqjIFxrpDwWRZYWYjA==} + + '@clack/prompts@0.9.1': + resolution: {integrity: sha512-JIpyaboYZeWYlyP0H+OoPPxd6nqueG/CmN6ixBiNFsIDHREevjIf0n0Ohh5gr5C8pEDknzgvz+pIJ8dMhzWIeg==} + '@discordjs/builders@1.13.1': resolution: {integrity: sha512-cOU0UDHc3lp/5nKByDxkmRiNZBpdp0kx55aarbiAfakfKJHlxv/yFW1zmIqCAmwH5CRlrH9iMFKJMpvW4DPB+w==} engines: {node: '>=16.11.0'} @@ -2779,6 +2811,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/js-yaml@4.0.9': + resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -3253,6 +3288,10 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + commander@13.1.0: resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} engines: {node: '>=18'} @@ -4658,6 +4697,9 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + slice-ansi@5.0.0: resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} engines: {node: '>=12'} @@ -5146,6 +5188,9 @@ packages: peerDependencies: zod: ^3.25 || ^4 + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} @@ -5158,11 +5203,11 @@ snapshots: '@alloc/quick-lru@5.2.0': {} - '@anthropic-ai/sdk@0.73.0(zod@4.3.6)': + '@anthropic-ai/sdk@0.73.0(zod@3.25.76)': dependencies: json-schema-to-ts: 3.1.1 optionalDependencies: - zod: 4.3.6 + zod: 3.25.76 '@aws-crypto/crc32@5.2.0': dependencies: @@ -5594,6 +5639,17 @@ snapshots: '@borewit/text-codec@0.2.2': {} + '@clack/core@0.4.1': + dependencies: + picocolors: 1.1.1 + sisteransi: 1.0.5 + + '@clack/prompts@0.9.1': + dependencies: + '@clack/core': 0.4.1 + picocolors: 1.1.1 + sisteransi: 1.0.5 + '@discordjs/builders@1.13.1': dependencies: '@discordjs/formatters': 0.6.2 @@ -6249,9 +6305,9 @@ snapshots: std-env: 3.10.0 yoctocolors: 2.1.2 - '@mariozechner/pi-agent-core@0.57.1(ws@8.19.0)(zod@4.3.6)': + '@mariozechner/pi-agent-core@0.57.1(ws@8.19.0)(zod@3.25.76)': dependencies: - '@mariozechner/pi-ai': 0.57.1(ws@8.19.0)(zod@4.3.6) + '@mariozechner/pi-ai': 0.57.1(ws@8.19.0)(zod@3.25.76) transitivePeerDependencies: - '@modelcontextprotocol/sdk' - aws-crt @@ -6261,9 +6317,9 @@ snapshots: - ws - zod - '@mariozechner/pi-ai@0.57.1(ws@8.19.0)(zod@4.3.6)': + '@mariozechner/pi-ai@0.57.1(ws@8.19.0)(zod@3.25.76)': dependencies: - '@anthropic-ai/sdk': 0.73.0(zod@4.3.6) + '@anthropic-ai/sdk': 0.73.0(zod@3.25.76) '@aws-sdk/client-bedrock-runtime': 3.1008.0 '@google/genai': 1.45.0 '@mistralai/mistralai': 1.14.1 @@ -6271,11 +6327,11 @@ snapshots: ajv: 8.18.0 ajv-formats: 3.0.1(ajv@8.18.0) chalk: 5.6.2 - openai: 6.26.0(ws@8.19.0)(zod@4.3.6) + openai: 6.26.0(ws@8.19.0)(zod@3.25.76) partial-json: 0.1.7 proxy-agent: 6.5.0 undici: 7.24.0 - zod-to-json-schema: 3.25.1(zod@4.3.6) + zod-to-json-schema: 3.25.1(zod@3.25.76) transitivePeerDependencies: - '@modelcontextprotocol/sdk' - aws-crt @@ -6285,11 +6341,11 @@ snapshots: - ws - zod - '@mariozechner/pi-coding-agent@0.57.1(ws@8.19.0)(zod@4.3.6)': + '@mariozechner/pi-coding-agent@0.57.1(ws@8.19.0)(zod@3.25.76)': dependencies: '@mariozechner/jiti': 2.6.5 - '@mariozechner/pi-agent-core': 0.57.1(ws@8.19.0)(zod@4.3.6) - '@mariozechner/pi-ai': 0.57.1(ws@8.19.0)(zod@4.3.6) + '@mariozechner/pi-agent-core': 0.57.1(ws@8.19.0)(zod@3.25.76) + '@mariozechner/pi-ai': 0.57.1(ws@8.19.0)(zod@3.25.76) '@mariozechner/pi-tui': 0.57.1 '@silvia-odwyer/photon-node': 0.3.4 chalk: 5.6.2 @@ -6330,8 +6386,8 @@ snapshots: '@mistralai/mistralai@1.14.1': dependencies: ws: 8.19.0 - zod: 4.3.6 - zod-to-json-schema: 3.25.1(zod@4.3.6) + zod: 3.25.76 + zod-to-json-schema: 3.25.1(zod@3.25.76) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -7659,6 +7715,8 @@ snapshots: '@types/estree@1.0.8': {} + '@types/js-yaml@4.0.9': {} + '@types/json-schema@7.0.15': {} '@types/memcached@2.2.10': @@ -8116,6 +8174,8 @@ snapshots: colorette@2.0.20: {} + commander@12.1.0: {} + commander@13.1.0: {} concat-map@0.0.1: {} @@ -9185,10 +9245,10 @@ snapshots: dependencies: mimic-function: 5.0.1 - openai@6.26.0(ws@8.19.0)(zod@4.3.6): + openai@6.26.0(ws@8.19.0)(zod@3.25.76): optionalDependencies: ws: 8.19.0 - zod: 4.3.6 + zod: 3.25.76 optionator@0.9.4: dependencies: @@ -9568,6 +9628,8 @@ snapshots: signal-exit@4.1.0: {} + sisteransi@1.0.5: {} + slice-ansi@5.0.0: dependencies: ansi-styles: 6.2.3 @@ -10021,8 +10083,10 @@ snapshots: yoga-layout@3.2.1: {} - zod-to-json-schema@3.25.1(zod@4.3.6): + zod-to-json-schema@3.25.1(zod@3.25.76): dependencies: - zod: 4.3.6 + zod: 3.25.76 + + zod@3.25.76: {} zod@4.3.6: {}