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
2 changes: 1 addition & 1 deletion .changeset/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Changesets

Run `npm run changeset` from `apps/cli/` to add a changeset when making changes to DiffHub.
Run `pnpm run changeset` from `apps/cli/` to add a changeset when making changes to DiffHub.

This generates a changeset file that describes the change and its semver bump type (patch, minor, or major). Changesets are consumed during release to update the version and generate changelog entries.
2 changes: 2 additions & 0 deletions .changeset/giant-trains-drum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
2 changes: 1 addition & 1 deletion .cmux/cmux.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
{
"pane": {
"surfaces": [
{ "type": "terminal", "name": "Dev", "command": "npm run dev" }
{ "type": "terminal", "name": "Dev", "command": "pnpm run dev" }
]
}
},
Expand Down
2 changes: 1 addition & 1 deletion .cmux/dock.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"controls": [
{ "id": "git", "title": "Git", "command": "git status -sb && echo && git -c color.ui=always log --oneline --graph --decorate -20; exec ${SHELL:-/bin/zsh} -l", "cwd": ".", "height": 340 },
{ "id": "checks", "title": "Checks", "command": "npm run check; exec ${SHELL:-/bin/zsh} -l", "cwd": ".", "height": 240 },
{ "id": "checks", "title": "Checks", "command": "pnpm run check; exec ${SHELL:-/bin/zsh} -l", "cwd": ".", "height": 240 },
{ "id": "feed", "title": "Feed", "command": "cmux feed tui --opentui", "height": 240 }
]
}
9 changes: 9 additions & 0 deletions .factory/plans/F283.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
## Decisions for the reviewer

- Marketing copy (`apps/web/app/(marketing)/**`, `home-islands.tsx`, `guides.ts`) that tells end users to run `npx diffhub@latest` or `npm install -g diffhub` is left unchanged: those commands install the *published* `diffhub` package from the npm registry, which stays the npm registry regardless of which tool builds this workspace. Triage flagged these files by grepping for `npm`, not because the copy is wrong.
- `packageManager` pinned to `pnpm@12.8.1` (the pnpm already installed on this machine, `pnpm -v` reads it from this exact field).
- `workspaces` in root `package.json` replaced by `pnpm-workspace.yaml`. The old npm lockfile recorded postinstall scripts for `core-js`, `fsevents`, and `lefthook` (`hasInstallScript: true`); pnpm blocks all lifecycle scripts by default, so I ran `pnpm approve-builds --all`, which allow-listed `core-js` and `lefthook` under `allowBuilds:` in `pnpm-workspace.yaml` (the field name pnpm 12 actually uses, not the `onlyBuiltDependencies` I first guessed) — `fsevents` had no pending build in this install, so pnpm left it out. lefthook's postinstall re-synced the git hook during the same run.
- `.github/workflows/npm-publish.yml`: switched the install/build/typecheck/test steps to pnpm, and the final `npm run release --workspace=apps/cli` to `pnpm --filter diffhub release` (with its internal `npm run prepare:standalone` becoming `pnpm run prepare:standalone`). Kept `registry-url`/`NPM_CONFIG_PROVENANCE` since those are npm-registry (not npm-client) settings; changesets auto-detects the package manager from the lockfile and will shell out to `pnpm publish`. Dropped the `npm install -g npm@11.5.1` OIDC step since pnpm 12 has its own trusted-publishing support — **not exercised by this PR's CI**, so flagging as the one part of this migration proven by reading pnpm's release notes rather than a real publish.
- Left `apps/cli/CHANGELOG.md` untouched (historical changesets output, not living docs).

Stages: migration
17 changes: 10 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,30 +15,33 @@ jobs:
with:
fetch-depth: 0

- name: Setup pnpm
uses: pnpm/action-setup@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
cache: pnpm

- name: Install
run: npm install
run: pnpm install --frozen-lockfile

- name: Changeset Status
if: github.event_name == 'pull_request' && github.head_ref != 'changeset-release/main'
run: npx changeset status --since origin/main
run: pnpm exec changeset status --since origin/main

- name: Lint
run: npm run lint
run: pnpm run lint

- name: Type check
run: npm run check-types
run: pnpm run check-types

# The schema tests guard a contract a human cannot eyeball: unique and
# resolvable JSON-LD ids, and FAQ answers matching the rendered ones.
# Unrun, they are documentation.
- name: Test
run: npm run test
run: pnpm run test

- name: Build
run: npm run build
run: pnpm run build
12 changes: 6 additions & 6 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,23 @@ jobs:
with:
fetch-depth: 0

- name: Setup pnpm
uses: pnpm/action-setup@v4

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 24
registry-url: https://registry.npmjs.org
cache: npm

- name: Upgrade npm for OIDC trusted publishing
run: npm install -g npm@11.5.1
cache: pnpm

