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
36 changes: 23 additions & 13 deletions CLAUDE.md

Large diffs are not rendered by default.

17 changes: 10 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ A local diff viewer, built on a vendored fork of Pierre's [`@pierre/diffs`](http

diffdeck is the local diff viewer originally embedded in [cc-statusline](https://github.com/say8425/cc-statusline), extracted into its own product. Instead of depending on the upstream Pierre packages — which move fast (`@pierre/diffs` churns heavily; `@pierre/trees` is pre-1.0 beta) and whose internal markup we had already coupled to heavily — diffdeck **recovers the original TypeScript from the packages' source maps and vendors it**, so we own the rendering engine outright.

The result is a Bun-workspace monorepo where a commodity-hard, framework-agnostic diff engine (Pierre's `CodeView`, ~27k lines) is kept as-is, while the parts we customize live in our own code.
The result is a Bun-workspace monorepo where a commodity-hard, framework-agnostic diff engine (Pierre's `CodeView`, the ~29.5k lines of `packages/diffs`) is kept as-is, while the parts we customize live in our own code.

## Features

Expand Down Expand Up @@ -102,7 +102,7 @@ These view flags set the initial state for this launch only — they don't chang
your saved preferences, and the in-app toggles reflect the launched state.

Environment: `DIFFDECK_PORT` sets the default port. The token is cached under
`~/.cache/diffdeck/`.
`$XDG_CACHE_HOME/diffdeck/`, or `~/.cache/diffdeck/` when that is unset.

## Skills

Expand Down Expand Up @@ -165,11 +165,13 @@ packages/
theming/ @diffdeck/theming theme system + 10 vendored shiki theme JSONs
diffs/ @diffdeck/diffs CodeView diff-rendering engine
trees/ @diffdeck/trees FileTree engine (vanilla render)
apps/viewer/ @say8425/diffdeck — CLI + diff-server (data API) + browser viewer + agent skill
apps/viewer/ @say8425/diffdeck — CLI + diff-server (data API) + browser viewer
skills/ the agent skill (SKILL.md), copied into the package at build time
scripts/ source-map extraction tool, css-inline Bun plugin, render-parity harness
docs/ README translations and screenshots
```

Dependency graph: `path-store` (no deps) ← `trees`; `theming` (shiki) ← `diffs`, `trees`. Runtime externals: shiki + `@shikijs/*`, `diff`, `hast-util-to-html`, `lru_map`.
Dependency graph: `path-store` (no deps) ← `trees`; `theming` (shiki) ← `diffs`, `trees`. Runtime externals: shiki + `@shikijs/*`, `diff`, `hast-util-to-html`, `lru_map` — these are the packages' own dependencies; the published CLI bundles everything, so nothing is resolved at install time.

## Development

Expand All @@ -191,9 +193,10 @@ Three lanes:
collection, so this never launches a browser.
- `bun run test:coverage` — the same suite with a **100% coverage gate on
diffdeck's owned runtime code** (`apps/viewer/{browser,cli,server}`).
Intentionally out of the gate: the vendored `packages/*`, the browser entry
`main.ts` (integration entry — exercised by the e2e suite instead, not
in-process), and `build.ts`.
Intentionally out of the gate: the vendored `packages/*`, the tooling under
`scripts/`, the browser entry `main.ts` (integration entry — exercised by the
e2e suite instead, not in-process), `build.ts`, and the specs themselves
(`*.test.ts`, `e2e/**`).
- `bun run test:e2e` — the Playwright real-browser suite (`apps/viewer/e2e/`).
Drives the system Google Chrome via `channel: "chrome"` (no Chromium
download) and covers `main.ts` and the vendored render paths end-to-end.
Expand Down
31 changes: 16 additions & 15 deletions apps/viewer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,24 @@ default browser.

## Options

| Flag | Description |
| ----------------- | ------------------------------------------------------------ |
| `--port <n>` | Port to serve on (default: `$DIFFDECK_PORT` or `49573`) |
| `--no-open` | Do not open a browser automatically (prints the URL instead) |
| `--untracked` | Start with untracked files included |
| `--watch` | Start with watch (auto-refresh) on |
| `--no-flatten` | Start with the file tree un-flattened (flatten is on) |
| `--tree-right` | Start with the file tree on the right |
| `--split` | Start in split view (unified is the default) |
| `--hide-tree` | Start with the file tree hidden |
| `-h`, `--help` | Show help |
| `-v`, `--version` | Show version |
| Flag | Description |
| ------------------ | ------------------------------------------------------------ |
| `--port <n>` | Port to serve on (default: `$DIFFDECK_PORT` or `49573`) |
| `--no-open` | Do not open a browser automatically (prints the URL instead) |
| `--untracked` | Start with untracked files included |
| `--watch` | Start with watch (auto-refresh) on |
| `--no-flatten` | Start with the file tree un-flattened (flatten is on) |
| `--tree-right` | Start with the file tree on the right |
| `--split` | Start in split view (unified is the default) |
| `--hide-tree` | Start with the file tree hidden |
| `--fold-with-tree` | Start with sidebar directory collapse synced to diff folds |
| `-h`, `--help` | Show help |
| `-v`, `--version` | Show version |

The view flags (`--untracked`, `--watch`, `--no-flatten`, `--tree-right`,
`--split`, `--hide-tree`) set the initial state for this launch only — they
don't change your saved preferences, and the in-app toggles reflect the
launched state.
`--split`, `--hide-tree`, `--fold-with-tree`) set the initial state for this
launch only — they don't change your saved preferences, and the in-app toggles
reflect the launched state.

## Environment

Expand Down
9 changes: 0 additions & 9 deletions apps/viewer/__tests__/diff-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { describe, expect, test } from "bun:test";
import {
DEFAULT_DIFF_PORT,
getCacheDir,
isDiffViewerDisabled,
resolveDiffPort,
} from "../server/config.ts";

Expand All @@ -20,14 +19,6 @@ describe("resolveDiffPort", () => {
});
});

describe("isDiffViewerDisabled", () => {
test("true only when exactly '1'", () => {
expect(isDiffViewerDisabled({ DIFFDECK_DISABLE: "1" })).toBe(true);
expect(isDiffViewerDisabled({ DIFFDECK_DISABLE: "0" })).toBe(false);
expect(isDiffViewerDisabled({})).toBe(false);
});
});

describe("getCacheDir", () => {
test("respects XDG_CACHE_HOME", () => {
expect(getCacheDir({ XDG_CACHE_HOME: "/tmp/xdg" })).toBe(
Expand Down
75 changes: 75 additions & 0 deletions apps/viewer/__tests__/docs-flags-parity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { describe, expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { HELP } from "../cli.ts";

const repoRoot = join(import.meta.dir, "..", "..", "..");

// cli.ts의 HELP가 CLI 표면의 단일 진실이고, 아래 문서들은 전부 그 표면을
// 사람에게 되풀이해 말한다. 예전엔 SKILL.md 하나만 대조했는데, 그 사이
// `--fold-with-tree`가 npm README(apps/viewer/README.md)와 번역 4종에서
// 조용히 빠져 있었다 — 플래그를 만든 #14은 루트 README와 CLAUDE.md만
// 갱신했고, SKILL.md는 이 테스트를 만든 #24이 뒤늦게 채웠기 때문이다.
// 게이트가 감시하는 문서만 따라잡힌다. 그래서 목록을 전부로 넓혔다.
const DOCS = [
"skills/diffdeck/SKILL.md",
"README.md",
"apps/viewer/README.md",
"docs/README.ko.md",
"docs/README.ja.md",
"docs/README.zh.md",
"docs/README.es.md",
] as const;

const metaFlags = new Set(["--help", "--version"]);

const extractOptionsFlags = (help: string): string[] => {
const optionsBlock = help.split("Options:")[1].split(/\n\s*\n/)[0];
const tokens = optionsBlock.match(/--[a-z][a-z-]*/g) ?? [];
return [...new Set(tokens)].filter((flag) => !metaFlags.has(flag));
};

/**
* 문서에서 플래그는 항상 코드 스팬으로 적힌다: `--split` 또는 `--port <n>`.
* 여는 백틱을 요구해 산문 속 우연한 일치를 막고, 뒤에 백틱이나 공백을 요구해
* `--tree-right`가 가상의 `--tree-right-foo`에 매칭되지 않게 한다.
*
* 이 게이트의 한계를 알고 쓸 것 — 셋 다 실측으로 확인했다:
* 1. **단방향**이다. HELP의 각 플래그가 문서에 있는지만 본다. 플래그를
* 삭제하면 문서에 남은 유령 플래그가 그대로 통과한다.
* 2. **언급**을 증명하지 실제 **위치**를 증명하지 않는다. 표의 행을 지우고
* 같은 코드 스팬을 산문 한 줄로 옮겨도 통과한다. 그래도 표 문법에
* 앵커를 걸지 않는 건, 번역 4종의 컬럼 폭이 제각각이고 SKILL.md는
* 애초에 표가 아니라서다 — 앵커를 걸면 게이트가 먼저 부서진다.
* 3. `Options:` 블록만 읽는다. `install-skill`의 `--codex`·`--project`는
* Commands 섹션이라 이 게이트 밖이다.
* 무는 곳은 확실히 문다: 번역에서 행 하나를 지우면 그 문서만 빨간불이 되고,
* HELP에 새 플래그를 넣으면 일곱 문서가 한꺼번에 빨간불이 된다.
*/
const documents = (content: string, flag: string): boolean =>
new RegExp(`\`${flag}[\`\\s]`).test(content);

describe("CLI flag parity between cli.ts HELP and every doc that lists flags", () => {
const flags = extractOptionsFlags(HELP);

test("HELP's Options block has flags to check", () => {
expect(flags.length).toBeGreaterThan(0);
});

test("HELP's Options block excludes meta-flags --help/--version", () => {
expect(flags).not.toContain("--help");
expect(flags).not.toContain("--version");
});

// 파일 읽기는 test 콜백 **안**에서 한다. describe 본문에서 읽으면 경로가
// 하나만 틀려도 테스트가 등록되기 전에 throw해서, "문서 X가 없다"가 아니라
// 정체불명의 수집 크래시로 보인다 — 위 가드 두 개조차 돌지 않는다.
for (const doc of DOCS) {
for (const flag of flags) {
test(`${doc} documents ${flag}`, () => {
const content = readFileSync(join(repoRoot, doc), "utf8");
expect(documents(content, flag)).toBe(true);
});
}
}
});
43 changes: 0 additions & 43 deletions apps/viewer/__tests__/skill-flags-parity.test.ts

This file was deleted.

3 changes: 0 additions & 3 deletions apps/viewer/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ export const resolveDiffPort = (env: Env = process.env): number => {
return Number.isInteger(n) && n > 0 && n < 65536 ? n : DEFAULT_DIFF_PORT;
};

export const isDiffViewerDisabled = (env: Env = process.env): boolean =>
env.DIFFDECK_DISABLE === "1";

export const getCacheDir = (env: Env = process.env): string => {
const base = env.XDG_CACHE_HOME || join(env.HOME || homedir(), ".cache");
return join(base, "diffdeck");
Expand Down
5 changes: 4 additions & 1 deletion bunfig.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ root = "."
# is intentionally NOT preloaded here. Registering happy-dom globally replaces
# Node's fetch/http with happy-dom's implementations for the *entire* test run,
# which broke apps/viewer's real HTTP server tests (diff-server, api/blob,
# built-serving — 21 failures). Trees view/DOM tests must instead
# built-serving) with `NetworkError: ... Parse Error` out of happy-dom's fetch.
# The blast radius scales with the suite: 21 failures when first hit, 58 when
# re-measured on 2026-08-05 — don't treat the count as a constant, just keep
# happy-dom out of this list. Trees view/DOM tests must instead
# `import "../__tests__/happydom";` at the top of each file that needs a DOM
# (see happydom-smoke.test.ts).
preload = ["./scripts/parity/preload.ts"]
Expand Down
13 changes: 8 additions & 5 deletions docs/README.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Un visor de diff local, construido sobre un fork vendorizado de los paquetes de

diffdeck es el visor de diff local que originalmente estaba embebido en [cc-statusline](https://github.com/say8425/cc-statusline), ahora extraído como un producto propio. En lugar de depender de los paquetes originales de Pierre — que evolucionan rápido (`@pierre/diffs` cambia mucho; `@pierre/trees` está en beta pre-1.0) y cuyo markup interno ya estaba fuertemente acoplado a nuestro código —, diffdeck **recupera el TypeScript original a partir de los source maps de los paquetes y lo vendoriza**, de modo que somos dueños por completo del motor de renderizado.

El resultado es un monorepo de workspaces de Bun donde un motor de diff sólido y agnóstico de framework (el `CodeView` de Pierre, ~27k líneas) se mantiene tal cual, mientras que las partes que personalizamos viven en nuestro propio código.
El resultado es un monorepo de workspaces de Bun donde un motor de diff sólido y agnóstico de framework (el `CodeView` de Pierre — las ~29 500 líneas de `packages/diffs`) se mantiene tal cual, mientras que las partes que personalizamos viven en nuestro propio código.

## Características

Expand Down Expand Up @@ -93,12 +93,13 @@ Opciones:
| `--tree-right` | Iniciar con el árbol de archivos a la derecha |
| `--split` | Iniciar en vista split (unified es el valor predeterminado) |
| `--hide-tree` | Iniciar con el árbol de archivos oculto |
| `--fold-with-tree` | Iniciar con el plegado de directorios de la barra lateral sincronizado con el plegado del diff |
| `-h`, `--help` | Mostrar ayuda |
| `-v`, `--version` | Mostrar versión |

Estos flags de vista establecen el estado inicial solo para este lanzamiento — no cambian tus preferencias guardadas, y los toggles dentro de la app reflejan el estado con el que se lanzó.

Entorno: `DIFFDECK_PORT` establece el puerto predeterminado. El token se guarda en caché bajo `~/.cache/diffdeck/`.
Entorno: `DIFFDECK_PORT` establece el puerto predeterminado. El token se guarda en caché bajo `$XDG_CACHE_HOME/diffdeck/`, o `~/.cache/diffdeck/` si esa variable no está definida.

## Skills

Expand Down Expand Up @@ -154,11 +155,13 @@ packages/
theming/ @diffdeck/theming sistema de temas + 10 JSONs de temas shiki vendorizados
diffs/ @diffdeck/diffs motor de renderizado de diffs CodeView
trees/ @diffdeck/trees motor FileTree (render vanilla)
apps/viewer/ @say8425/diffdeck — CLI + diff-server (API de datos) + visor de navegador + skill de agente
apps/viewer/ @say8425/diffdeck — CLI + diff-server (API de datos) + visor de navegador
skills/ la skill de agente (SKILL.md), copiada al paquete durante el build
scripts/ herramienta de extracción de source maps, plugin de Bun css-inline, arnés de paridad de renderizado
docs/ traducciones del README y capturas de pantalla
```

Grafo de dependencias: `path-store` (sin dependencias) ← `trees`; `theming` (shiki) ← `diffs`, `trees`. Externals en runtime: shiki + `@shikijs/*`, `diff`, `hast-util-to-html`, `lru_map`.
Grafo de dependencias: `path-store` (sin dependencias) ← `trees`; `theming` (shiki) ← `diffs`, `trees`. Externals en runtime: shiki + `@shikijs/*`, `diff`, `hast-util-to-html`, `lru_map` — son dependencias de los propios paquetes; la CLI publicada lo empaqueta todo, así que no se resuelve nada al instalar.

## Desarrollo

Expand All @@ -177,7 +180,7 @@ bun run format # oxfmt
Tres carriles:

- `bun test` — tests unitarios/de integración, rápidos. Las specs `*.e2e.ts` quedan excluidas de la recolección, así que esto nunca lanza un navegador.
- `bun run test:coverage` — la misma suite con una **puerta de cobertura del 100% sobre el código de runtime propio de diffdeck** (`apps/viewer/{browser,cli,server}`). Intencionalmente fuera de la puerta: los `packages/*` vendorizados, el punto de entrada del navegador `main.ts` (entry de integración — ejercitado por la suite e2e en su lugar, no in-process), y `build.ts`.
- `bun run test:coverage` — la misma suite con una **puerta de cobertura del 100% sobre el código de runtime propio de diffdeck** (`apps/viewer/{browser,cli,server}`). Intencionalmente fuera de la puerta: los `packages/*` vendorizados, las herramientas bajo `scripts/`, el punto de entrada del navegador `main.ts` (entry de integración — ejercitado por la suite e2e en su lugar, no in-process), `build.ts` y las propias specs (`*.test.ts`, `e2e/**`).
- `bun run test:e2e` — la suite de Playwright con navegador real (`apps/viewer/e2e/`). Conduce el Google Chrome del sistema vía `channel: "chrome"` (sin descarga de Chromium) y cubre `main.ts` y las rutas de render vendorizadas de extremo a extremo.

### Arnés de paridad de renderizado
Expand Down
Loading
Loading