Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 2 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
- name: Check Drizzle migrations are up to date
run: |
pnpm db:generate
git diff --exit-code drizzle/ || (echo "::error::Drizzle migrations are out of date. Run 'pnpm db:generate' and commit." && exit 1)
git diff --exit-code packages/cli/drizzle/ || (echo "::error::Drizzle migrations are out of date. Run 'pnpm db:generate' and commit." && exit 1)

- name: Lint
run: pnpm lint
Expand All @@ -47,6 +47,4 @@ jobs:
run: pnpm test

- name: Build
run: |
pnpm build
pnpm build:web
run: pnpm build
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ node_modules
dist
web-dist

# Generated at publish time by packages/cli prepack — root README is the source of truth.
packages/cli/README.md

# TypeScript incremental builds
*.tsbuildinfo

Expand Down
85 changes: 51 additions & 34 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,76 +7,93 @@ This file provides guidance to coding agents working in this repository, includi
```bash
pnpm install # Install dependencies (also installs husky pre-commit hook)
pnpm dev:web # Start the web UI in Vite dev mode
pnpm build # Bundle the CLI with tsdown into dist/
pnpm build:web # Build the web UI with Vite into web-dist/
pnpm test # Run tests (Vitest)
pnpm build # Build SPA, then bundle the CLI (writes packages/cli/{dist,web-dist})
pnpm test # Run tests (Vitest, from root)
pnpm lint # Biome check (lint + format) — fails on warnings
pnpm lint:fix # Biome check with auto-fix
pnpm format # Format code with Biome
pnpm typecheck # tsc --noEmit for both root and web tsconfigs
pnpm typecheck # tsc --noEmit across every package (`pnpm -r typecheck`)
```

The package manager is pinned via `packageManager` in `package.json`. Use `corepack enable` if pnpm isn't on your PATH.

### Database (Drizzle ORM + SQLite)

```bash
pnpm db:generate # Generate a new migration into drizzle/ from schema changes
pnpm db:generate # Generate a new migration into packages/cli/drizzle/ from schema changes
```

The CLI uses an embedded SQLite database via `better-sqlite3`. There is no separate dev database to start — `getDb()` opens (or creates) the local SQLite file and runs pending migrations on first use.

### Adding UI Components

```bash
npx shadcn@latest add <component>
cd packages/web && npx shadcn@latest add <component>
```

Components land under `web/src/components/ui/` per `components.json`.
Components land under `packages/web/src/components/ui/` per `packages/web/components.json`.

## Architecture

**Single npm package**, published as `stagereview` with the `stage-cli` binary. The CLI starts a local-loopback HTTP server that serves the prebuilt React UI and a small JSON API.
**pnpm workspace.** Three packages with real boundaries — no path-alias indirection. The published unit is `packages/cli` (npm name `stagereview`, binary `stage-cli`); the rest are private workspace deps that get inlined at build time.

```
src/ # CLI + local HTTP server (Node, ESM)
index.ts # CLI entry (Commander)
show.ts # `stage-cli show <path>` implementation
server.ts # Plain Node http server with regex-compiled routes
routes/ # API route handlers (one file per resource)
runs/ # Chapter run import + processing
db/ # Drizzle client, path resolution, schema/
schema.ts # Zod schemas for chapter JSON ingestion
__tests__/ # Vitest tests
drizzle/ # Generated SQL migrations + meta journal
web/ # Vite + React 19 + Tailwind 4 frontend (built into web-dist/)
src/components/ # UI components (shadcn/ui under components/ui/)
src/lib/ # Frontend utilities
src/styles/ # Tailwind globals
pnpm-workspace.yaml # packages: ["packages/*"]
packages/
cli/ # stagereview — published npm package
src/ # CLI + local HTTP server (Node, ESM)
index.ts # CLI entry (Commander)
show.ts # `stage-cli show <path>` implementation
server.ts # Plain Node http server with regex-compiled routes
routes/ # API route handlers (one file per resource)
runs/ # Chapter run import + processing
db/ # Drizzle client, path resolution, schema/
schema.ts # Zod schemas for chapter JSON ingestion (strict)
__tests__/ # Vitest tests
drizzle/ # Generated SQL migrations + meta journal
drizzle.config.ts # Drizzle Kit config
tsdown.config.ts # CLI bundler config (inlines @stage-cli/types)
types/ # @stage-cli/types (private, TS-native)
src/chapters.ts # Wire-format chapter/key-change schemas + shared HunkRef/LineRef
src/view-state.ts # Wire-format view-state schema
src/index.ts # Barrel re-export
web/ # @stage-cli/web (private) — built into ../cli/web-dist
src/components/ # UI components (shadcn/ui under components/ui/)
src/lib/ # Frontend utilities + tests
src/routes/ # SPA route components
src/styles/ # Tailwind globals
vite.config.ts # outDir → ../cli/web-dist
components.json # shadcn config
```