- name: Install dependencies
run: npm ci
run: pnpm install --frozen-lockfile

- name: Create release PR or publish
uses: changesets/action@v1
with:
publish: npm run release --workspace=apps/cli
publish: pnpm --filter diffhub release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_CONFIG_PROVENANCE: "true"
43 changes: 22 additions & 21 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@ GitHub PR-style local diff viewer. Monorepo with three apps and one shared packa

```bash
# Development
npm run dev # Start all apps via Turbo
npm run build # Build all apps
npm run check-types # TypeScript check across all workspaces
npm run test # vitest across workspaces
npm run test --workspace=apps/cli -- lib/git.test.ts --reporter=dot # one file, quiet
pnpm run dev # Start all apps via Turbo
pnpm run build # Build all apps
pnpm run check-types # TypeScript check across all workspaces
pnpm run test # vitest across workspaces
pnpm --filter diffhub test -- lib/git.test.ts --reporter=dot # one file, quiet

# Quality
npm run lint # oxlint via Turbo
npm run lint:fix # oxlint --fix via Turbo
npm run format # oxfmt --write via Turbo
npm run check # ultracite check (lint + format)
npm run fix # ultracite fix (lint + format --fix)
pnpm run lint # oxlint via Turbo
pnpm run lint:fix # oxlint --fix via Turbo
pnpm run format # oxfmt --write via Turbo
pnpm run check # ultracite check (lint + format)
pnpm run fix # ultracite fix (lint + format --fix)
```

Run all commands from the **monorepo root**. Do not `cd` into individual apps for routine tasks.
Expand All @@ -31,22 +31,23 @@ diffhub/
├── apps/docs/ # Documentation MDX content (deploys to blode.md)
├── packages/diff-core/ # Shared viewer: streaming, CodeView wiring, themes, worker pool, chrome
├── turbo.json # Task pipelines
└── package.json # Root workspace (npm workspaces)
├── pnpm-workspace.yaml # Root workspace (pnpm workspaces)
└── package.json
```

`apps/docs/` is pure MDX content with no `package.json`; it is not an npm workspace. Deploy with `cd apps/docs && npx blodemd push docs`.
`apps/docs/` is pure MDX content with no `package.json`; it is not a pnpm workspace. Deploy with `cd apps/docs && pnpm dlx blodemd push docs`.

## Nested AGENTS.md files

Each workspace has its own `AGENTS.md` with boundary rules specific to it: [`apps/cli/AGENTS.md`](apps/cli/AGENTS.md), [`apps/web/AGENTS.md`](apps/web/AGENTS.md), [`apps/docs/AGENTS.md`](apps/docs/AGENTS.md), [`packages/diff-core/AGENTS.md`](packages/diff-core/AGENTS.md). Codex only reads `AGENTS.md` files from the repo root down to its current working directory, so an agent editing a package should read that package's `AGENTS.md` first, not just this root file.

## Gotchas

- **No inner lockfile**: `apps/cli/package-lock.json` must not exist; only the root lockfile is used. If it appears, delete it and run `npm install` from root.
- **Changesets gate PRs**: CI runs `npx changeset status --since origin/main`, so a PR that touches a workspace package (even its AGENTS.md) needs `npx changeset`, or `npx changeset add --empty` when nothing ships.
- **`format:check` is not read-only in diff-core**: its script is `oxfmt .`, which rewrites files. Use `npm run check` for a read-only format check.
- **CLI dev uses portless**: `npm run dev` in `apps/cli` serves at `https://diffhub.localhost`. The marketing site (`apps/web`) serves at `https://diffhub-web.localhost`.
- **CLI uses standalone build**: `bin/diffhub.mjs` runs `.next/standalone/apps/cli/server.js` (not `next start`). Build it with `npm run prepack --workspace=apps/cli`, which runs `next build` and then copies `.next/static/` and `public/` into `.next/standalone/`. `npm run build` alone leaves the CLI without static assets.
- **No inner lockfile**: `apps/cli/package-lock.json` must not exist; only the root `pnpm-lock.yaml` is used. If a lockfile appears there, delete it and run `pnpm install` from root.
- **Changesets gate PRs**: CI runs `pnpm exec changeset status --since origin/main`, so a PR that touches a workspace package (even its AGENTS.md) needs `pnpm exec changeset`, or `pnpm exec changeset add --empty` when nothing ships.
- **`format:check` is not read-only in diff-core**: its script is `oxfmt .`, which rewrites files. Use `pnpm run check` for a read-only format check.
- **CLI dev uses portless**: `pnpm run dev` in `apps/cli` serves at `https://diffhub.localhost`. The marketing site (`apps/web`) serves at `https://diffhub-web.localhost`.
- **CLI uses standalone build**: `bin/diffhub.mjs` runs `.next/standalone/apps/cli/server.js` (not `next start`). Build it with `pnpm --filter diffhub run prepack`, which runs `next build` and then copies `.next/static/` and `public/` into `.next/standalone/`. `pnpm run build` alone leaves the CLI without static assets.
- **Env for dev**: Set `DIFFHUB_REPO` in `apps/cli/.env.local` to point at a real git repo when developing. Without it, the diff API defaults to `process.cwd()`.
- **Marketing site proxies docs**: `apps/web/app/docs/[[...slug]]/route.ts` proxies `/docs/*` to `https://diffhub.blode.md/docs/*` through `apps/web/lib/docs-proxy.ts`, which rewrites the upstream's `/_docs/_next/` asset URLs to `/diffhub/docs/_chunks/*` so they stay inside the zone prefix blode.co forwards to us. This will 404 until the docs site is deployed via blodemd.
- **Docs assets track blode.md, not this repo**: `UPSTREAM_ASSET_PREFIX` in `docs-proxy.ts` is the platform's Next.js `assetPrefix`. It has changed under us once, which left every docs page unstyled. Chunks are also served only from the apex `blode.md`; the tenant host `diffhub.blode.md` emits those URLs but 404s on them. If the docs render with no CSS, diff the upstream HTML's asset paths against `UPSTREAM_ASSET_PREFIX` first.
Expand All @@ -65,15 +66,15 @@ Three options, in order of preference:
CI (`.github/workflows/ci.yml`) runs changeset status, then:

