diff --git a/.gitignore b/.gitignore index db169364..9d5b7140 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ release /tsc /openchamber@* local-dev* +.tmp/ # Editor directories and files .vscode/* !.vscode/extensions.json diff --git a/.opencode/commands/rd-fixes.md b/.opencode/commands/rd-fixes.md new file mode 100644 index 00000000..86231f19 --- /dev/null +++ b/.opencode/commands/rd-fixes.md @@ -0,0 +1,63 @@ +--- +description: Create a React Doctor diagnostics cleanup PR from the next generated batch +agent: build +--- + +You are working in the OpenChamber repository. + +Goal: reduce React Doctor diagnostics in a small, reviewable maintenance PR. + +Start by running: + +`bun run doctor -- next-batch --min-issues 75 --max-issues 120` + +Use the command output as the source of truth for this task scope. + +Workflow: +- Before generating the batch, switch to `main` and pull the latest remote changes. +- Read the `next-batch` output carefully. +- Use the exact `Run ID`, `Batch name`, `Branch name`, and `PR title` printed by the command. +- Create the branch using the printed `Branch name`. +- Work only on the selected files listed in the batch output. +- Treat the selected files as complete-file scope. Do not cherry-pick only the first N diagnostics. +- Fix as many diagnostics as practical in the selected files. Your default should be to fix selected diagnostics, not to skip them. +- Prefer direct, behavior-preserving fixes: missing effect cleanup, mutable effect dependencies, accessibility issues with semantic fixes, local performance improvements, Tailwind shorthand replacements, component extraction when the boundary is clear, dead-code removal after verifying no references, and reducer or derived-state cleanup when the state relationship is local and clear. +- Handle larger diagnostics deliberately instead of skipping them: for component splits, extract the smallest coherent subcomponent that reduces the diagnostic while preserving props/state flow; for dead code, verify references with search before deleting exports, types, or files; for state architecture issues, prefer the smallest local reducer or derived-state simplification that preserves behavior; for render-function extraction, extract only stable render helpers that do not depend on large implicit closure state, or pass explicit props; for behavior-sensitive diagnostics, read the surrounding code first and preserve existing runtime behavior. +- Skip a diagnostic only when the fix would require broad architectural changes, unclear behavior changes, or changes outside the selected batch scope. If skipped, mention it in the PR body. +- Do not suppress React Doctor diagnostics unless there is a clear false positive. +- If a listed diagnostic requires changes outside the selected files, make only the minimal required supporting change. Do not expand the cleanup scope. + +After edits, run: + +`bun run doctor -- check-batch --run ` + +Then run: + +`bun run type-check` + +`bun run lint` + +Validation and delivery: +- Confirm selected files have fewer diagnostics than before. +- If validation fails, fix failures only if the fixes stay within the task scope. Otherwise stop and report the blocker. +- Commit the changes with a concise message. +- Push the branch. +- Create exactly one PR with `gh pr create` using the exact printed `PR title`. +- After the PR is created, switch back to `main` and pull the latest remote changes again. + +PR requirements: +- Use the exact printed `PR title`. +- Include the `Run ID`, `Batch name`, and `Branch name`. +- Include selected files. +- Include diagnostics fixed according to `check-batch`. +- Include remaining diagnostics in selected files. +- Include validation results for `bun run type-check` and `bun run lint`. +- Include any skipped diagnostics and why. + +Constraints: +- Keep the PR small and reviewable. +- Do not auto-merge. +- Do not modify unrelated files except minimal supporting changes required by selected-file fixes. +- Do not run broad formatting. +- Do not fix diagnostics outside the selected files. +- Leave `.tmp/react-doctor/runs//` intact after creating the PR. These files are the handoff for the review follow-up task. diff --git a/.opencode/commands/rd-follow-up.md b/.opencode/commands/rd-follow-up.md new file mode 100644 index 00000000..b3ee888b --- /dev/null +++ b/.opencode/commands/rd-follow-up.md @@ -0,0 +1,56 @@ +--- +description: Follow up on a React Doctor PR by addressing Greptile review feedback +agent: build +--- + +You are working in the OpenChamber repository. + +Goal: follow up on an existing React Doctor maintenance PR, address Greptile/review bot feedback, and clean up the local batch handoff files when done. + +Inspect local React Doctor batch handoff files: + +`find .tmp/react-doctor/runs -maxdepth 2 -name batch.json -print 2>/dev/null || true` + +Workflow: +- Read the available `.tmp/react-doctor/runs/*/batch.json` files. +- Find the most recent batch that has `branchName`, `batchName`, and `prTitle`. +- Read its `Run ID`, `Batch name`, `Branch name`, `PR title`, and selected files. +- Use `gh` to find the open PR for that branch or title. +- If no open PR exists for the batch, stop and report that there is no PR to follow up. +- Switch to the batch branch using the exact `branchName`. +- Pull or update the branch from remote if needed. +- Use `gh` to inspect PR review comments, PR issue comments, review threads if available, and check run summaries if relevant. +- Focus specifically on Greptile/review bot feedback and actionable reviewer comments. +- Address actionable comments with minimal follow-up fixes. +- Keep changes within the original selected files whenever possible. +- If a review comment requires changes outside the selected files, make only the minimal required supporting change. +- Do not perform unrelated cleanup. +- Do not rewrite the original PR. +- Do not force-push. + +After fixes, run: + +`bun run doctor -- check-batch --run ` + +`bun run type-check` + +`bun run lint` + +Delivery: +- Commit follow-up fixes with a concise message. +- Push the branch. +- Reply to addressed review comments using `gh`. +- For each specific review comment you addressed, reply with what was changed and the follow-up commit hash. +- If the feedback was a general PR comment, add one general PR comment summarizing what was addressed, commit hashes, and validation results. +- If a comment is intentionally not addressed, reply with a concise reason. +- After successful push and replies, delete only the completed batch handoff directory: `.tmp/react-doctor/runs//`. +- After the follow-up is complete, switch back to `main` and pull the latest remote changes. + +Constraints: +- Work on exactly one React Doctor batch PR. +- Prefer the most recent batch with an open PR. +- Do not auto-merge. +- Do not close the PR. +- Do not delete handoff files until comments are addressed, validation passes, and follow-up commits are pushed. +- Do not delete unrelated `.tmp/react-doctor/runs/*` directories. +- If validation fails and cannot be fixed safely within scope, do not delete the handoff directory. diff --git a/package.json b/package.json index afb5f796..e9684b9e 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "vscode:package": "bun run --cwd packages/vscode package", "vscode:type-check": "bun run --cwd packages/vscode type-check", "docs:validate": "node scripts/docs/validate-docs.mjs", + "doctor": "node scripts/react-doctor.mjs", "icons:sprite": "node scripts/generate-file-type-sprite.mjs", "icons:generate": "bun run scripts/generate-icon-sprite.mjs", "themes:port:opencode": "tsx scripts/port-opencode-theme.ts", diff --git a/scripts/react-doctor.mjs b/scripts/react-doctor.mjs new file mode 100644 index 00000000..f800ebb4 --- /dev/null +++ b/scripts/react-doctor.mjs @@ -0,0 +1,490 @@ +#!/usr/bin/env node + +import { execFileSync } from "node:child_process"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; + +const PROJECT_NAME = "openchamber-monorepo"; +const RUNS_DIR = join(process.cwd(), ".tmp", "react-doctor", "runs"); + +const PRIORITY_RULES = new Map([ + ["effect-needs-cleanup", 100], + ["no-mutable-in-deps", 95], + ["click-events-have-key-events", 90], + ["no-static-element-interactions", 88], + ["no-direct-state-mutation", 86], + ["no-secrets-in-client-code", 85], + ["no-danger", 84], + ["no-prevent-default", 82], + ["async-await-in-loop", 80], + ["server-sequential-independent-await", 80], + ["async-parallel", 78], + ["async-defer-await", 76], + ["js-set-map-lookups", 75], + ["js-index-maps", 74], + ["js-cache-property-access", 72], + ["advanced-event-handler-refs", 70], + ["client-passive-event-listeners", 70], + ["js-combine-iterations", 68], + ["js-flatmap-filter", 66], + ["js-batch-dom-css", 66], + ["js-cache-storage", 64], + ["js-hoist-intl", 64], + ["js-hoist-regexp", 64], + ["js-length-check-first", 64], + ["js-min-max-loop", 64], + ["js-tosorted-immutable", 62], + ["no-fetch-in-effect", 62], + ["rerender-state-only-in-handlers", 62], + ["rerender-functional-setstate", 60], + ["rerender-lazy-state-init", 60], + ["rerender-memo-before-early-return", 60], + ["rerender-memo-with-default-value", 60], + ["rendering-hydration-mismatch-time", 58], + ["rendering-hydration-no-flicker", 58], + ["rendering-usetransition-loading", 58], + ["rendering-conditional-render", 56], + ["rendering-svg-precision", 56], + ["no-unknown-property", 55], + ["no-autofocus", 54], + ["no-array-index-as-key", 52], + ["no-react19-deprecated-apis", 52], + ["client-localstorage-no-version", 50], + ["no-dynamic-import-path", 50], + ["no-flush-sync", 50], + ["no-long-transition-duration", 48], + ["no-tiny-text", 48], + ["prefer-dynamic-import", 48], + ["use-lazy-motion", 48], + ["duplicates", 45], + ["no-barrel-import", 45], + ["no-derived-useState", 44], + ["no-derived-state-effect", 42], + ["no-effect-chain", 42], + ["no-effect-event-handler", 42], + ["no-mirror-prop-effect", 42], + ["no-prop-callback-in-effect", 42], + ["prefer-use-effect-event", 42], + ["no-cascading-set-state", 40], + ["no-usememo-simple-expression", 40], + ["design-no-redundant-size-axes", 35], + ["design-no-redundant-padding-axes", 35], + ["design-no-em-dash-in-jsx-text", 34], + ["design-no-space-on-flex-children", 34], + ["design-no-three-period-ellipsis", 34], + ["no-inline-prop-on-memo-component", 32], + ["no-many-boolean-props", 30], + ["no-polymorphic-children", 30], + ["no-render-in-render", 28], + ["prefer-useReducer", 26], + ["no-generic-handler-names", 24], + ["no-giant-component", 15], + ["exports", 10], + ["types", 10], + ["files", 5], +]); + +function usage(exitCode = 0) { + const out = exitCode === 0 ? console.log : console.error; + out(`Usage: + bun run doctor -- next-batch [--min-issues 75] [--max-issues 120] [--max-files 4] + bun run doctor -- check-batch --run + bun run doctor -- file + bun run doctor -- top [--limit 10] + +Examples: + bun run doctor -- next-batch --min-issues 75 --max-issues 120 + bun run doctor -- file packages/ui/src/components/chat/ChatInput.tsx + bun run doctor -- check-batch --run 2026-05-14T12-31-44`); + process.exit(exitCode); +} + +function parseArgs(argv) { + const args = { _: [] }; + for (let i = 0; i < argv.length; i += 1) { + const arg = argv[i]; + if (!arg.startsWith("--")) { + args._.push(arg); + continue; + } + const key = arg.slice(2); + const next = argv[i + 1]; + if (!next || next.startsWith("--")) { + args[key] = true; + continue; + } + args[key] = next; + i += 1; + } + return args; +} + +function asPositiveInt(value, fallback, name) { + if (value === undefined) return fallback; + const parsed = Number(value); + if (!Number.isInteger(parsed) || parsed <= 0) { + throw new Error(`Invalid --${name}: expected a positive integer.`); + } + return parsed; +} + +function runReactDoctor() { + const output = execFileSync( + "npx", + [ + "react-doctor@latest", + "--project", + PROJECT_NAME, + "--json", + "--offline", + "--fail-on", + "none", + ], + { cwd: process.cwd(), encoding: "utf8", maxBuffer: 64 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] }, + ); + return JSON.parse(output); +} + +function allDiagnostics(report) { + return report.diagnostics ?? report.projects?.flatMap((project) => project.diagnostics ?? []) ?? []; +} + +function groupByFile(diagnostics) { + const byFile = new Map(); + for (const diagnostic of diagnostics) { + const list = byFile.get(diagnostic.filePath) ?? []; + list.push(diagnostic); + byFile.set(diagnostic.filePath, list); + } + return byFile; +} + +function rulePriority(rule) { + return PRIORITY_RULES.get(rule) ?? 50; +} + +function filePriority(diagnostics) { + const usefulScore = diagnostics.reduce((sum, diagnostic) => sum + rulePriority(diagnostic.rule), 0); + const noisyCount = diagnostics.filter((diagnostic) => rulePriority(diagnostic.rule) <= 35).length; + const highSignalCount = diagnostics.filter((diagnostic) => rulePriority(diagnostic.rule) >= 66).length; + const noisyRatio = diagnostics.length === 0 ? 0 : noisyCount / diagnostics.length; + const noisyPenalty = noisyRatio > 0.5 ? diagnostics.length * 35 : noisyCount * 8; + return usefulScore + highSignalCount * 25 - noisyPenalty; +} + +function sortedFileEntries(diagnostics) { + return [...groupByFile(diagnostics).entries()].sort((a, b) => { + const scoreDiff = filePriority(b[1]) - filePriority(a[1]); + if (scoreDiff !== 0) return scoreDiff; + const countDiff = b[1].length - a[1].length; + if (countDiff !== 0) return countDiff; + return a[0].localeCompare(b[0]); + }); +} + +function summarizeRules(diagnostics) { + const counts = new Map(); + for (const diagnostic of diagnostics) { + const key = `${diagnostic.category} / ${diagnostic.rule}`; + counts.set(key, (counts.get(key) ?? 0) + 1); + } + return [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])); +} + +function summarizeCategories(diagnostics) { + const counts = new Map(); + for (const diagnostic of diagnostics) { + counts.set(diagnostic.category, (counts.get(diagnostic.category) ?? 0) + 1); + } + return [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])); +} + +function createRunId() { + return new Date().toISOString().replace(/:/g, "-").replace(/\.\d{3}Z$/, "Z"); +} + +function titleCase(value) { + return value + .replace(/[-_]+/g, " ") + .replace(/\b\w/g, (char) => char.toUpperCase()) + .trim(); +} + +function fileNameWithoutExtension(filePath) { + const fileName = filePath.split("/").at(-1) ?? filePath; + return fileName.replace(/\.[^.]+$/, ""); +} + +function slugify(value) { + return value + .replace(/([a-z0-9])([A-Z])/g, "$1-$2") + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, "") + .slice(0, 80); +} + +function createBatchMetadata(runId, selectedFiles) { + const [datePart, timePart = ""] = runId.replace(/Z$/, "").split("T"); + const timestamp = `${datePart.replace(/-/g, "")}-${timePart.replace(/-/g, "")}`; + const stems = selectedFiles.map((file) => fileNameWithoutExtension(file.filePath)); + const readableArea = stems.length === 1 ? stems[0] : `${stems.slice(0, 2).join(" and ")}${stems.length > 2 ? ` plus ${stems.length - 2}` : ""}`; + const areaSlug = slugify(stems.slice(0, 3).join("-")) || "batch"; + const batchName = `rd-${timestamp}-${areaSlug}`; + + return { + batchName, + branchName: `react-doctor/${batchName}`, + prTitle: `Reduce React Doctor diagnostics in ${titleCase(readableArea)}`, + }; +} + +function selectBatch(entries, minIssues, maxIssues, maxFiles) { + if (entries.length === 0) return { selected: [], oversized: false, belowTarget: false, reason: "No diagnostics found." }; + + const firstFitting = entries.find(([, diagnostics]) => diagnostics.length >= minIssues && diagnostics.length <= maxIssues); + if (firstFitting) { + return { + selected: [firstFitting], + oversized: false, + belowTarget: false, + reason: "A prioritized file already fits the target window.", + }; + } + + const oversized = entries.find(([, diagnostics]) => diagnostics.length > maxIssues); + if (oversized) { + return { + selected: [oversized], + oversized: true, + belowTarget: false, + reason: "A prioritized file exceeds the target window and was selected as a single complete-file batch.", + }; + } + + const selected = []; + let total = 0; + for (const entry of entries) { + if (selected.length >= maxFiles) break; + const count = entry[1].length; + if (total + count > maxIssues) { + if (total >= minIssues) break; + continue; + } + selected.push(entry); + total += count; + if (total >= minIssues) break; + } + + if (selected.length > 0) { + return { + selected, + oversized: false, + belowTarget: total < minIssues, + reason: total >= minIssues + ? "Added complete files until the batch reached the target window." + : "No combination reached the minimum without exceeding the maximum; selected the best smaller complete-file batch.", + }; + } + + return { + selected: [entries[0]], + oversized: false, + belowTarget: entries[0][1].length < minIssues, + reason: "Selected the best available complete file below the target window.", + }; +} + +function writeRun(runId, payload) { + const dir = join(RUNS_DIR, runId); + mkdirSync(dir, { recursive: true }); + writeFileSync(join(dir, "baseline.json"), `${JSON.stringify(payload.report, null, 2)}\n`); + writeFileSync(join(dir, "batch.json"), `${JSON.stringify(payload.batch, null, 2)}\n`); + return dir; +} + +function readRun(runId) { + const dir = join(RUNS_DIR, runId); + const baselinePath = join(dir, "baseline.json"); + const batchPath = join(dir, "batch.json"); + if (!existsSync(baselinePath) || !existsSync(batchPath)) { + throw new Error(`Unknown run: ${runId}`); + } + return { + baseline: JSON.parse(readFileSync(baselinePath, "utf8")), + batch: JSON.parse(readFileSync(batchPath, "utf8")), + }; +} + +function printReportHeader(report) { + const summary = report.summary ?? {}; + console.log(`Repository score: ${summary.score ?? "unknown"} / 100 ${summary.scoreLabel ?? ""}`.trim()); + console.log(`Total diagnostics: ${summary.totalDiagnosticCount ?? allDiagnostics(report).length} issues across ${summary.affectedFileCount ?? "unknown"} files`); + console.log(`Severity: ${summary.errorCount ?? 0} errors, ${summary.warningCount ?? 0} warnings`); +} + +function printDiagnostics(diagnostics, limit = diagnostics.length) { + for (const diagnostic of diagnostics.slice(0, limit)) { + console.log(`line ${diagnostic.line ?? "?"}:${diagnostic.column ?? "?"} ${diagnostic.severity} ${diagnostic.category} / ${diagnostic.rule}`); + console.log(` ${diagnostic.message}`); + if (diagnostic.help && diagnostic.help !== diagnostic.message) console.log(` Help: ${diagnostic.help}`); + } +} + +function commandNextBatch(args) { + const minIssues = asPositiveInt(args["min-issues"], 75, "min-issues"); + const maxIssues = asPositiveInt(args["max-issues"], 120, "max-issues"); + const maxFiles = asPositiveInt(args["max-files"], 4, "max-files"); + if (minIssues > maxIssues) throw new Error("--min-issues cannot be greater than --max-issues."); + + const report = runReactDoctor(); + const diagnostics = allDiagnostics(report); + const entries = sortedFileEntries(diagnostics); + const selection = selectBatch(entries, minIssues, maxIssues, maxFiles); + const runId = createRunId(); + const selectedFiles = selection.selected.map(([filePath, fileDiagnostics]) => ({ + filePath, + diagnosticCount: fileDiagnostics.length, + rules: summarizeRules(fileDiagnostics), + })); + const metadata = createBatchMetadata(runId, selectedFiles); + const batch = { runId, ...metadata, minIssues, maxIssues, maxFiles, selectedFiles, oversized: selection.oversized, belowTarget: selection.belowTarget, reason: selection.reason }; + const runDir = writeRun(runId, { report, batch }); + + console.log("React Doctor Next Batch"); + console.log(""); + console.log(`Run ID: ${runId}`); + console.log(`Batch name: ${batch.batchName}`); + console.log(`Branch name: ${batch.branchName}`); + console.log(`PR title: ${batch.prTitle}`); + console.log(`Baseline: ${join(runDir, "baseline.json")}`); + console.log(`Batch metadata: ${join(runDir, "batch.json")}`); + console.log(""); + printReportHeader(report); + console.log(""); + console.log(`Batch window: ${minIssues}-${maxIssues} diagnostics`); + console.log(`Selection mode: complete files only`); + console.log(`Batch total: ${selectedFiles.reduce((sum, file) => sum + file.diagnosticCount, 0)} diagnostics`); + console.log(`Oversized: ${selection.oversized ? "yes" : "no"}`); + console.log(`Below target: ${selection.belowTarget ? "yes" : "no"}`); + console.log(`Selection reason: ${selection.reason}`); + console.log(""); + console.log("Selected files:"); + selection.selected.forEach(([filePath, fileDiagnostics], index) => { + console.log(`${index + 1}. ${filePath}`); + console.log(` Diagnostics: ${fileDiagnostics.length}`); + console.log(" Categories: " + summarizeCategories(fileDiagnostics).map(([category, count]) => `${category} ${count}`).join(", ")); + console.log(" Rules:"); + for (const [rule, count] of summarizeRules(fileDiagnostics).slice(0, 10)) { + console.log(` ${String(count).padStart(3)} ${rule}`); + } + console.log(" Diagnostics:"); + for (const diagnostic of fileDiagnostics) { + console.log(` line ${diagnostic.line ?? "?"}:${diagnostic.column ?? "?"} ${diagnostic.severity} ${diagnostic.category} / ${diagnostic.rule}`); + console.log(` ${diagnostic.message}`); + } + console.log(""); + }); +} + +function commandTop(args) { + const limit = asPositiveInt(args.limit, 10, "limit"); + const report = runReactDoctor(); + const entries = sortedFileEntries(allDiagnostics(report)).slice(0, limit); + console.log(`Top ${limit} files by prioritized React Doctor diagnostics`); + console.log(""); + for (const [filePath, diagnostics] of entries) { + console.log(`${String(diagnostics.length).padStart(4)} ${filePath}`); + console.log(` ${summarizeCategories(diagnostics).map(([category, count]) => `${category} ${count}`).join(", ")}`); + } +} + +function commandFile(args) { + const filePath = args._[1]; + if (!filePath) throw new Error("Missing file path. Usage: bun run doctor -- file "); + const report = runReactDoctor(); + const diagnostics = groupByFile(allDiagnostics(report)).get(filePath) ?? []; + console.log(filePath); + console.log(`${diagnostics.length} issues`); + console.log(""); + if (diagnostics.length === 0) return; + console.log("Rules:"); + for (const [rule, count] of summarizeRules(diagnostics)) { + console.log(`${String(count).padStart(4)} ${rule}`); + } + console.log(""); + console.log("Issues:"); + printDiagnostics(diagnostics); +} + +function commandCheckBatch(args) { + const runId = args.run; + if (!runId || runId === true) throw new Error("Missing --run ."); + const { baseline, batch } = readRun(runId); + const current = runReactDoctor(); + const beforeByFile = groupByFile(allDiagnostics(baseline)); + const afterByFile = groupByFile(allDiagnostics(current)); + const selected = batch.selectedFiles ?? []; + let beforeTotal = 0; + let afterTotal = 0; + + console.log("React Doctor Batch Check"); + console.log(""); + console.log(`Run ID: ${runId}`); + if (batch.batchName) console.log(`Batch name: ${batch.batchName}`); + if (batch.branchName) console.log(`Branch name: ${batch.branchName}`); + if (batch.prTitle) console.log(`PR title: ${batch.prTitle}`); + console.log(""); + console.log("Selected files:"); + for (const file of selected) { + const before = beforeByFile.get(file.filePath)?.length ?? 0; + const after = afterByFile.get(file.filePath)?.length ?? 0; + beforeTotal += before; + afterTotal += after; + console.log(file.filePath); + console.log(` Before: ${before}`); + console.log(` After: ${after}`); + console.log(` Delta: ${after - before}`); + } + + const selectedPaths = new Set(selected.map((file) => file.filePath)); + const beforeOutside = allDiagnostics(baseline).filter((diagnostic) => !selectedPaths.has(diagnostic.filePath)).length; + const afterOutside = allDiagnostics(current).filter((diagnostic) => !selectedPaths.has(diagnostic.filePath)).length; + + console.log(""); + console.log("Batch result:"); + console.log(`Fixed diagnostics in selected files: ${Math.max(0, beforeTotal - afterTotal)}`); + console.log(`Remaining diagnostics in selected files: ${afterTotal}`); + console.log(`Diagnostics outside selected files delta: ${afterOutside - beforeOutside}`); + console.log(""); + console.log("Current repository summary:"); + printReportHeader(current); +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + const command = args._[0]; + if (!command || command === "help" || args.help) usage(0); + + switch (command) { + case "next-batch": + commandNextBatch(args); + break; + case "top": + commandTop(args); + break; + case "file": + commandFile(args); + break; + case "check-batch": + commandCheckBatch(args); + break; + default: + throw new Error(`Unknown command: ${command}`); + } +} + +main().catch((error) => { + console.error(error.message); + process.exit(1); +});