SPB Git

spb/spbgit Public MIT

SPB Git — the platform hosting itself

JavaScript 73.9% CSS 11.7% Nunjucks 11.6% Shell 2.7%
25.3 KB

# CLAUDE.mdSPB Git · Personal Git Platform for Simon-Pierre Boucher

Read this entire document before writing a single line of code. 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.


# 0. Identity & Non-Negotiables

Key Value
Product name SPB Git
Owner / sole user Simon-Pierre Boucher
Contact contact@spboucher.ai
Public domain https://git.spboucher.ai (ngrok custom domain)
Deployment host Node m3u96a
Runtime Node.js ≥ 20, ESM only
Visibility model ALL repositories are PUBLIC (read-only). Write access = owner only. No sign-up, no visitor accounts, ever.

# 0.1 THE GOLDEN RULE — Mandatory Author Header

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.

Canonical template (adapt comment syntax per language):

js
/**
 * ─────────────────────────────────────────────
 *  SPB Git — Personal Git Platform
 * ─────────────────────────────────────────────
 *  Author  : Simon-Pierre Boucher
 *  Contact : contact@spboucher.ai
 *  File    : <relative/path/filename.ext>
 *  Purpose : <one-line description>
 *  License : MIT © Simon-Pierre Boucher
 * ─────────────────────────────────────────────
 */

Per-language syntax:

Language Syntax
JS / TS / CSS / SCSS /** ... */ block
Python / Bash / YAML / Dockerfile / TOML # lines
HTML / Vue / Markdown (when appropriate) <!-- ... -->
SQL -- lines
EJS / Nunjucks templates <%# ... %> / {# ... #}

Additionally, 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.


# 1. Product Vision

SPB Git is not a toy. It is a polished, self-hosted software forge — the personal equivalent of GitHub — that serves three purposes:

  1. Canonical home for all of Simon-Pierre's repositories, clonable by anyone at https://git.spboucher.ai/<repo>.git.
  2. 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.
  3. Frictionless daily driver: a powerful CLI (spbgit) that manages all repos from the terminal — create, commit, push, sync, open — in single commands.

The bar for quality is: "If a stranger lands on git.spboucher.ai, they should assume a small team built this."


# 2. High-Level Architecture