```bash
npm run lint && npm run check-types && npm run test && npm run build
pnpm run lint && pnpm run check-types && pnpm run test && pnpm run build
```

All four pass on `main` as of 27 Sep 2026. `npm run check` does not: `apps/cli/README.md` and `.captain/browser/report.md` are unformatted, and CI does not run it, so judge it by the files you touched. For a viewer change, prove it in the real app:
All four pass on `main` as of 27 Sep 2026. `pnpm run check` does not: `apps/cli/README.md` and `.captain/browser/report.md` are unformatted, and CI does not run it, so judge it by the files you touched. For a viewer change, prove it in the real app:

```bash
npm run prepack --workspace=apps/cli
pnpm --filter diffhub run prepack
node apps/cli/bin/diffhub.mjs serve --port 2099 --no-open --repo "$PWD"
curl -s localhost:2099/api/files # JSON file list; /api/diff streams text/plain
```

There is no `npm run doctor`, `npm run verify`, or feature map. Gap: nothing scripts the build, serve, and API or browser check above, so UI behaviour (scroll anchoring, comments, themes) is proven only by the unit tests and by hand; `apps/web` has a Playwright `test:instant` that CI does not run.
There is no `pnpm run doctor`, `pnpm run verify`, or feature map. Gap: nothing scripts the build, serve, and API or browser check above, so UI behaviour (scroll anchoring, comments, themes) is proven only by the unit tests and by hand; `apps/web` has a Playwright `test:instant` that CI does not run.
16 changes: 8 additions & 8 deletions apps/cli/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,21 @@ This version has breaking changes — APIs, conventions, and file structure may
## Commands

```bash
npm run dev # portless run next dev → https://diffhub.localhost
npm run build # next build → .next/standalone/apps/cli/server.js
npm run start # next start (the CLI binary itself serves on 2047)
npm run lint # oxlint .
npm run check-types # tsc --noEmit
npm run test # vitest run
pnpm run dev # portless run next dev → https://diffhub.localhost
pnpm run build # next build → .next/standalone/apps/cli/server.js
pnpm run start # next start (the CLI binary itself serves on 2047)
pnpm run lint # oxlint .
pnpm run check-types # tsc --noEmit
pnpm run test # vitest run
```

After `npm run build`, copy static assets before running the standalone server:
After `pnpm run build`, copy static assets before running the standalone server:

```bash
cp -r .next/static .next/standalone/apps/cli/.next/static
```

`npm run prepack` (and `npm publish` / `npm pack`) does this automatically.
`pnpm run prepack` (and `pnpm publish` / `pnpm pack`) does this automatically.

## Gotchas

