A diagnostic CLI that measures Dependabot toil and CVE exposure across a GitHub org. It runs in the user's environment, crawls api.github.com, and writes a self-contained HTML report to a temporary directory. No data leaves the user's network unless they choose to share the generated report.
The single entrypoint is patchwave-analysis [<org-or-user>] — an interactive session that prompts for the target if omitted, then whether to share the report when the scan finishes. There are no other flags; the time window (90 days) is fixed.
- Runtime: Bun (the binary is
bun build --compiled; see.goreleaser.yaml). - Language: TypeScript (strict).
- Layout: single package, flat
src/tree. No workspaces, no monorepo. - Report UI: React + Tailwind, built into
dist/report-web/index.htmland embedded into the binary via awith { type: 'text' }import. Browser tests use vitest (browser mode via Playwright/Chromium); CLI tests use bun:test.
patchwave-analysis/
├── src/
│ ├── collectors/ per-slice GitHub data collectors (repos, PRs, CVEs, …)
│ ├── github/ Octokit client wiring (retry + throttling plugins)
│ ├── heuristics/ derived metrics (toil cost, automation upside, …)
│ ├── interactive/ Clack prompts: token walkthrough, share/open, banner, TTY gate
│ ├── prompt/ Prompter abstraction over @clack/prompts
│ ├── report/ report aggregation + `report/web/` React UI
│ ├── upload/ artifact sharing
│ ├── testHelpers/ shared test utilities
│ ├── context.ts CliContext DI root (see Conventions)
│ ├── cli.ts arg parsing + main()
│ └── index.ts bootstrap: telemetry/logger/analytics wiring, then main()
├── scripts/ analyze.sh installer + build-report-web.ts
├── .goreleaser.yaml cross-platform compile + macOS notarize
└── justfile root-level recipes
Before marking a task complete, run just verify and fix anything that fails. It runs four steps in order:
bun run format:check— Prettierbun run typecheck— stricttsc --noEmit(builds the web report first)bun run lint— ESLint (--max-warnings 0)bun run test— dispatchestest:unit(bun:test oversrc/**/*.test.ts) thentest:browser(vitest)
Do not run a bare bun test at the repo root — Bun's runner would walk the report UI's .test.tsx files, which depend on a real DOM and only run under vitest. Use bun run test (the dispatch script) or a targeted bun test ./src/<area>/foo.test.ts during iteration.
Before opening a PR, read .github/pull_request_template.md and follow it exactly — title rules (Conventional Commits, chosen by changelog visibility per release-please-config.json), the Summary/Review-focus/Commits sections, and the commit-hygiene guidance.
The following files under .claude/rules/ carry team conventions enforced for src/. Read the relevant one before editing matching files:
- Error Handling Neverthrow — no try/catch in business logic; wrap fallible I/O in
Result/ResultAsync. - Context Interfaces and Fakes — injected dependencies get a narrow public interface plus a separate real implementation.
- Testing Patterns — test data comes from Fishery factories (
testFactories.ts), never hand-rolledcreateXxx()helpers. - Bun-native APIs — reach for
Bun.*globals before the Node equivalent. - Bun testing — non-obvious
bun:testconventions. - GitHub API interaction — batch via GraphQL
nodes(ids:), never the search API for private data, tolerate partial responses, keep queries light; fail loudly instead of returning a silent zero.
Beyond the rules:
- Dependency injection is non-negotiable. All business logic flows through the typed
CliContext(src/context.ts). Prefer explicit context-object wiring over module-level singletons or global mocking. ctxalways comes first, and is destructured at the point of use. Helpers that take a context list it as the first parameter (fn(ctx, other)); pull the fields you need at the top of the body (const { io, logger } = ctx;) rather than reaching throughctx.io.stdoutat each call site. This narrows each function to the surface it depends on and keeps test stubs honest.- Business output (stdout) and diagnostics (logger → stderr) stay on separate channels so piped consumers see clean stdout while humans get readable logs.
- Use Temporal for all time handling. Use
@js-temporal/polyfillviasrc/time.ts, keep in-process values as Temporal objects, serialize only ISO strings at JSON boundaries, and do not useDate. - In Zod string schemas, prefer
.nonempty()over.min(1)(.trim().nonempty()when surrounding whitespace should not count). - Destructured defaults over
??fallbacks. Apply defaults in a single destructuring assignment —const { version = '0.0.1' } = input;, not per-field??. - Helpers at the bottom of files. Primary exports come first; module-local helpers and factories sit below them. In test files they live after all
describe()blocks. - The product is always "PatchWave" in prose and UI copy — capital P and W, one word. Never "patchwave", "Patchwave", or "patch wave". The only lowercase forms are literal identifiers that must match their real-world spelling: the
patchwave-analysisCLI/binary name, thepatchwave-report.htmlartifact, and thepatchwave.aidomain.
- camelCase for
.ts/.tsxfiles (cli.ts,context.ts,tokenWalkthrough.ts). - PascalCase when a file's primary export is a module-level class, matching the class name (
Telemetry.tsexportsclass …,IoImpl.ts,BrowserOpener.ts). Test files follow the same casing. - Tooling-mandated filenames (
tsconfig.json,package.json,eslint.config.mjs, workflow files, etc.) follow upstream conventions. - Directory names are lowercase. Tests are co-located next to the implementation (
cli.ts→cli.test.ts); no__tests__/or top-leveltests/tree.
Use relative imports with explicit .ts / .tsx extensions (import { main } from './cli.ts';). This is a single package — there are no subpath (#src/*) or cross-package (@scope/*) imports.
Instrumentation (PostHog analytics + Sentry crash reporting, gated by build-time keys) is wired in src/index.ts. Sentry.init must run before createLogger so its pinoIntegration subscribes to pino's diagnostics channel first. Org names, repo names, tokens, report contents, and the machine hostname are never sent — see the "Telemetry & privacy" section of README.md for the full guarantee.