### CLI (`src/index.ts`)
### CLI (`packages/cli/src/index.ts`)

Uses [Commander](https://github.com/tj/commander.js) for subcommand parsing. Add new subcommands by chaining `.command(...)` and delegating to a module under `src/`.
Uses [Commander](https://github.com/tj/commander.js) for subcommand parsing. Add new subcommands by chaining `.command(...)` and delegating to a module under `packages/cli/src/`.

### Local Server (`src/server.ts`)
### Local Server (`packages/cli/src/server.ts`)

Plain Node `http` server bound to `127.0.0.1`. Route patterns use `:name` placeholders and are compiled to regexes at startup. The server resolves `/api/*` against registered routes and otherwise serves static files from `web-dist/` with an `index.html` SPA fallback.
Plain Node `http` server bound to `127.0.0.1`. Route patterns use `:name` placeholders and are compiled to regexes at startup. The server resolves `/api/*` against registered routes and otherwise serves static files from `web-dist/` (next to the bundled CLI) with an `index.html` SPA fallback.

- Route handlers live in `src/routes/` — one file per resource (`runs.ts`, `view-state.ts`, `json.ts`).
- Route handlers live in `packages/cli/src/routes/` — one file per resource (`runs.ts`, `view-state.ts`, `json.ts`).
- Path traversal is blocked by computing `path.relative(webDist, resolved)` and rejecting any result that escapes the root. **Don't bypass that check** when adding static-serving features.
- The server picks the first free port starting at `5391`. Don't hard-code ports in callers.

### Database Layer (`src/db/`)
### Database Layer (`packages/cli/src/db/`)

- **Client:** `getDb()` in `src/db/client.ts` is a singleton wrapped around `better-sqlite3`. It enables WAL + foreign keys and auto-runs migrations from `drizzle/`.
- **Schemas:** `src/db/schema/*.ts`, re-exported from `src/db/schema/index.ts`. Pass `* as schema` into `drizzle()` so relational queries work.
- **Path:** `src/db/path.ts` decides where the SQLite file lives (per-OS app data dir).
- **Client:** `getDb()` in `db/client.ts` is a singleton wrapped around `better-sqlite3`. It enables WAL + foreign keys and auto-runs migrations from `packages/cli/drizzle/`.
- **Schemas:** `db/schema/*.ts`, re-exported from `db/schema/index.ts`. Pass `* as schema` into `drizzle()` so relational queries work.
- **Path:** `db/path.ts` decides where the SQLite file lives (per-OS app data dir).
- Prefer Drizzle's Relational Queries API over the SQL-like query builder unless you need aggregations, custom column selections, or complex joins.

### Web UI (`web/`)
### Shared Types (`packages/types/`)

Vite app with React 19, Tailwind 4, and shadcn/ui (new-york style, zinc base, lucide icons). Built to `web-dist/`, which is bundled into the published npm package and served by the CLI's local HTTP server.
Wire-format types shared between the CLI's HTTP routes and the SPA. The package exports `.ts` source directly (no compile step) — `tsdown` and `vite` resolve TypeScript natively. The CLI bundle inlines this package via `deps.alwaysBundle` in `tsdown.config.ts`, so the published tarball never has a runtime require for `@stage-cli/types`.

Building blocks like `HunkRef`, `LineRef`, and `DIFF_SIDE` live here; the strict ingestion schema (`ChaptersFileSchema`) stays in `packages/cli/src/schema.ts` and re-exports them.

### Web UI (`packages/web/`)

Vite app with React 19, Tailwind 4, and shadcn/ui (new-york style, zinc base, lucide icons). Builds into `../cli/web-dist`, which is bundled into the published npm package and served by the CLI's local HTTP server.

### Key Technologies

Expand All @@ -103,7 +120,7 @@ A `pre-commit` hook (husky + lint-staged) runs `biome check --write` against sta

## Package Naming

This is a single-package repo published as `stagereview`. The CLI binary is `stage-cli`. There is no monorepo or workspace scope.
The published npm package is `stagereview` (lives in `packages/cli`); the CLI binary is `stage-cli`. Internal workspace packages use the `@stage-cli/*` scope (`@stage-cli/types`, `@stage-cli/web`) — they are private and never published.

## Testing

Expand Down
22 changes: 11 additions & 11 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,20 @@ Test API route handlers through the real `startServer()` HTTP boundary, hitting
**This is the highest-ROI test layer.** Most logic worth testing is request handling, schema validation, and database state transitions.

Examples:
- `src/__tests__/runs.routes.test.ts` — exercises run/chapter routes against a real server + SQLite
- `src/__tests__/view-state.routes.test.ts` — exercises view-state routes end-to-end
- `src/__tests__/server.test.ts` — covers the static-file fallback, route compilation, and path-traversal guard
- `packages/cli/src/__tests__/runs.routes.test.ts` — exercises run/chapter routes against a real server + SQLite
- `packages/cli/src/__tests__/view-state.routes.test.ts` — exercises view-state routes end-to-end
- `packages/cli/src/__tests__/server.test.ts` — covers the static-file fallback, route compilation, and path-traversal guard

Use the helpers in `src/__tests__/fixtures.ts` to spin up a temp DB and the server.
Use the helpers in `packages/cli/src/__tests__/fixtures.ts` to spin up a temp DB and the server.

### 3. Pure Logic Unit Tests

Schemas, parsers, and pure helpers. No mocks needed — these are pure functions.

Examples:
- `src/__tests__/schema.test.ts` — Zod chapter-import schemas
- `src/__tests__/path.test.ts` — DB path resolution
- `src/__tests__/import-chapters.test.ts` — chapter import transformation
- `packages/cli/src/__tests__/schema.test.ts` — Zod chapter-import schemas
- `packages/cli/src/__tests__/path.test.ts` — DB path resolution
- `packages/cli/src/__tests__/import-chapters.test.ts` — chapter import transformation

### 4. Web UI Component Tests

Expand All @@ -44,9 +44,9 @@ Narrow exception: tests for keyboard navigation, focus management, and form beha
**Constraints:**
- Must mock zero or one external boundary (the CLI's `/api/*` fetch calls)
- Must mock at most one internal module
- If a test needs more, lift the logic out of the component into `web/src/lib/` and test it there
- If a test needs more, lift the logic out of the component into `packages/web/src/lib/` and test it there

There are no web UI tests today. If you add the first one, set up a JSDOM Vitest project under `web/` rather than mixing it into the Node test config.
Web tests live alongside their modules under `packages/web/src/lib/__tests__/` and use happy-dom (set per-file via the `// @vitest-environment happy-dom` directive). Vitest runs from the workspace root and picks up tests in any package.

## What to Test

Expand Down Expand Up @@ -92,7 +92,7 @@ If a test needs 2+ internal mocks, the test is testing the mock setup, not the a
3. **Never mock more than one external-service boundary** per test file.
4. **Never mock more than one internal module** per test file.
5. **Never test that a component "renders without crashing"** — TypeScript already guarantees this.
6. **Factory functions over inline object literals.** Use `make*` or `create*` helpers in `src/__tests__/fixtures.ts` (or alongside the test file) with overrides.
6. **Factory functions over inline object literals.** Use `make*` or `create*` helpers in `packages/cli/src/__tests__/fixtures.ts` (or alongside the test file) with overrides.
7. **One clear behavior per test.** Name by behavior, not method name.
8. **Arrange-Act-Assert.** One clear action per test.
9. **Use a real DB, not a mock.** Spin up a temp SQLite via the existing fixtures. Drizzle/better-sqlite3 are fast enough that mocking them is never the right call.
Expand Down Expand Up @@ -135,6 +135,6 @@ When modifying code that is covered by a slop test (a test that violates the moc
| Path-resolution / static-file change | Route/server integration | Required |
| Visual-only UI change | None | N/A |
| New parser / transformer | Pure unit | Required |
| New React component (logic-heavy) | Extract logic to `web/src/lib/`, test there | Optional |
| New React component (logic-heavy) | Extract logic to `packages/web/src/lib/`, test there | Optional |
| New React component (display-only) | None | N/A |
| New schema migration | Route integration that exercises new columns | Required |
2 changes: 1 addition & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"!**/node_modules",
"!**/dist",
"!**/web-dist",
"!drizzle"
"!**/drizzle"
]
},
"linter": {
Expand Down
73 changes: 7 additions & 66 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,8 @@
{
"name": "stagereview",
"name": "stagereview-monorepo",
"version": "0.1.0",
"description": "Chapter-style code review against your local git branch.",
"keywords": [
"code-review",
"cli",
"chapters",
"git",
"claude-code",
"cursor",
"codex",
"gemini",
"opencode"
],
"private": true,
"description": "pnpm workspace root for stagereview (packages/cli) and its supporting packages.",
"homepage": "https://github.com/ReviewStage/stage-cli#readme",
"bugs": {
"url": "https://github.com/ReviewStage/stage-cli/issues"
Expand All @@ -25,29 +15,18 @@
"author": "Stage",
"type": "module",
"packageManager": "pnpm@10.24.0",
"main": "./dist/index.js",
"bin": {
"stage-cli": "./dist/index.js"
},
"files": [
"dist",
"drizzle",
"web-dist",
"skills"
],
"engines": {
"node": ">=20"
},
"scripts": {
"build": "tsdown",
"build:web": "vite build --config web/vite.config.ts",
"dev:web": "vite --config web/vite.config.ts",
"build": "pnpm --filter @stage-cli/web build && pnpm --filter stagereview build",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Disambiguate pnpm filter target for CLI package scripts

The workspace now has two packages named stagereview (/package.json and packages/cli/package.json), but these scripts still use pnpm --filter stagereview .... Per pnpm’s filtering docs, when multiple workspace packages share the same name, an unscoped name filter matches nothing; and pnpm-workspace.yaml docs state the root package is always included in the workspace. This means build (and similarly db:generate) can silently skip the CLI package, so CI/local runs may pass without actually building packages/cli or regenerating migrations.

Useful? React with 👍 / 👎.

"dev:web": "pnpm --filter @stage-cli/web dev",
"test": "vitest run",
"lint": "biome check .",
"lint:fix": "biome check --write .",
"format": "biome format --write .",
"typecheck": "tsc --noEmit && tsc --noEmit -p web/tsconfig.json",
"db:generate": "drizzle-kit generate",
"typecheck": "pnpm -r typecheck",
"db:generate": "pnpm --filter stagereview db:generate",
"prepare": "husky"
},
"lint-staged": {
Expand All @@ -57,50 +36,12 @@
},
"devDependencies": {
"@biomejs/biome": "^2.3.10",
"@pierre/diffs": "^1.0.11",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/vite": "^4.1.18",
"@tanstack/react-query": "^5.100.7",
"@testing-library/react": "^16.3.2",
"@types/better-sqlite3": "^7.6.13",
"@types/node": "^25.6.0",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"drizzle-kit": "^0.31.10",
"happy-dom": "^20.9.0",
"husky": "^9.1.7",
"lint-staged": "^16.2.7",
"lucide-react": "^0.562.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.18",
"tsdown": "^0.21.10",
"tw-animate-css": "^1.4.0",
"typescript": "^5.6.3",
"vite": "^7.3.1",
"vite-tsconfig-paths": "^6.1.1",
"vitest": "^4.1.5"
},
"dependencies": {
"better-sqlite3": "^12.9.0",
"commander": "^14.0.3",
"drizzle-orm": "^0.45.2",
"open": "^11.0.0",
"zod": "^4.3.6"
},
"pnpm": {
"onlyBuiltDependencies": [
"better-sqlite3",
"esbuild"
]
}
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading
Loading