Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
bun 1.3.14
just 1.49.0
85 changes: 85 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# AGENTS.md

## Project: patchwave-analysis

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 plus a raw-data zip to a temporary directory. No data leaves the user's network unless they choose to share the generated artifacts.

The single entrypoint is `patchwave-analysis [<org-or-user>]` — an interactive session that prompts for the target if omitted, then for what to share when the scan finishes. There are no other flags; the time window (90 days) is fixed.

## Stack

- **Runtime:** Bun (the binary is `bun build --compile`d; 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.html` and embedded into the binary via a `with { type: 'text' }` import. Browser tests use **vitest** (browser mode via Playwright/Chromium); CLI tests use **bun:test**.

## Repository layout

```
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
```

## Verification

Before marking a task complete, run `just verify` and fix anything that fails. It runs four steps in order:

- `bun run format:check` — Prettier
- `bun run typecheck` — strict `tsc --noEmit` (builds the web report first)
- `bun run lint` — ESLint (`--max-warnings 0`)
- `bun run test` — dispatches `test:unit` (bun:test over `src/**/*.test.ts`) then `test: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.

## Pull requests

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.

## Conventions

The following files under `.claude/rules/` carry team conventions enforced for `src/`. Read the relevant one before editing matching files:

- [Error Handling Neverthrow](.claude/rules/error-handling-neverthrow.md) — no try/catch in business logic; wrap fallible I/O in `Result` / `ResultAsync`.
- [Context Interfaces and Fakes](.claude/rules/context-interfaces-and-fakes.md) — injected dependencies get a narrow public interface plus a separate real implementation.
- [Testing Patterns](.claude/rules/testing-patterns.md) — test data comes from Fishery factories (`testFactories.ts`), never hand-rolled `createXxx()` helpers.
- [Bun-native APIs](.claude/rules/bun-native-apis.md) — reach for `Bun.*` globals before the Node equivalent.
- [Bun testing](.claude/rules/bun-testing.md) — non-obvious `bun:test` conventions.

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.
- **`ctx` always 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 through `ctx.io.stdout` at 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/polyfill` via `src/time.ts`, keep in-process values as Temporal objects, serialize only ISO strings at JSON boundaries, and do not use `Date`.
- **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.

### File naming

- **camelCase** for `.ts` / `.tsx` files (`cli.ts`, `context.ts`, `tokenWalkthrough.ts`).
- **PascalCase when a file's primary export is a module-level class**, matching the class name (`Telemetry.ts` exports `class …`, `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-level `tests/` tree.

### Imports

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.

## Telemetry

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.
118 changes: 1 addition & 117 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,117 +1 @@
---
description: Use Bun instead of Node.js, npm, pnpm, or vite.
globs: '*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json'
alwaysApply: false
---

Default to using Bun instead of Node.js.

- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
- Use `bun test` instead of `jest` or `vitest`
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
- Use `bunx <package> <command>` instead of `npx <package> <command>`
- Bun automatically loads .env, so don't use dotenv.

## APIs

- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
- `Bun.redis` for Redis. Don't use `ioredis`.
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
- `WebSocket` is built-in. Don't use `ws`.
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
- Bun.$`ls` instead of execa.

## Testing

Use `bun test` to run tests.

```ts#index.test.ts
import { test, expect } from "bun:test";

test("hello world", () => {
expect(1).toBe(1);
});
```

## Frontend

Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.

Server:

```ts#index.ts
import index from "./index.html"

Bun.serve({
routes: {
"/": index,
"/api/users/:id": {
GET: (req) => {
return new Response(JSON.stringify({ id: req.params.id }));
},
},
},
// optional websocket support
websocket: {
open: (ws) => {
ws.send("Hello, world!");
},
message: (ws, message) => {
ws.send(message);
},
close: (ws) => {
// handle close
}
},
development: {
hmr: true,
console: true,
}
})
```

HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.

```html#index.html
<html>
<body>
<h1>Hello, world!</h1>
<script type="module" src="./frontend.tsx"></script>
</body>
</html>
```

With the following `frontend.tsx`:

```tsx#frontend.tsx
import React from "react";
import { createRoot } from "react-dom/client";

// import .css files directly and it works
import './index.css';

const root = createRoot(document.body);

export default function Frontend() {
return <h1>Hello, world!</h1>;
}

root.render(<Frontend />);
```

Then, run index.ts

```sh
bun --hot ./index.ts
```

For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`.

## Conventions

Team rules under `.claude/rules/` apply to files under `src/`. Read them before editing matching files:

- [Error Handling Neverthrow](.claude/rules/error-handling-neverthrow.md) — no try/catch in business logic; use `Result` / `ResultAsync`
@AGENTS.md
77 changes: 77 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Contributing to patchwave-analysis

## Setup

You need [`asdf`](https://asdf-vm.com/) installed and on your `PATH` (it manages the `bun` and `just` versions pinned in `.tool-versions`). Then:

```sh
git clone https://github.com/contextbridge/patchwave-analysis
cd patchwave-analysis
asdf install
just bootstrap # asdf toolchain + bun install (husky git hooks install on bun install)
just run -- --help
```

## Repo layout

This is a single package with a flat `src/` tree:

```
src/
├── collectors/ per-slice GitHub data collectors (repos, PRs, CVEs, …)
├── github/ Octokit client wiring (retry + throttling)
├── 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
└── *.ts CLI bootstrap, context (DI root), Octokit/IO/telemetry plumbing
```

## Development

Run the CLI locally (this builds the embedded report first):

```sh
just run # prompts for an org/user
just run contextbridge # scan a specific org/user
```

Iterate on the embedded report UI with HMR, or browse components in Storybook:

```sh
just dev # report UI dev server (bun run dev:report-web)
just storybook # http://localhost:6006
```

## Testing

Run all checks with `just verify` (format, typecheck, lint, tests). For individual steps:

1. `bun run format` (Prettier)
2. `bun run typecheck`
3. `bun run lint` (ESLint)
4. `bun run test` — `test:unit` (bun:test over `src/**/*.test.ts`) then `test:browser` (vitest for the report UI)

Don't run a bare `bun test` at the repo root: Bun's runner walks the report UI's `.test.tsx` files, which only run under vitest. Use `bun run test`, or target a single file with `bun test ./src/<area>/foo.test.ts`.

## Coding conventions

See [`AGENTS.md`](./AGENTS.md) and the rule files under [`.claude/rules/`](./.claude/rules/).

## Pull requests

1. Use [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, `chore:`, etc.) for PR titles. The `Lint PR title` check enforces this, and the prefix decides changelog visibility (see `release-please-config.json`).
2. Follow the PR template [`.github/pull_request_template.md`](./.github/pull_request_template.md).
3. Ensure `just verify` runs without errors before opening the PR.

## Releases

Releases are cut by the ContextBridge team. Stable releases are automated by [release-please](https://github.com/googleapis/release-please) feeding into [goreleaser](https://goreleaser.com/):

- On every push to `main`, release-please opens (or updates) a release PR that bumps `CHANGELOG.md` from conventional-commit titles since the last release. Merging it creates the tag; goreleaser then compiles, signs/notarizes, and attaches the binaries. Don't edit `CHANGELOG.md` by hand.
- To validate the release build locally without publishing or notarizing, run `just release dry-run` (requires `goreleaser` on `PATH`).

## Code of conduct

Be kind. Assume good faith. If something feels off, email [`support@contextbridge.ai`](mailto:support@contextbridge.ai).
15 changes: 6 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,8 @@ tar -xzf patchwave-analysis_darwin_arm64.tar.gz
```sh
git clone https://github.com/contextbridge/patchwave-analysis
cd patchwave-analysis
bun install
bun run build:report-web
bun run src/index.ts
```

For local report UI development:

```sh
bun run dev:report-web
just install
just run
```

## Usage
Expand Down Expand Up @@ -119,6 +112,10 @@ Both are disabled together by setting any of:

When disabled, no anonymous-id file is created, no analytics events are sent, and Sentry is never initialized.

## Contributing

Development setup, testing, and release workflow live in [`CONTRIBUTING.md`](./CONTRIBUTING.md).

## License

MIT.
9 changes: 9 additions & 0 deletions just/release.just
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Release recipes
# All recipes run from the project root so scripts/ and .goreleaser.yaml resolve correctly.

set working-directory := '..'

# Build all release artifacts locally without publishing or notarizing.
# Requires `goreleaser` on PATH (brew install goreleaser/tap/goreleaser).
dry-run:
goreleaser release --snapshot --clean --skip=publish,notarize
40 changes: 40 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# patchwave-analysis justfile

mod release 'just/release.just'

# Default recipe - list available commands
default:
@just --list

# Install dependencies
install:
bun install {{ if env("CI", "") != "" { "--frozen-lockfile" } else { "" } }}

# Full verification: format + typecheck + lint + test
verify: install
bun run format:check
bun run typecheck
bun run lint
bun run test

# Bootstrap development environment (asdf toolchain, deps, git hooks)
bootstrap:
asdf install
just install

# Run the CLI from source (builds the embedded report first). Pass an org/user as an argument.
run *args:
bun run start {{ args }}

# Compile a host-platform binary (for cross-platform artifacts use `just release dry-run`)
build:
bun run build:report-web
bun build --compile ./src/index.ts --outfile dist/patchwave-analysis

# Run the embedded report UI dev server with HMR
dev:
bun run dev:report-web

# Run Storybook for the report UI (http://localhost:6006)
storybook:
bun run storybook
Loading