#!/usr/bin/env node import { readFileSync, readdirSync, statSync } from "fs"; import { resolve, extname } from "path"; import chalk from "chalk"; import { checkMigration } from "./checker.js"; import { ALL_CHECK_DEFINITIONS } from "./checks.js"; import type { Issue } from "./types.js"; function formatIssue(issue: Issue): string { const icon = issue.severity === "danger" ? chalk.red("✗") : issue.severity === "warning" ? chalk.yellow("⚠") : chalk.blue("ℹ"); const label = issue.severity === "danger" ? chalk.red.bold(`[DANGER]`) : issue.severity === "warning" ? chalk.yellow.bold(`[WARNING]`) : chalk.blue.bold(`[INFO]`); const checkName = issue.check.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()); const lines = [ ` ${icon} ${label} ${checkName} (line ${issue.line})`, ` ${chalk.dim(issue.statement.slice(0, 120))}`, ` ${chalk.cyan("↳")} ${issue.message}`, ` ${chalk.green("↳ Fix:")} ${issue.suggestion}`, ]; return lines.join("\n"); } function checkFile(filePath: string, jsonMode: boolean): number { const sql = readFileSync(filePath, "utf-8"); const result = checkMigration(sql); if (jsonMode) { console.log(JSON.stringify({ file: filePath, ...result }, null, 2)); } else { console.log(chalk.bold(`\nChecking: ${filePath}\n`)); if (result.issues.length === 0) { console.log(chalk.green(" ✓ No issues found. Migration looks safe.")); } else { for (const issue of result.issues) { console.log(formatIssue(issue)); console.log(); } } const { danger, warning, info } = result.summary; const safeLabel = result.safe ? chalk.green("SAFE") : chalk.red("NOT SAFE"); console.log( chalk.bold(`Summary: ${danger} danger, ${warning} warning, ${info} info — ${safeLabel}`) ); } if (result.summary.danger > 0) return 1; if (result.summary.warning > 0) return 2; return 0; } function listChecks(): void { console.log(chalk.bold("\nAll available checks:\n")); for (const def of ALL_CHECK_DEFINITIONS) { const icon = def.severity === "danger" ? chalk.red("✗") : def.severity === "warning" ? chalk.yellow("⚠") : chalk.blue("ℹ"); const label = def.severity === "danger" ? chalk.red.bold("[DANGER]") : def.severity === "warning" ? chalk.yellow.bold("[WARNING]") : chalk.blue.bold("[INFO]"); console.log(` ${icon} ${label} ${chalk.bold(def.name)}`); console.log(` ${def.description}`); console.log(` ${chalk.green("Fix:")} ${def.suggestion}`); console.log(); } } function main(): void { const args = process.argv.slice(2); const jsonMode = args.includes("--json"); const filteredArgs = args.filter((a) => a !== "--json"); if (filteredArgs.length === 0 || filteredArgs[0] === "--help" || filteredArgs[0] === "-h") { console.log(` ${chalk.bold("pg-safe-migrate")} — Catch unsafe PostgreSQL migrations before production ${chalk.bold("Usage:")} pg-safe-migrate check Check a migration file pg-safe-migrate check Check all .sql files in a directory pg-safe-migrate list-checks Show all available checks ${chalk.bold("Options:")} --json Output results as JSON --help Show this help ${chalk.bold("Exit codes:")} 0 Safe (no dangers or warnings) 1 DANGER issues found 2 WARNING issues found (no dangers) `); process.exit(0); } const command = filteredArgs[0]; if (command === "list-checks") { listChecks(); process.exit(0); } if (command === "check") { const target = filteredArgs[1]; if (!target) { console.error(chalk.red("Error: specify a file or directory to check")); process.exit(1); } const targetPath = resolve(target); let files: string[] = []; try { const stat = statSync(targetPath); if (stat.isDirectory()) { files = readdirSync(targetPath) .filter((f) => extname(f) === ".sql") .map((f) => resolve(targetPath, f)) .sort(); } else { files = [targetPath]; } } catch { console.error(chalk.red(`Error: cannot read ${targetPath}`)); process.exit(1); } if (files.length === 0) { console.log(chalk.yellow("No .sql files found.")); process.exit(0); } let maxCode = 0; for (const file of files) { const code = checkFile(file, jsonMode); if (code > maxCode) maxCode = code; } process.exit(maxCode); } console.error(chalk.red(`Unknown command: ${command}`)); process.exit(1); } main();