# SPB Git **The personal git platform of Simon-Pierre Boucher** [![node](https://img.shields.io/badge/node-%E2%89%A520-brightgreen)](https://nodejs.org) [![license](https://img.shields.io/badge/license-MIT-blue)](LICENSE) [![runtime](https://img.shields.io/badge/runtime-Fastify%205-black)](https://fastify.dev) [![deploy](https://img.shields.io/badge/host-m3u96a-4f8cff)](https://git.spboucher.ai) Self-hosted software forge · every repo public & clonable · owner-only writes `https://git.spboucher.ai`
--- ## What it is SPB Git is a complete, self-hosted GitHub-equivalent built for one person: - **Git hosting** — Smart HTTP v2 (streamed `upload-pack`/`receive-pack`), anonymous clone/fetch, PAT-authenticated push, post-receive hooks, zip/tar.gz snapshots. - **Showcase web UI** — SSR (Nunjucks), dark/light themes, pinned repos, activity feed, contribution heatmap, GitHub-grade README rendering (badges, mermaid, task lists, relative images), shiki-highlighted file browsing with line anchors, commits, diffs, blame, branches, tags, global search, OG cards, Atom feed, sitemap. - **`spbgit` CLI** — the terminal command center: `init`, `token`, `list`, `create --push`, `clone --all`, `status`, `commit`, `push`, `pull`, `sync`, `open`, `info`, `rm`, `doctor`, `release`. - **Releases** — GitHub-style binary assets (dmg, pkg, zip…) attached to tags, streamed to disk with sha256 sidecars, public downloads at `/releases///`: ```bash git tag v1.0.0 && git push --tags spbgit release upload myapp v1.0.0 MyApp.dmg # → https://git.spboucher.ai/releases/myapp/v1.0.0/MyApp.dmg spbgit release list myapp ``` - **JSON API** — public reads, token-gated writes at `/api/v1/*`. **Filesystem is the database.** Bare repos under the git root are the source of truth; `meta.json` holds descriptions/topics/pins; everything expensive is cached per `@` and busted on push. ## Quick start (development) ```bash npm install # also fetches self-hosted fonts cp .env.example .env # point the paths at ./dev/* for local work npm run dev # http://127.0.0.1:7420 ``` Mint a first token, then wire up the CLI: ```bash node --input-type=module -e " import { loadConfig, ensureDirs } from './src/config.mjs'; import { TokenStore } from './src/auth/token.mjs'; const c = loadConfig(); ensureDirs(c); console.log((await new TokenStore(c.dataDir).create('bootstrap')).token); " npm link # exposes `spbgit` spbgit init # paste server URL + token spbgit create hello --push # first repo, live immediately ``` ## Deployment (node m3u96a) ```bash # on m3u96a, from the app directory bash deploy/setup-m3u96a.sh ``` The script is idempotent: checks node ≥ 20 / git / ngrok / pm2, creates the data prefix (`/srv` on Linux, `~/srv` on macOS), installs production deps, writes `.env`, prints the **bootstrap PAT once**, and starts both pm2 apps (`spbgit-server`, `spbgit-tunnel`). Run `pm2 startup` once so everything survives a reboot. DNS: CNAME `git.spboucher.ai` to the ngrok edge target (see `deploy/ngrok.yml`). Nightly backups: cron `deploy/backup.sh` (keeps 14). A systemd unit is provided as the documented alternative: `deploy/spbgit.service`. ## Scripts | Command | Purpose | |---|---| | `npm start` / `npm run dev` | run the server (dev = watch + template reload) | | `npm test` | vitest — unit + full clone/push round-trip e2e | | `npm run lint` | eslint | | `npm run check:headers` | fail if any file lacks the author header | | `npm run inject:headers` | add missing headers in bulk | | `npm run fetch:fonts` | (re)download self-hosted Inter / JetBrains Mono | ## Security model - All repositories are **public read-only**; there is no private flag anywhere. - Writes require a PAT (`spbgit__`), stored argon2id-hashed, revocable, with last-used tracking. - Repo names match `^[a-z0-9][a-z0-9._-]{0,63}$`; tree paths are traversal-checked twice (validation + prefix check). - Rendered Markdown is sanitized with a strict allowlist (badges, `
`, ``, align HTML survive; scripts, iframes, event handlers never do). - `/raw` serves with `nosniff` + sandbox CSP, and repo HTML is served as `text/plain`. - `/internal/*` hook routes accept loopback connections only. ## License MIT © [Simon-Pierre Boucher](mailto:contact@spboucher.ai)