spb/spbgit Public MIT
SPB Git — the platform hosting itself
JavaScript 73.9%
CSS 11.7%
Nunjucks 11.6%
Shell 2.7%
1# CLAUDE.md — **SPB Git** · Personal Git Platform for Simon-Pierre Boucher23> **Read this entire document before writing a single line of code.**4> This file is the single source of truth for the project. Every architectural decision, naming convention, UI detail, and non-negotiable rule lives here. When in doubt, re-read this file. When this file conflicts with your instinct, this file wins.56---78## 0. Identity & Non-Negotiables910| Key | Value |11|---|---|12| **Product name** | SPB Git |13| **Owner / sole user** | Simon-Pierre Boucher |14| **Contact** | [contact@spboucher.ai](mailto:contact@spboucher.ai) |15| **Public domain** | `https://git.spboucher.ai` (ngrok custom domain) |16| **Deployment host** | Node **m3u96a** |17| **Runtime** | Node.js ≥ 20, ESM only |18| **Visibility model** | **ALL repositories are PUBLIC (read-only)**. Write access = owner only. No sign-up, no visitor accounts, ever. |1920### 0.1 THE GOLDEN RULE — Mandatory Author Header2122**Every single file you generate — source, config, script, style, test, docs tooling — MUST begin with an author header.** No exceptions. A file without this header is a bug.2324Canonical template (adapt comment syntax per language):2526```js27/**28 * ─────────────────────────────────────────────29 * SPB Git — Personal Git Platform30 * ─────────────────────────────────────────────31 * Author : Simon-Pierre Boucher32 * Contact : contact@spboucher.ai33 * File : <relative/path/filename.ext>34 * Purpose : <one-line description>35 * License : MIT © Simon-Pierre Boucher36 * ─────────────────────────────────────────────37 */38```3940Per-language syntax:4142| Language | Syntax |43|---|---|44| JS / TS / CSS / SCSS | `/** ... */` block |45| Python / Bash / YAML / Dockerfile / TOML | `#` lines |46| HTML / Vue / Markdown (when appropriate) | `<!-- ... -->` |47| SQL | `-- ` lines |48| EJS / Nunjucks templates | `<%# ... %>` / `{# ... #}` |4950Additionally, the server itself must **inject/verify** this header: a lint script `npm run check:headers` fails CI if any tracked file in *this* project lacks the header. Provide `scripts/inject-headers.mjs` to add headers in bulk.5152---5354## 1. Product Vision5556SPB Git is not a toy. It is a **polished, self-hosted software forge** — the personal equivalent of GitHub — that serves three purposes:57581. **Canonical home** for all of Simon-Pierre's repositories, clonable by anyone at `https://git.spboucher.ai/<repo>.git`.592. **Public showcase / portfolio**: every repo page must look as good as (or better than) a GitHub repo page — rendered README with badges, syntax-highlighted code browsing, commit graphs, language stats.603. **Frictionless daily driver**: a powerful CLI (`spbgit`) that manages *all* repos from the terminal — create, commit, push, sync, open — in single commands.6162The bar for quality is: *"If a stranger lands on git.spboucher.ai, they should assume a small team built this."*6364---6566## 2. High-Level Architecture6768```69┌──────────────────────────── node m3u96a ────────────────────────────┐70│ │71│ ┌─────────────┐ ┌───────────────────────────────────────────────┐ │72│ │ ngrok │ │ SPB Git Server (Node 20) │ │73│ │ tunnel │──▶│ Fastify app :7420 │ │74│ │ git.spboucher│ │ ├─ / Web UI (SSR, Nunjucks) │ │75│ │ .ai │ │ ├─ /api/v1/* JSON API │ │76│ └─────────────┘ │ ├─ /:repo.git/* Git Smart HTTP (backend) │ │77│ │ ├─ /raw/* Raw file serving │ │78│ │ └─ /archive/* zip/tar.gz snapshots │ │79│ └───────────────┬───────────────────────────────┘ │80│ │ │81│ ┌──────────────────────────┼───────────────────────────┐ │82│ │ │ │ │83│ ┌──────▼──────┐ ┌────────────────▼─────────┐ ┌──────────────▼───┐ │84│ │ /srv/git/ │ │ /srv/spbgit/data/ │ │ /srv/spbgit/ │ │85│ │ *.git │ │ meta.json (repo index) │ │ cache/ │ │86│ │ bare repos │ │ tokens, hooks, stats │ │ rendered HTML, │ │87│ │ (source of │ │ │ │ lang stats, zips │ │88│ │ truth) │ │ │ │ │ │89│ └─────────────┘ └──────────────────────────┘ └──────────────────┘ │90└───────────────────────────────────────────────────────────────────────┘91```9293**Principles:**94- **Filesystem is the database.** Bare Git repos under `/srv/git/` are the source of truth. A single `meta.json` holds per-repo metadata (description, topics, pinned, homepage, created date). No SQL/NoSQL database.95- **Cache aggressively, invalidate on push.** Rendered READMEs, language stats, commit counts, and archives are cached in `/srv/spbgit/cache/` keyed by `<repo>@<commit-sha>`; a post-receive hook busts the cache.96- **SSR-first web UI.** Server-rendered pages (Nunjucks templates + a small vanilla JS layer). No SPA framework. Fast, crawlable, sharp.9798### 2.1 Repository Layout of *this* project99100```101spbgit/102├── CLAUDE.md # this file103├── README.md # project readme (with badges, of course)104├── package.json105├── src/106│ ├── server.mjs # Fastify bootstrap107│ ├── config.mjs # env + config loading (zod-validated)108│ ├── git/109│ │ ├── smart-http.mjs # upload-pack / receive-pack over HTTP110│ │ ├── repo.mjs # repo model: branches, tree, blob, log, blame111│ │ ├── hooks.mjs # post-receive hook installer + handlers112│ │ └── archive.mjs # zip / tar.gz snapshot generation113│ ├── auth/114│ │ └── token.mjs # owner PAT verification (argon2-hashed)115│ ├── render/116│ │ ├── markdown.mjs # GFM pipeline (markdown-it + plugins)117│ │ ├── highlight.mjs # code highlighting (shiki)118│ │ └── og-image.mjs # dynamic Open Graph card per repo (satori/resvg)119│ ├── api/120│ │ └── v1.mjs # JSON API routes121│ ├── web/122│ │ ├── routes.mjs # HTML routes123│ │ ├── views/ # Nunjucks templates124│ │ └── assets/ # css (hand-written, design tokens), js, fonts, logo125│ └── stats/126│ ├── languages.mjs # linguist-style language detection + percentages127│ └── activity.mjs # commit calendar / punch-card data128├── cli/129│ ├── spbgit.mjs # entry point (bin)130│ └── commands/ # one file per command131├── deploy/132│ ├── ngrok.yml133│ ├── ecosystem.config.cjs # pm2: server + ngrok, autorestart134│ ├── spbgit.service # systemd alternative135│ ├── setup-m3u96a.sh # idempotent server bootstrap script136│ └── backup.sh # nightly tar of /srv/git + meta.json137├── scripts/138│ ├── inject-headers.mjs139│ └── check-headers.mjs140└── test/ # vitest: unit + integration (smart HTTP round-trip)141```142143---144145## 3. Git Hosting Engine146147### 3.1 Smart HTTP protocol148149- Implement Git Smart HTTP v2 by spawning `git upload-pack` / `git receive-pack` (via `git http-backend` or direct spawn with proper pkt-line streaming — **stream, never buffer** packfiles).150- Routes:151 - `GET /:repo.git/info/refs?service=git-upload-pack` → public (clone/fetch/pull).152 - `POST /:repo.git/git-upload-pack` → public.153 - `GET /:repo.git/info/refs?service=git-receive-pack` → **auth required**.154 - `POST /:repo.git/git-receive-pack` → **auth required** (push).155- Auth for push: HTTP Basic where username = `spb` and password = a **Personal Access Token**. Tokens are generated by `spbgit token new`, stored argon2-hashed in `data/tokens.json`, revocable, with a label and last-used timestamp.156- Set correct `Content-Type`, chunked encoding, gzip request body support (`Content-Encoding: gzip` from git clients), and side-band progress passthrough.157- Repo names: `^[a-z0-9][a-z0-9._-]{0,63}$`. Reject path traversal hard.158159### 3.2 Post-receive hooks (installed automatically in every bare repo)160161On every push, the hook POSTs to `http://127.0.0.1:7420/internal/hooks/post-receive` (localhost-only route) which:1621. Busts all caches for that repo.1632. Recomputes language stats + default-branch README render (warm cache).1643. Appends an event to the **activity feed** (`data/activity.jsonl`).1654. Regenerates the repo's OG image.166167### 3.3 Repo lifecycle168169- `POST /api/v1/repos` (auth) → `git init --bare`, install hooks, write `meta.json` entry, set `HEAD` to `refs/heads/main`.170- `PATCH /api/v1/repos/:name` (auth) → description, topics (max 10), homepage URL, pinned flag, default branch.171- `DELETE /api/v1/repos/:name` (auth) → moves repo to `/srv/git/.trash/<name>-<timestamp>.git` (soft delete, recoverable for 30 days).172- Everything is public: there is **no** `private` field anywhere in the codebase. Don't build one.173174---175176## 4. Web UI — "It must look sharp" (Design Spec)177178### 4.1 Design system179180- **Aesthetic:** modern developer-forge. Dark theme by default, light theme toggle (persisted in `localStorage`, respects `prefers-color-scheme` on first visit).181- **Design tokens** (CSS custom properties in `assets/css/tokens.css`):182 - Dark: background `#0b0e14`, surface `#11151c`, border `#1f2530`, text `#e6e9ef`, muted `#8b93a3`, accent `#4f8cff`, accent-2 `#22d3aa`, danger `#ff5d5d`.183 - Radius `10px`, subtle 1px borders, no heavy shadows; hover states elevate with border-color shifts, not box-shadows.184- **Typography:** `Inter` (UI) + `JetBrains Mono` (code), self-hosted woff2 in `assets/fonts/` — no third-party font CDNs.185- **Layout:** max-width `1200px`, sticky top nav: logo "**SPB Git**" (monogram SVG), global search input (`/` focuses it), theme toggle, link to `spboucher.ai`.186- Fully responsive down to 360px. Code views scroll horizontally, never break the page.187- Footer on every page: `© Simon-Pierre Boucher · contact@spboucher.ai · Powered by SPB Git`.188189### 4.2 Pages190191**`/` — Home / repo index**192- Hero strip: avatar/monogram, "Simon-Pierre Boucher", tagline, contact link, count badges (X repos · Y commits · Z languages).193- **Pinned repos** grid (up to 6 cards) then full repo list.194- Repo card: name, description, top language with colored dot (GitHub linguist colors), star-like "clone count" if tracked, last-pushed relative time ("3 h ago"), topic chips.195- Controls: search-as-you-type filter, sort by *Recently pushed / Name / Created*, filter by language and topic.196- **Activity feed** sidebar: last 15 pushes ("pushed 3 commits to `api-tools` · 2 h ago").197- **Commit calendar** (GitHub-style contribution heatmap, last 52 weeks, SVG, tooltip per day).198199**`/:repo` — Repo home**200- Header: repo name (breadcrumb `spb / name`), description, topic chips, homepage link, language bar (stacked percentage bar like GitHub, with legend).201- **Clone box**: HTTPS URL with copy button + `spbgit clone <name>` snippet + "Download ZIP".202- Meta row: default branch selector, branch/tag counts, commit count, repo size, license badge (auto-detected from `LICENSE`).203- File table for the current tree: icon, name, last commit message touching that path, relative date (computed efficiently via a single `git log` walk, cached).204- **README.md rendered below the file table.** See §5 — this must be pixel-perfect.205206**`/:repo/tree/:ref/*path`** — directory browsing (same file table pattern).207208**`/:repo/blob/:ref/*path`** — file view:209- Sticky header: path breadcrumb, size, LOC, buttons: **Raw**, **Blame**, **History**, **Copy permalink** (permalink pins the commit SHA).210- Shiki-highlighted code with line numbers; clickable line anchors (`#L42`, ranges `#L10-L20` with highlight).211- Markdown files render as HTML by default with a "View source" toggle. Images display inline. Jupyter notebooks: at minimum render as pretty JSON with a note (nice-to-have: nbconvert-style render).212- Binary files: show size + download button; images/SVG preview inline.213214**`/:repo/commits/:ref`** — paginated history (40/page): avatar-less but with identicon per author email, message (first line, expandable body), short SHA chip (copy on click), relative + absolute date, files-changed count.215216**`/:repo/commit/:sha`** — full diff view: per-file collapsible diffs, syntax-highlighted, green/red gutters, stats header (+X −Y across N files), rename/binary detection.217218**`/:repo/blame/:ref/*path`** — blame view with commit hunks grouped and color-aged (newer = brighter accent).219220**`/:repo/branches`, `/:repo/tags`** — lists with ahead/behind counts vs default branch (branches) and archive download links (tags → releases-lite).221222**`/search?q=`** — global search across repo names, descriptions, topics, and README content (build a tiny inverted index in cache; refresh on push). Grouped results.223224**Error pages** — custom 404/500 with the design system (404: "This ref does not exist in any timeline.").225226### 4.3 Meta / polish227228- Every page: proper `<title>`, meta description, canonical URL, **Open Graph + Twitter card** with the dynamically generated per-repo OG image (repo name, description, language bar — generated with satori → resvg, cached).229- `sitemap.xml` + `robots.txt` + RSS/Atom feed of activity at `/feed.atom`.230- Favicon set (SVG + PNG) with the SPB monogram.231- Lighthouse targets: Performance ≥ 95, A11y ≥ 95. No render-blocking third-party requests at all.232233---234235## 5. README & Markdown Rendering — Badge-Perfect (Critical)236237This is a flagship feature. The rendered README **must be indistinguishable from GitHub's rendering quality**.238239Pipeline (`src/render/markdown.mjs`), built on `markdown-it` +:240- **GFM tables**, **task lists** (rendered checkboxes, disabled), **strikethrough**, **autolinks**, **footnotes**.241- **Heading anchors** with hover link icon; auto-generated slug IDs (GitHub algorithm) so `#installation` links work.242- **Syntax highlighting** in fenced blocks via **shiki** (same themes as file view; dual dark/light output via CSS variables). Support ` ```mermaid ` blocks → render client-side with mermaid.js (lazy-loaded only when present).243- **Badges & images:**244 - Badges (`img.shields.io`, etc.) must render **inline, on one line, correctly sized** — CSS: `img[src*="shields.io"], img[height] { display: inline-block; vertical-align: middle; }` and consecutive badge images separated by single spaces must not wrap awkwardly.245 - **Relative image paths resolve to the raw endpoint**: `./docs/demo.png` → `/raw/:repo/:ref/docs/demo.png`. Same for relative links → blob URLs. This is mandatory; broken relative images are a failing grade.246 - `<img>`/`<picture>`/`<details>`/`<summary>`/`<kbd>`/`<sup>`/`<sub>`/`align` attributes: allowed. Sanitize with a strict allowlist (DOMPurify-style via `sanitize-html`): no scripts, no event handlers, no iframes (except a curated allowlist: YouTube embeds off by default).247- **HTML in Markdown** (centered headers, badge tables — the classic GitHub README style) must render correctly.248- Emoji shortcodes (`:rocket:` → 🚀).249- Look for `README.md` case-insensitively, then `readme.rst`/`README.txt` fallbacks (plain render).250- Cache the rendered HTML per `(repo, sha, theme-agnostic)`.251252Typography of rendered Markdown mirrors GitHub's `.markdown-body`: 16px base, 1.6 line-height, bordered tables with zebra rows, blockquote left-border in muted accent, `code` chips with surface background, hr as 1px border.253254---255256## 6. `spbgit` CLI — Terminal Command Center257258A standalone Node CLI (`cli/spbgit.mjs`, exposed as `bin: { "spbgit": ... }`, installable via `npm i -g` from the repo or `npm link`). Zero heavy deps — use `commander` (or hand-rolled parser), `picocolors` for output, native `fetch`.259260### 6.1 Configuration261262`~/.spbgit/config.json`:263```json264{265 "server": "https://git.spboucher.ai",266 "token": "<PAT>",267 "workspace": "~/code",268 "author": { "name": "Simon-Pierre Boucher", "email": "contact@spboucher.ai" }269}270```271- `spbgit init` — interactive setup wizard (server URL, token paste, workspace dir); sets `git config` user.name/email globally if missing; chmod 600 the config.272- Token is **never** printed back, never logged, never in argv.273274### 6.2 Commands (full spec)275276| Command | Behavior |277|---|---|278| `spbgit init` | Setup wizard (above). |279| `spbgit token new [--label "laptop"]` / `token list` / `token revoke <id>` | Manage PATs via API. |280| `spbgit list` (`ls`) | Table of server repos: name, description, language, last push, clone-URL. `--json` for scripting. |281| `spbgit create <name> [-d "desc"] [--topics a,b] [--push]` | Create on server; with `--push`, also init current dir, add remote `origin`, create initial commit, push. |282| `spbgit clone <name> [dir]` | Clone into workspace (or dir). |283| `spbgit clone --all` | Clone every server repo missing from the workspace. |284| `spbgit status [name\|--all]` | Aggregated status across all workspace repos: branch, ahead/behind, dirty file count. One aligned, colorized row per repo; clean repos dimmed. |285| `spbgit commit <name\|--all> -m "msg"` | `git add -A && git commit` in target repo(s). Skips clean repos with a note. |286| `spbgit push [name\|--all]` | Push current branch; `--all` iterates every workspace repo with commits ahead. |287| `spbgit pull [name\|--all]` | Pull with `--ff-only` by default; `--rebase` flag. |288| `spbgit sync [-m "msg"]` | **The killer command**: for every workspace repo → add-all, commit (default msg `chore: sync YYYY-MM-DD HH:mm`), pull --rebase, push. Summary table at the end: ✓ synced / ↑ pushed n / ✗ conflict (with instructions). Never force-pushes. |289| `spbgit open [name]` | Opens `https://git.spboucher.ai/<name>` (or home) in browser (`open`/`xdg-open`/`start`). |290| `spbgit info <name>` | Server-side details: size, branches, commit count, topics, clone URL. |291| `spbgit rm <name> --confirm` | Soft-delete on server (types the repo name to confirm, GitHub-style). |292| `spbgit doctor` | Diagnoses: config present, token valid (API ping), server reachable, git installed, workspace exists. |293294### 6.3 CLI UX rules295296- Colorized, aligned output; respects `NO_COLOR` and `--json` everywhere.297- Exit codes: 0 ok, 1 user error, 2 network/auth, 3 partial failure in `--all` operations.298- All multi-repo operations run with a concurrency limit of 4 and stream per-repo progress lines.299- Helpful errors: on 401 → "Token invalid or revoked. Run `spbgit token new` then `spbgit init`."300301---302303## 7. JSON API (`/api/v1`)304305Public reads, token-gated writes (`Authorization: Bearer <PAT>`).306307- `GET /api/v1/repos` · `GET /api/v1/repos/:name` · `GET /api/v1/repos/:name/languages` · `GET /api/v1/repos/:name/commits?ref&page`308- `POST /api/v1/repos` · `PATCH /api/v1/repos/:name` · `DELETE /api/v1/repos/:name` (auth)309- `POST /api/v1/tokens` · `GET /api/v1/tokens` · `DELETE /api/v1/tokens/:id` (auth; creation of the *first* token happens server-side via `deploy/setup` script printing a bootstrap token once)310- `GET /api/v1/stats` — totals for the home hero.311- Rate limit public endpoints (200 req/min/IP, `@fastify/rate-limit`), sane JSON error envelope `{ "error": { "code", "message" } }`.312313---314315## 8. Deployment — node m3u96a + ngrok + `git.spboucher.ai`316317### 8.1 `deploy/setup-m3u96a.sh` (idempotent)3183191. Install Node 20 (via nvm or distro), git, ngrok.3202. Create dirs: `/srv/git`, `/srv/spbgit/{data,cache}`, correct ownership.3213. Clone/pull this repo to `/srv/spbgit/app`, `npm ci --omit=dev`.3224. Generate bootstrap PAT, print it **once** to stdout.3235. Install pm2 config (or systemd unit) and start.324325### 8.2 ngrok (`deploy/ngrok.yml`)326327```yaml328version: 3329agent:330 authtoken: ${NGROK_AUTHTOKEN}331endpoints:332 - name: spbgit333 url: https://git.spboucher.ai334 upstream:335 url: http://127.0.0.1:7420336```337- Document the DNS step: CNAME `git.spboucher.ai` → the ngrok-provided edge target, plus domain registration in the ngrok dashboard.338- ngrok provides TLS termination; the app still sets `trustProxy: true` and derives client IPs from `x-forwarded-for` for rate limiting.339340### 8.3 Process management (`deploy/ecosystem.config.cjs`)341342- Two pm2 apps: `spbgit-server` (`node src/server.mjs`) and `spbgit-tunnel` (`ngrok start --config deploy/ngrok.yml spbgit`), both `autorestart: true`, `max_restarts` sane, logs to `/srv/spbgit/logs/`.343- `pm2 save` + `pm2 startup` documented so everything survives reboot of m3u96a.344- Provide the systemd unit as the documented alternative.345346### 8.4 Backups & ops347348- `deploy/backup.sh` — nightly cron: `tar.zst` of `/srv/git` + `data/` into `/srv/spbgit/backups/`, keep 14, log result.349- `GET /healthz` (uptime, repo count, cache size) for monitoring; pm2 healthcheck hits it.350- Structured logs (pino), request logging with latency, log rotation.351352---353354## 9. Security Checklist (verify each before "done")355356- [ ] Path traversal impossible in repo names, tree paths, raw endpoint (normalize + verify prefix).357- [ ] Push requires valid PAT; tokens argon2-hashed at rest; constant-time compare.358- [ ] Rendered Markdown sanitized (no script/style/iframe/event-handlers) while keeping badges, `<details>`, alignment HTML working.359- [ ] Raw endpoint serves with `Content-Type` sniffing disabled (`X-Content-Type-Options: nosniff`) and correct types; HTML files served as `text/plain` from `/raw` to prevent stored-XSS-via-repo.360- [ ] CSP header on web UI (self + data: images + shields.io/img allowlist for remote images in READMEs — use a relaxed `img-src *` since READMEs embed arbitrary badges, but everything else locked down).361- [ ] Localhost-only guard on `/internal/*` hook routes.362- [ ] No secrets in code, logs, or error messages. `.env` + zod-validated config.363364---365366## 10. Quality Bar & Definition of Done367368**Engineering standards:** ESM, small modules, JSDoc on exported functions, vitest coverage on the git plumbing (tree/log/blame parsing) and an end-to-end test that spins the server, creates a repo via API, pushes via real `git` CLI, and asserts the web UI + API reflect it. `npm run lint` (eslint) + `npm run check:headers` both green.369370**The platform is DONE when every box below is checked:**371372- [ ] `bash deploy/setup-m3u96a.sh` on a fresh m3u96a gets the whole stack live at `https://git.spboucher.ai` and survives a reboot.373- [ ] `git clone https://git.spboucher.ai/anything.git` works for a stranger with no auth; pushing without a PAT is rejected with 401.374- [ ] `spbgit init` → `spbgit create demo --push` → repo instantly visible on the home page with correct language stats.375- [ ] `spbgit sync` walks every workspace repo and prints an accurate summary table.376- [ ] A README containing a badge row, a centered HTML header, a mermaid diagram, a task list, relative images, and code blocks renders flawlessly in dark **and** light theme.377- [ ] Repo pages have working file browser, blob view with line anchors, commits, single-commit diff, blame, branches, tags, ZIP download.378- [ ] Home page shows pinned repos, activity feed, and the contribution heatmap.379- [ ] Every generated file in the codebase carries the **Simon-Pierre Boucher / contact@spboucher.ai** header (`npm run check:headers` passes).380- [ ] OG image cards, sitemap, Atom feed, custom 404 — all present.381- [ ] Lighthouse ≥ 95 / 95 on home and a repo page.382383---384385## 11. Build Order (follow this sequence)3863871. **Skeleton**: config, Fastify bootstrap, logging, healthz, header scripts.3882. **Git core**: bare-repo model + Smart HTTP (clone/push round-trip test passes) + hooks + PAT auth.3893. **API v1** + `meta.json` model.3904. **CLI** (init → create → push → sync working end-to-end against local server).3915. **Web UI**: design tokens → layout → home → repo page → README pipeline (badges!) → blob/commits/diff/blame → search/stats/heatmap.3926. **Polish**: OG images, feeds, error pages, a11y pass, Lighthouse.3937. **Deploy**: scripts, ngrok, pm2, backups; go live on m3u96a; run the full DoD checklist.394395Work in small commits with conventional-commit messages (`feat:`, `fix:`, `chore:`). At the end of each phase, state which DoD boxes are now satisfied.396