text
┌────────────────────────────  node m3u96a  ────────────────────────────┐
│                                                                       │
│  ┌─────────────┐   ┌───────────────────────────────────────────────┐  │
│  │   ngrok      │   │              SPB Git Server (Node 20)         │  │
│  │  tunnel      │──▶│  Fastify app :7420                            │  │
│  │ git.spboucher│   │  ├─ /            Web UI (SSR, Nunjucks)       │  │
│  │    .ai       │   │  ├─ /api/v1/*    JSON API                     │  │
│  └─────────────┘   │  ├─ /:repo.git/* Git Smart HTTP (backend)     │  │
│                    │  ├─ /raw/*        Raw file serving             │  │
│                    │  └─ /archive/*    zip/tar.gz snapshots         │  │
│                    └───────────────┬───────────────────────────────┘  │
│                                    │                                  │
│         ┌──────────────────────────┼───────────────────────────┐      │
│         │                          │                           │      │
│  ┌──────▼──────┐  ┌────────────────▼─────────┐  ┌──────────────▼───┐  │
│  │ /srv/git/   │  │ /srv/spbgit/data/        │  │ /srv/spbgit/     │  │
│  │ *.git       │  │ meta.json (repo index)   │  │ cache/           │  │
│  │ bare repos  │  │ tokens, hooks, stats     │  │ rendered HTML,   │  │
│  │ (source of  │  │                          │  │ lang stats, zips │  │
│  │  truth)     │  │                          │  │                  │  │
│  └─────────────┘  └──────────────────────────┘  └──────────────────┘  │
└───────────────────────────────────────────────────────────────────────┘

Principles:

  • 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.
  • 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.
  • SSR-first web UI. Server-rendered pages (Nunjucks templates + a small vanilla JS layer). No SPA framework. Fast, crawlable, sharp.

# 2.1 Repository Layout of this project

text
spbgit/
├── CLAUDE.md                  # this file
├── README.md                  # project readme (with badges, of course)
├── package.json
├── src/
│   ├── server.mjs             # Fastify bootstrap
│   ├── config.mjs             # env + config loading (zod-validated)
│   ├── git/
│   │   ├── smart-http.mjs     # upload-pack / receive-pack over HTTP
│   │   ├── repo.mjs           # repo model: branches, tree, blob, log, blame
│   │   ├── hooks.mjs          # post-receive hook installer + handlers
│   │   └── archive.mjs        # zip / tar.gz snapshot generation
│   ├── auth/
│   │   └── token.mjs          # owner PAT verification (argon2-hashed)
│   ├── render/
│   │   ├── markdown.mjs       # GFM pipeline (markdown-it + plugins)
│   │   ├── highlight.mjs      # code highlighting (shiki)
│   │   └── og-image.mjs       # dynamic Open Graph card per repo (satori/resvg)
│   ├── api/
│   │   └── v1.mjs             # JSON API routes
│   ├── web/
│   │   ├── routes.mjs         # HTML routes
│   │   ├── views/             # Nunjucks templates
│   │   └── assets/            # css (hand-written, design tokens), js, fonts, logo
│   └── stats/
│       ├── languages.mjs      # linguist-style language detection + percentages
│       └── activity.mjs       # commit calendar / punch-card data
├── cli/
│   ├── spbgit.mjs             # entry point (bin)
│   └── commands/              # one file per command
├── deploy/
│   ├── ngrok.yml
│   ├── ecosystem.config.cjs   # pm2: server + ngrok, autorestart
│   ├── spbgit.service         # systemd alternative
│   ├── setup-m3u96a.sh        # idempotent server bootstrap script
│   └── backup.sh              # nightly tar of /srv/git + meta.json
├── scripts/
│   ├── inject-headers.mjs
│   └── check-headers.mjs
└── test/                      # vitest: unit + integration (smart HTTP round-trip)

# 3. Git Hosting Engine

# 3.1 Smart HTTP protocol

  • 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).
  • Routes:
    • GET /:repo.git/info/refs?service=git-upload-pack → public (clone/fetch/pull).
    • POST /:repo.git/git-upload-pack → public.
    • GET /:repo.git/info/refs?service=git-receive-packauth required.
    • POST /:repo.git/git-receive-packauth required (push).
  • 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.
  • Set correct Content-Type, chunked encoding, gzip request body support (Content-Encoding: gzip from git clients), and side-band progress passthrough.
  • Repo names: ^[a-z0-9][a-z0-9._-]{0,63}$. Reject path traversal hard.

# 3.2 Post-receive hooks (installed automatically in every bare repo)

On every push, the hook POSTs to http://127.0.0.1:7420/internal/hooks/post-receive (localhost-only route) which:

  1. Busts all caches for that repo.
  2. Recomputes language stats + default-branch README render (warm cache).
  3. Appends an event to the activity feed (data/activity.jsonl).
  4. Regenerates the repo's OG image.

# 3.3 Repo lifecycle

  • POST /api/v1/repos (auth) → git init --bare, install hooks, write meta.json entry, set HEAD to refs/heads/main.
  • PATCH /api/v1/repos/:name (auth) → description, topics (max 10), homepage URL, pinned flag, default branch.
  • DELETE /api/v1/repos/:name (auth) → moves repo to /srv/git/.trash/<name>-<timestamp>.git (soft delete, recoverable for 30 days).
  • Everything is public: there is no private field anywhere in the codebase. Don't build one.

# 4. Web UI — "It must look sharp" (Design Spec)

# 4.1 Design system

  • Aesthetic: modern developer-forge. Dark theme by default, light theme toggle (persisted in localStorage, respects prefers-color-scheme on first visit).
  • Design tokens (CSS custom properties in assets/css/tokens.css):
    • Dark: background #0b0e14, surface #11151c, border #1f2530, text #e6e9ef, muted #8b93a3, accent #4f8cff, accent-2 #22d3aa, danger #ff5d5d.
    • Radius 10px, subtle 1px borders, no heavy shadows; hover states elevate with border-color shifts, not box-shadows.
  • Typography: Inter (UI) + JetBrains Mono (code), self-hosted woff2 in assets/fonts/ — no third-party font CDNs.
  • Layout: max-width 1200px, sticky top nav: logo "SPB Git" (monogram SVG), global search input (/ focuses it), theme toggle, link to spboucher.ai.
  • Fully responsive down to 360px. Code views scroll horizontally, never break the page.
  • Footer on every page: © Simon-Pierre Boucher · contact@spboucher.ai · Powered by SPB Git.

# 4.2 Pages

/ — Home / repo index

  • Hero strip: avatar/monogram, "Simon-Pierre Boucher", tagline, contact link, count badges (X repos · Y commits · Z languages).
  • Pinned repos grid (up to 6 cards) then full repo list.
  • 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.
  • Controls: search-as-you-type filter, sort by Recently pushed / Name / Created, filter by language and topic.
  • Activity feed sidebar: last 15 pushes ("pushed 3 commits to api-tools · 2 h ago").
  • Commit calendar (GitHub-style contribution heatmap, last 52 weeks, SVG, tooltip per day).

/:repo — Repo home

  • Header: repo name (breadcrumb spb / name), description, topic chips, homepage link, language bar (stacked percentage bar like GitHub, with legend).
  • Clone box: HTTPS URL with copy button + spbgit clone <name> snippet + "Download ZIP".
  • Meta row: default branch selector, branch/tag counts, commit count, repo size, license badge (auto-detected from LICENSE).
  • 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).
  • README.md rendered below the file table. See §5 — this must be pixel-perfect.

/:repo/tree/:ref/*path — directory browsing (same file table pattern).

/:repo/blob/:ref/*path — file view:

  • Sticky header: path breadcrumb, size, LOC, buttons: Raw, Blame, History, Copy permalink (permalink pins the commit SHA).
  • Shiki-highlighted code with line numbers; clickable line anchors (#L42, ranges #L10-L20 with highlight).
  • 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).
  • Binary files: show size + download button; images/SVG preview inline.

/: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.

/: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.

/:repo/blame/:ref/*path — blame view with commit hunks grouped and color-aged (newer = brighter accent).

/:repo/branches, /:repo/tags — lists with ahead/behind counts vs default branch (branches) and archive download links (tags → releases-lite).

/search?q= — global search across repo names, descriptions, topics, and README content (build a tiny inverted index in cache; refresh on push). Grouped results.

Error pages — custom 404/500 with the design system (404: "This ref does not exist in any timeline.").

# 4.3 Meta / polish

  • 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).
  • sitemap.xml + robots.txt + RSS/Atom feed of activity at /feed.atom.
  • Favicon set (SVG + PNG) with the SPB monogram.
  • Lighthouse targets: Performance ≥ 95, A11y ≥ 95. No render-blocking third-party requests at all.

# 5. README & Markdown Rendering — Badge-Perfect (Critical)

This is a flagship feature. The rendered README must be indistinguishable from GitHub's rendering quality.

Pipeline (src/render/markdown.mjs), built on markdown-it +:

  • GFM tables, task lists (rendered checkboxes, disabled), strikethrough, autolinks, footnotes.
  • Heading anchors with hover link icon; auto-generated slug IDs (GitHub algorithm) so #installation links work.
  • 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).
  • Badges & images:
    • 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.
    • 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.
    • <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).
  • HTML in Markdown (centered headers, badge tables — the classic GitHub README style) must render correctly.
  • Emoji shortcodes (:rocket: → 🚀).
  • Look for README.md case-insensitively, then readme.rst/README.txt fallbacks (plain render).
  • Cache the rendered HTML per (repo, sha, theme-agnostic).

Typography 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.


# 6. spbgit CLI — Terminal Command Center

A 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.

# 6.1 Configuration

~/.spbgit/config.json:

json
{
  "server": "https://git.spboucher.ai",
  "token": "<PAT>",
  "workspace": "~/code",
  "author": { "name": "Simon-Pierre Boucher", "email": "contact@spboucher.ai" }
}
  • spbgit init — interactive setup wizard (server URL, token paste, workspace dir); sets git config user.name/email globally if missing; chmod 600 the config.
  • Token is never printed back, never logged, never in argv.

# 6.2 Commands (full spec)

Command Behavior
spbgit init Setup wizard (above).
spbgit token new [--label "laptop"] / token list / token revoke <id> Manage PATs via API.
spbgit list (ls) Table of server repos: name, description, language, last push, clone-URL. --json for scripting.
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.
spbgit clone <name> [dir] Clone into workspace (or dir).
spbgit clone --all Clone every server repo missing from the workspace.
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.
spbgit commit <name|--all> -m "msg" git add -A && git commit in target repo(s). Skips clean repos with a note.
spbgit push [name|--all] Push current branch; --all iterates every workspace repo with commits ahead.
spbgit pull [name|--all] Pull with --ff-only by default; --rebase flag.
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.
spbgit open [name] Opens https://git.spboucher.ai/<name> (or home) in browser (open/xdg-open/start).
spbgit info <name> Server-side details: size, branches, commit count, topics, clone URL.
spbgit rm <name> --confirm Soft-delete on server (types the repo name to confirm, GitHub-style).
spbgit doctor Diagnoses: config present, token valid (API ping), server reachable, git installed, workspace exists.

# 6.3 CLI UX rules

  • Colorized, aligned output; respects NO_COLOR and --json everywhere.
  • Exit codes: 0 ok, 1 user error, 2 network/auth, 3 partial failure in --all operations.
  • All multi-repo operations run with a concurrency limit of 4 and stream per-repo progress lines.
  • Helpful errors: on 401 → "Token invalid or revoked. Run spbgit token new then spbgit init."

# 7. JSON API (/api/v1)

Public reads, token-gated writes (Authorization: Bearer <PAT>).

  • GET /api/v1/repos · GET /api/v1/repos/:name · GET /api/v1/repos/:name/languages · GET /api/v1/repos/:name/commits?ref&page
  • POST /api/v1/repos · PATCH /api/v1/repos/:name · DELETE /api/v1/repos/:name (auth)
  • 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)
  • GET /api/v1/stats — totals for the home hero.
  • Rate limit public endpoints (200 req/min/IP, @fastify/rate-limit), sane JSON error envelope { "error": { "code", "message" } }.

# 8. Deployment — node m3u96a + ngrok + git.spboucher.ai

# 8.1 deploy/setup-m3u96a.sh (idempotent)

  1. Install Node 20 (via nvm or distro), git, ngrok.
  2. Create dirs: /srv/git, /srv/spbgit/{data,cache}, correct ownership.
  3. Clone/pull this repo to /srv/spbgit/app, npm ci --omit=dev.
  4. Generate bootstrap PAT, print it once to stdout.
  5. Install pm2 config (or systemd unit) and start.

# 8.2 ngrok (deploy/ngrok.yml)

yaml
version: 3
agent:
  authtoken: ${NGROK_AUTHTOKEN}
endpoints:
  - name: spbgit
    url: https://git.spboucher.ai
    upstream:
      url: http://127.0.0.1:7420
  • Document the DNS step: CNAME git.spboucher.ai → the ngrok-provided edge target, plus domain registration in the ngrok dashboard.
  • ngrok provides TLS termination; the app still sets trustProxy: true and derives client IPs from x-forwarded-for for rate limiting.

# 8.3 Process management (deploy/ecosystem.config.cjs)

  • 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/.
  • pm2 save + pm2 startup documented so everything survives reboot of m3u96a.
  • Provide the systemd unit as the documented alternative.

# 8.4 Backups & ops

  • deploy/backup.sh — nightly cron: tar.zst of /srv/git + data/ into /srv/spbgit/backups/, keep 14, log result.
  • GET /healthz (uptime, repo count, cache size) for monitoring; pm2 healthcheck hits it.
  • Structured logs (pino), request logging with latency, log rotation.

# 9. Security Checklist (verify each before "done")

  • Path traversal impossible in repo names, tree paths, raw endpoint (normalize + verify prefix).
  • Push requires valid PAT; tokens argon2-hashed at rest; constant-time compare.
  • Rendered Markdown sanitized (no script/style/iframe/event-handlers) while keeping badges, <details>, alignment HTML working.
  • 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.
  • 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).
  • Localhost-only guard on /internal/* hook routes.
  • No secrets in code, logs, or error messages. .env + zod-validated config.

# 10. Quality Bar & Definition of Done

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.

The platform is DONE when every box below is checked:

  • bash deploy/setup-m3u96a.sh on a fresh m3u96a gets the whole stack live at https://git.spboucher.ai and survives a reboot.
  • git clone https://git.spboucher.ai/anything.git works for a stranger with no auth; pushing without a PAT is rejected with 401.
  • spbgit initspbgit create demo --push → repo instantly visible on the home page with correct language stats.
  • spbgit sync walks every workspace repo and prints an accurate summary table.
  • 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.
  • Repo pages have working file browser, blob view with line anchors, commits, single-commit diff, blame, branches, tags, ZIP download.
  • Home page shows pinned repos, activity feed, and the contribution heatmap.
  • Every generated file in the codebase carries the Simon-Pierre Boucher / contact@spboucher.ai header (npm run check:headers passes).
  • OG image cards, sitemap, Atom feed, custom 404 — all present.
  • Lighthouse ≥ 95 / 95 on home and a repo page.

# 11. Build Order (follow this sequence)

  1. Skeleton: config, Fastify bootstrap, logging, healthz, header scripts.
  2. Git core: bare-repo model + Smart HTTP (clone/push round-trip test passes) + hooks + PAT auth.
  3. API v1 + meta.json model.
  4. CLI (init → create → push → sync working end-to-end against local server).
  5. Web UI: design tokens → layout → home → repo page → README pipeline (badges!) → blob/commits/diff/blame → search/stats/heatmap.
  6. Polish: OG images, feeds, error pages, a11y pass, Lighthouse.
  7. Deploy: scripts, ngrok, pm2, backups; go live on m3u96a; run the full DoD checklist.

Work in small commits with conventional-commit messages (feat:, fix:, chore:). At the end of each phase, state which DoD boxes are now satisfied.