Expand Down
6 changes: 3 additions & 3 deletions apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"dev": "portless run --force next dev",
"build": "next build",
"prepare:standalone": "node ./bin/prepare-standalone.mjs",
"prepack": "next build && npm run prepare:standalone",
"prepack": "next build && pnpm run prepare:standalone",
"start": "next start",
"lint": "oxlint .",
"lint:fix": "oxlint --fix .",
Expand All @@ -45,7 +45,7 @@
"test:watch": "vitest",
"changeset": "changeset",
"changeset:version": "changeset version",
"release": "next build && npm run prepare:standalone && changeset publish"
"release": "next build && pnpm run prepare:standalone && changeset publish"
},
"dependencies": {
"chokidar": "^5.0.0",
Expand All @@ -54,7 +54,7 @@
"devDependencies": {
"@base-ui/react": "^1.8.0",
"@changesets/cli": "^3.0.3",
"@diffhub/diff-core": "*",
"@diffhub/diff-core": "workspace:*",
"@pierre/diffs": "^1.4.3",
"@pierre/trees": "^1.0.0-beta.6",
"@tailwindcss/postcss": "^4",
Expand Down
10 changes: 5 additions & 5 deletions apps/docs/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
# apps/docs

Pure MDX content, not an npm workspace (no `package.json`). Deploys via `blodemd`. See the root [`AGENTS.md`](../../AGENTS.md) for repo-wide commands and gotchas.
Pure MDX content, not a pnpm workspace (no `package.json`). Deploys via `blodemd`. See the root [`AGENTS.md`](../../AGENTS.md) for repo-wide commands and gotchas.

## Commands

`blodemd` is not a devDependency of this repo; run it via `npx` from the **monorepo root**, pointing at this directory:
`blodemd` is not a devDependency of this repo; run it via `pnpm dlx` from the **monorepo root**, pointing at this directory:

```bash
npx blodemd validate apps/docs # validates docs.json; verified passing in this checkout
pnpm dlx blodemd validate apps/docs # validates docs.json; verified passing in this checkout
```

Deploy (from the root AGENTS.md, not re-verified here): `cd apps/docs && npx blodemd push docs`.
Deploy (from the root AGENTS.md, not re-verified here): `cd apps/docs && pnpm dlx blodemd push docs`.

There is no local dev-preview or lint command verified for this checkout; do not assume `npx blodemd dev` works here without trying it first.
There is no local dev-preview or lint command verified for this checkout; do not assume `pnpm dlx blodemd dev` works here without trying it first.

## Boundary

Expand Down
2 changes: 1 addition & 1 deletion apps/web/lib/docs-proxy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { proxyDocsRequest, rewriteDocsHtml, toUpstreamPath } from "./docs-proxy"
* keeps passing while production breaks. Refresh the fixture when the docs
* origin changes, and run the live check below after any blode.md change:
*
* DOCS_PROXY_LIVE=1 npm run test --workspace @diffhub/web
* DOCS_PROXY_LIVE=1 pnpm --filter @diffhub/web run test
*/
const FIXTURE = readFileSync(path.join(import.meta.dirname, "docs-proxy.fixture.html"), "utf-8");

Expand Down
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
},
"dependencies": {
"@base-ui/react": "^1.8.0",
"@diffhub/diff-core": "*",
"@diffhub/diff-core": "workspace:*",
"@pierre/diffs": "^1.4.3",
"@pierre/trees": "^1.0.0-beta.6",
"@tailwindcss/typography": "^0.5.20",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { defineConfig, devices } from "@playwright/test";

/**
* Production-build e2e (`npm run test:instant`): instant navigation, and the
* Production-build e2e (`pnpm run test:instant`): instant navigation, and the
* live PR viewer's 404 status. It runs against `next start`, so the build has
* to expose the testing API that `@next/playwright`'s `instant()` drives: the
* script builds with `NEXT_EXPOSE_TESTING_API=1` first. A normal production
Expand Down
2 changes: 1 addition & 1 deletion apps/web/vercel.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"ignoreCommand": "case \"$VERCEL_GIT_COMMIT_MESSAGE\" in *\"[skip ci]\"*) exit 0 ;; esac; if [ \"$VERCEL_ENV\" = \"preview\" ]; then exit 0; fi; npx turbo-ignore"
"ignoreCommand": "case \"$VERCEL_GIT_COMMIT_MESSAGE\" in *\"[skip ci]\"*) exit 0 ;; esac; if [ \"$VERCEL_ENV\" = \"preview\" ]; then exit 0; fi; pnpm dlx turbo-ignore"
}
2 changes: 1 addition & 1 deletion apps/web/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default defineConfig({
"@": path.resolve(import.meta.dirname),
},
},
// `e2e/` is Playwright's (`npm run test:instant`), not vitest's.
// `e2e/` is Playwright's (`pnpm run test:instant`), not vitest's.
test: {
exclude: [...configDefaults.exclude, "e2e/**"],
},
Expand Down
6 changes: 2 additions & 4 deletions lefthook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,9 @@ pre-commit:
# only when it actually has files to work on.
- name: oxfmt
glob: "*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,json,jsonc,css}"
exclude:
- package-lock.json
run: npx oxfmt --write {staged_files}
run: pnpm exec oxfmt --write {staged_files}
stage_fixed: true
- name: oxlint
glob: "*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}"
run: npx oxlint --fix {staged_files}
run: pnpm exec oxlint --fix {staged_files}
stage_fixed: true
Loading
Loading