diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..0df9b1e --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,73 @@ +name: Pages + +on: + push: + branches: [main, brand] + paths: + - "site/**" + - "brand/**" + - "tools/build-docs.mjs" + - "tools/docs-build-mbt/**" + - ".github/workflows/pages.yml" + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 24 + + - name: Install MoonBit toolchain + run: | + curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash + echo "$HOME/.moon/bin" >> "$GITHUB_PATH" + + - name: Build MoonBit docs renderer + working-directory: tools/docs-build-mbt + run: | + moon update + moon build --target js --release + + - name: Refresh brand assets in site + run: | + mkdir -p site/assets/img + cp brand/tokens/tokens.css site/assets/tokens.css + cp brand/logo/*.svg site/assets/img/ + touch site/.nojekyll + + - name: Render markdown to HTML + run: node tools/build-docs.mjs + + - name: Configure Pages + uses: actions/configure-pages@v5 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./site + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/brand/README.md b/brand/README.md new file mode 100644 index 0000000..9496560 --- /dev/null +++ b/brand/README.md @@ -0,0 +1,191 @@ +# bit — Brand + +A visual identity for **bit**, a distributed Git implementation in MoonBit. + +The brand exists to make three ideas legible at a glance: + +> **a node, a source, a single bit of communication.** + +--- + +## Concept + +bit is distributed. Source is replicated. Peers communicate. The brand makes +this physical — a triad of geometric primitives, a pink node that always +represents *source*, an off-cream paper, an ink that is not quite black, and a +deliberately uncomfortable acid green held in reserve. + +Three rules that never bend: + +1. **No gradients.** Color is asserted, never blended. +2. **No shadows.** Depth comes from layout, not light. +3. **Geometric primitives only.** Circles, rectangles, lines. The wordmark is + constructed from the same primitives as the logo — they are literally + the same alphabet. + +--- + +## Logo + +| Asset | File | Use | +| --- | --- | --- | +| Logo | [`logo/logo.svg`](logo/logo.svg) | App icon, avatar, square placements | +| Logo, inverse | [`logo/logo-inverse.svg`](logo/logo-inverse.svg) | Dark surfaces | +| Logo, mono | [`logo/logo-mono.svg`](logo/logo-mono.svg) | Single-channel printing, embossing | +| Wordmark | [`logo/wordmark.svg`](logo/wordmark.svg) | In-line product name | +| Wordmark, inverse | [`logo/wordmark-inverse.svg`](logo/wordmark-inverse.svg) | Dark surfaces | +| Lockup, horizontal | [`logo/lockup-horizontal.svg`](logo/lockup-horizontal.svg) | Header, README hero | +| Lockup, stacked | [`logo/lockup-stack.svg`](logo/lockup-stack.svg) | Square hero, cover art | + +**Logo composition** — three nodes form an asymmetric triad: + +- **pink filled** — *source*. The origin of truth. +- **ink outlined** — *peer*. A replica that has not converged. +- **ink filled** — *peer*. A replica that has. + +Edges are thin. The triangle is intentionally non-equilateral. Symmetry is the +enemy of *distributed*. + +**Wordmark construction** — `b`, `i`, `t` are rectangles and circles. The +tittle of the **i** is the pink dot. This is the brand's signature: a single +node, a single source, a single bit. Use it as a reductive brand element on +its own where the full lockup would be too loud. + +### Clear space + +Minimum margin around any logo asset is **one source-node diameter** on every +side. The logo must never be cropped by a frame closer than this. When in +doubt, give it more room. + +### Minimum size + +- Logo: **24×24 px** on screen, **8 mm** in print. +- Wordmark: **80 px** wide on screen. +- Lockup: **160 px** wide on screen. + +### Misuse + +Do not stretch. Do not recolor (pink stays pink, ink stays ink). Do not add +strokes to the source node. Do not add a drop shadow. Do not place over a +photograph without a paper or ink rectangle behind it. Do not rotate beyond +the supplied compositions. + +--- + +## Color + +``` +Bit Pink #FF2D6F Primary. Source, accent, the single bit. +Bit Ink #0E0E12 Foreground. Off-black with a cool tint. +Bit Paper #F2EFE7 Background. Warm cream. +Bit Stone #D9D5CB UI lines, dividers, secondary tracks. +Bit Acid #D6FF3D Eccentric counterpoint. Use SPARINGLY. +``` + +Press / hover states: + +``` +Pink Deep #C7124F Pressed pink only. Never as a primary surface. +Pink Tint #FFD9E6 Chip/tag fill on paper. +``` + +**Rules.** Pink is a single state — owned by *source*, primary actions, and +the i-tittle. The acid green is permitted on one element per view and never +inside the logo. Ink is not `#000` and paper is not `#FFF` — the warmth in +paper and the coolness in ink are deliberate. + +Full token sources: [`tokens/tokens.css`](tokens/tokens.css) · +[`tokens/tokens.json`](tokens/tokens.json) (W3C draft format). + +--- + +## Typography + +| Role | Family | Loaded from | +| --- | --- | --- | +| Sans (display + body) | **Space Grotesk** — 300 / 400 / 500 / 600 / 700 | Google Fonts | +| Mono (machine voice, code) | **Space Mono** — 400 / 700 | Google Fonts | + +**No italic. Anywhere.** Italic is forbidden in the brand — not in display, +not in body, not in code. Emphasis is carried by weight, color, and tracking. + +### Scale + +| Token | px | Use | +| --- | --: | --- | +| `--type-001` | 11 | micro label, system meta | +| `--type-00` | 13 | caption | +| `--type-0` | 15 | small body | +| `--type-1` | 17 | body | +| `--type-2` | 21 | lead paragraph | +| `--type-3` | 28 | h4 | +| `--type-4` | 40 | h3 | +| `--type-5` | 60 | h2 | +| `--type-6` | 96 | h1 | +| `--type-7` | 148 | hero, poster | + +### Tracking + +- Display (≥ 60 px): **−0.035em** — tight, near-touching. +- Headings (28–60 px): **−0.018em**. +- Body: **0**. +- Labels (UPPERCASE, ≤ 13 px): **+0.16em**. + +### Use it + +- **Display headlines** in Space Grotesk **700** at `--type-5` or larger, tight + tracking, leading `0.95`–`1.08`. Let one phrase fill a column. Generous + whitespace below. +- **Body** in Space Grotesk **400** at `--type-1`, leading `1.55`. +- **Labels & meta** in Space Mono **400** UPPERCASE, `--type-001`, + `tracking +0.16em` — used for `// COMMIT`, `NODE.0142`, kicker text, and + data captions. This is where the eccentric mode-design voice lives. +- **Code** in Space Mono **400**, no italic. Comments in `--bit-ink-mute`. + +Never pair these with Inter, Roboto, Helvetica, or system defaults. If a +target environment can only render system fonts, fall back to the platform +sans — never substitute a "near miss" web font. + +--- + +## Layout principles + +- **Asymmetry over symmetry.** Compositions should feel like the triad logo — + weighted, intentional, never centered for the sake of it. +- **Rules, not boxes.** Use a 1 px ink rule to divide content. Cards with + rounded corners are forbidden; use rectangular ink-stroked panels with + `radius: 0` and stroke `1.5px`. +- **Mono labels as kicker text.** A small Space Mono UPPERCASE label sits + above every editorial heading, e.g. `// 003 — COMMUNICATION`. +- **One pink per view.** The primary action, or the source node, or the + i-tittle in the wordmark — pick one anchor, not three. +- **Acid is a guest.** One acid element per page maximum. Often zero. + +--- + +## Files + +``` +brand/ +├── README.md ← you are here +├── tokens/ +│ ├── tokens.css ← CSS custom properties + @import for fonts +│ └── tokens.json ← W3C-draft design tokens +├── logo/ +│ ├── logo.svg +│ ├── logo-inverse.svg +│ ├── logo-mono.svg +│ ├── wordmark.svg +│ ├── wordmark-inverse.svg +│ ├── lockup-horizontal.svg +│ └── lockup-stack.svg +├── preview.html ← visual showcase of the system +└── docs/ + └── docs-template.html ← documentation page applying the brand +``` + +--- + +**License.** Brand assets are released under the project's Apache-2.0 license. +The MoonBit name and pink palette are referenced as cultural lineage, not +ownership. diff --git a/brand/docs/docs-template.html b/brand/docs/docs-template.html new file mode 100644 index 0000000..fd82234 --- /dev/null +++ b/brand/docs/docs-template.html @@ -0,0 +1,387 @@ + + + + + +bit — docs · brand applied + + + + + + +
+
+ bit + docs · v0.1 +
+ +
+ + +
+ + + + + +
+ +
+

// guide · 002 · concepts

+

A node, a source, a single bit.

+

bit is a distributed Git implementation. Every clone is a peer. Every commit is a bit of evidence. There is no central authority — only a graph of nodes converging through gossip.

+
+ +

Nodes

+

A node in bit is any process holding a copy of the repository. Your laptop, a CI runner, a relay host — all peers, no master. Each node exposes an address and a set of refs, and accepts handshakes from other peers it trusts.

+ +

Nodes are addressed by URI:

+ +
// peer addresses
+bit://aurora.local:9418/repo
+relay://bit.example.com/u/mizchi/bit
+https://github.com/mizchi/bit.git
+ +
+

// note

+

The pink dot in the bit logo represents the source node — the origin of a particular truth. In a distributed system, every node can be a source. The brand asserts this visually: only one node in the triad is pink at a time, but which one can rotate.

+
+ +

Source

+

Source is whoever wrote the commit you are looking at. bit records the source identity in the commit metadata and reconciles concurrent writes through a conflict-free history graph — not through a server vote.

+ +

Replication

+

When two peers handshake, they exchange ref tips and walk the object graph backwards until they meet a common ancestor. Bytes flow in both directions. This is gossip, not consensus.

+ + + + + + + + + + + +
ModeTriggerDirection
pushexplicitlocal → remote
fetchexplicitremote → local
gossipperiodic, on handshakeboth
relayvia relay hostboth, async
+ +

Communication

+

A single edge between two nodes is the smallest unit of meaning in the system — one bit of exchange. Communication in bit is content-addressed: peers ask for object hashes, not for filenames, so the protocol is naturally idempotent and deduplicating.

+ +
// initiate a peer session
+let peer = Node::from_uri("bit://aurora.local:9418/repo")
+peer.handshake(timeout=2.s)?
+let wants = local.refs().diff(peer.refs())
+peer.fetch(wants)
+ +
+

// caveat

+

bit is experimental. Data corruption can occur in worst-case scenarios. Always keep backups of important repositories. The acid green of this callout exists for one reason: you should not miss it.

+
+ +

What's next

+ + +
+ + + + +
+ + + + + diff --git a/brand/logo/lockup-horizontal.svg b/brand/logo/lockup-horizontal.svg new file mode 100644 index 0000000..c482891 --- /dev/null +++ b/brand/logo/lockup-horizontal.svg @@ -0,0 +1,28 @@ + + bit horizontal lockup + Logo and wordmark side by side. Optical alignment: source node baseline aligned to wordmark cap height. + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/brand/logo/lockup-stack.svg b/brand/logo/lockup-stack.svg new file mode 100644 index 0000000..4127f11 --- /dev/null +++ b/brand/logo/lockup-stack.svg @@ -0,0 +1,28 @@ + + bit stacked lockup + Logo above wordmark, both centered. For square placements: avatars, app icons, cover composition. + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/brand/logo/logo-inverse.svg b/brand/logo/logo-inverse.svg new file mode 100644 index 0000000..1205b9f --- /dev/null +++ b/brand/logo/logo-inverse.svg @@ -0,0 +1,12 @@ + + bit logo, inverse + Logomark for dark surfaces. Paper-colored peers, pink source unchanged. + + + + + + + + + diff --git a/brand/logo/logo-mono.svg b/brand/logo/logo-mono.svg new file mode 100644 index 0000000..d498733 --- /dev/null +++ b/brand/logo/logo-mono.svg @@ -0,0 +1,12 @@ + + bit logo, monochrome + Single-color variant for embossing, single-channel printing, and reductive contexts. + + + + + + + + + diff --git a/brand/logo/logo.svg b/brand/logo/logo.svg new file mode 100644 index 0000000..8f30f8c --- /dev/null +++ b/brand/logo/logo.svg @@ -0,0 +1,16 @@ + + bit logo + A triad of nodes — one pink source, two ink peers — connected by edges. Distributed communication. + + + + + + + + + + + + + diff --git a/brand/logo/wordmark-inverse.svg b/brand/logo/wordmark-inverse.svg new file mode 100644 index 0000000..a7bba21 --- /dev/null +++ b/brand/logo/wordmark-inverse.svg @@ -0,0 +1,13 @@ + + bit wordmark, inverse + Paper-colored letterforms for dark surfaces. Pink tittle unchanged. + + + + + + + + + + diff --git a/brand/logo/wordmark.svg b/brand/logo/wordmark.svg new file mode 100644 index 0000000..36208fc --- /dev/null +++ b/brand/logo/wordmark.svg @@ -0,0 +1,16 @@ + + bit wordmark + "bit" constructed from geometric primitives: rectangles and circles. The pink dot above the i is the brand signature — a single node, a single source, a single bit. + + + + + + + + + + + + + diff --git a/brand/preview.html b/brand/preview.html new file mode 100644 index 0000000..c6894ff --- /dev/null +++ b/brand/preview.html @@ -0,0 +1,556 @@ + + + + + +bit — brand system + + + + + + +
+
+ bit / brand + v0.1 — 2026 +
+ +

// distributed source control · MoonBit

+ +

A node.
A source.
A single bt.

+ +

bit is a distributed Git implementation. The brand is its physical form — geometric primitives, asserted color, the pink dot as the smallest unit of meaning.

+ +
+ bit lockup +
+
+ + +
+
+
// 001
+

Color is asserted, never blended.

+
+ +
+ +
+
+
+

Bit Pink

+
+ #FF2D6F
+ rgb 255 45 111
+ primary · source +
+
+
+ +
+
+
+

Bit Ink

+
+ #0E0E12
+ rgb 14 14 18
+ foreground · rule +
+
+
+ +
+
+
+

Bit Paper

+
+ #F2EFE7
+ rgb 242 239 231
+ background · cream +
+
+
+ +
+
+
+

Bit Acid

+
+ #D6FF3D
+ rgb 214 255 61
+ counterpoint · rare +
+
+
+ +
+
+ + +
+
+
// 002
+

Type carries the voice. Italic is forbidden.

+
+ +
+ +
+
Hero
148 / 700
+

distributed.

+
+ +
+
H1
96 / 700
+

peer to peer.

+
+ +
+
H2
60 / 700
+

A graph of nodes.

+
+ +
+
H3
40 / 700
+

Communication is the smallest unit.

+
+ +
+
Body
17 / 400
+

bit stores history as content-addressed objects, replicated across peers without a central authority. Every node is a source. Every commit is a bit of evidence. The graph converges through gossip, not consensus.

+
+ +
+
Label
11 / 500 / mono
+

// node.0142 — commit a8c2f6 — peer wrote 14 bytes

+
+ +
+
+ + +
+
+
// 003
+

Logo. Three nodes, one asymmetric triangle.

+
+ +
+
+
+

lockup · horizontal · on paper

+
+
+
+

lockup · inverse · on ink

+
+
+
+

lockup · on acid (rare)

+
+ +
+
+

mark · on paper

+
+
+
+

mark · inverse

+
+
+
+

wordmark · the pink dot is the i

+
+
+
+ + +
+
+
// 004
+

Components. Rectangles. No corners. No shadow.

+
+ +
+ +
+

// node.0142 — origin

+

aurora.local

+
refs14
+
last gossip2s ago
+
heada8c2f6
+
+ + +
+
+ +
+

// pr.0007 — open

+

Replace gossip backoff with token bucket

+
+ pink · primary + ghost · meta + acid · 1 per view +
+ +
// peer announcement
+let peer = Node::from_uri(uri)
+peer.handshake(timeout=2.s)?
+peer.send(Bit::source(commit))
+
+ +
+
+ + +
+
+

// 006 — manifesto

+

no center.
no master.
only peers.

+
+
+
system bit / 2026
+
kernel moonbit
+
license apache-2.0
+
+
+
+ +
+
+ + + + + diff --git a/brand/tokens/tokens.css b/brand/tokens/tokens.css new file mode 100644 index 0000000..b47b9ac --- /dev/null +++ b/brand/tokens/tokens.css @@ -0,0 +1,164 @@ +/* + * bit — Design Tokens (CSS) + * Surface-level tokens for color, typography, spacing, motion, and shape. + * No gradients. No shadows. Strict palette. Geometric primitives only. + */ + +@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Space+Mono:wght@400;700&display=swap"); + +:root { + /* ────────────────────────────────────────────────────────────── + * COLOR — strict 5-token palette. Never blend, never gradient. + * ────────────────────────────────────────────────────────────── */ + + /* Primary — Bit Pink. The node, the source, the single bit. */ + --bit-pink: #FF2D6F; + --bit-pink-deep: #C7124F; /* for press/hover states only */ + --bit-pink-tint: #FFD9E6; /* for filled tag/chip backgrounds */ + + /* Ink — off-black with cool tint. Editorial. Never #000. */ + --bit-ink: #0E0E12; + --bit-ink-soft: #2B2B33; /* secondary text */ + --bit-ink-mute: #6B6B74; /* meta / captions */ + + /* Paper — warm cream. Never #FFF. */ + --bit-paper: #F2EFE7; + --bit-paper-deep: #E8E4D8; /* alt surface, panels */ + + /* Stone — desaturated neutral. UI lines, dividers, tracks. */ + --bit-stone: #D9D5CB; + + /* Acid — the eccentric counterpoint. Use SPARINGLY. */ + --bit-acid: #D6FF3D; + + /* ────────────────────────────────────────────────────────────── + * SEMANTIC ALIASES + * ────────────────────────────────────────────────────────────── */ + + --bg: var(--bit-paper); + --bg-alt: var(--bit-paper-deep); + --fg: var(--bit-ink); + --fg-soft: var(--bit-ink-soft); + --fg-mute: var(--bit-ink-mute); + --rule: var(--bit-ink); /* rules are ink, full strength */ + --rule-soft: var(--bit-stone); + --accent: var(--bit-pink); + --accent-press: var(--bit-pink-deep); + --highlight: var(--bit-acid); + + /* ────────────────────────────────────────────────────────────── + * TYPOGRAPHY + * Sans → Space Grotesk (display + body) + * Mono → Space Mono (machine voice, captions, code) + * No italic. Ever. + * ────────────────────────────────────────────────────────────── */ + + --font-sans: "Space Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; + --font-mono: "Space Mono", ui-monospace, "JetBrains Mono", "SFMono-Regular", Menlo, monospace; + + /* Type scale — modular, tight at display, loose at body */ + --type-001: 11px; /* micro label */ + --type-00: 13px; /* caption, meta */ + --type-0: 15px; /* small body */ + --type-1: 17px; /* body */ + --type-2: 21px; /* lead */ + --type-3: 28px; /* h4 */ + --type-4: 40px; /* h3 */ + --type-5: 60px; /* h2 */ + --type-6: 96px; /* h1 */ + --type-7: 148px; /* hero, poster */ + + /* Line heights — display tight, body comfortable */ + --leading-tight: 0.95; + --leading-snug: 1.08; + --leading-body: 1.55; + --leading-loose: 1.72; + + /* Tracking — display tightens, micro labels open up */ + --track-display: -0.035em; + --track-heading: -0.018em; + --track-body: 0; + --track-label: 0.16em; /* UPPERCASE labels only */ + --track-mono: 0; + + /* ────────────────────────────────────────────────────────────── + * SHAPE — geometric primitives, no soft rounding. + * ────────────────────────────────────────────────────────────── */ + + --radius-none: 0; + --radius-dot: 999px; /* circles only — never used on rectangles */ + --stroke-hair: 1px; + --stroke-thin: 1.5px; + --stroke-med: 2px; + --stroke-thick: 4px; + + /* ────────────────────────────────────────────────────────────── + * SPACE — 4px base, prefer larger jumps for editorial whitespace. + * ────────────────────────────────────────────────────────────── */ + + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --space-4: 16px; + --space-5: 24px; + --space-6: 32px; + --space-7: 48px; + --space-8: 72px; + --space-9: 112px; + --space-10: 168px; + + /* ────────────────────────────────────────────────────────────── + * MOTION — instant or slow. No "smooth-default". + * ────────────────────────────────────────────────────────────── */ + + --ease-snap: cubic-bezier(0.2, 0.8, 0.2, 1); + --ease-flat: linear; + --dur-fast: 120ms; + --dur-slow: 480ms; + + color-scheme: light; +} + +/* ────────────────────────────────────────────────────────────── + * DARK MODE — invert paper/ink, keep pink and acid identical. + * ────────────────────────────────────────────────────────────── */ +@media (prefers-color-scheme: dark) { + :root { + --bg: var(--bit-ink); + --bg-alt: #17171C; + --fg: var(--bit-paper); + --fg-soft: #C9C5BB; + --fg-mute: #8A867D; + --rule: var(--bit-paper); + --rule-soft: #3A3A42; + + color-scheme: dark; + } +} + +/* Forced dark utility (e.g. previews) */ +[data-theme="dark"] { + --bg: var(--bit-ink); + --bg-alt: #17171C; + --fg: var(--bit-paper); + --fg-soft: #C9C5BB; + --fg-mute: #8A867D; + --rule: var(--bit-paper); + --rule-soft: #3A3A42; + + color-scheme: dark; +} + +/* ────────────────────────────────────────────────────────────── + * BRAND RULE — NO ITALIC. + * Browser defaults render , , , ,
, + * in italic. We do not ship italic fonts, so the browser would + * synthesize a slanted face — on a geometric grotesque that reads + * as a deformed serif. We override globally and carry emphasis + * through weight + color instead. + * ────────────────────────────────────────────────────────────── */ +em, i, cite, dfn, address, var, q { + font-style: normal; + font-weight: 600; + color: var(--fg); +} diff --git a/brand/tokens/tokens.json b/brand/tokens/tokens.json new file mode 100644 index 0000000..25de729 --- /dev/null +++ b/brand/tokens/tokens.json @@ -0,0 +1,100 @@ +{ + "$schema": "https://design-tokens.github.io/community-group/format/", + "$description": "bit brand design tokens. Strict palette, no gradients, no shadows.", + "color": { + "bit": { + "pink": { "$value": "#FF2D6F", "$type": "color", "$description": "Primary. The node, the source, the single bit." }, + "pink-deep": { "$value": "#C7124F", "$type": "color", "$description": "Press/hover only." }, + "pink-tint": { "$value": "#FFD9E6", "$type": "color", "$description": "Filled chip/tag background." }, + "ink": { "$value": "#0E0E12", "$type": "color", "$description": "Foreground. Never #000." }, + "ink-soft": { "$value": "#2B2B33", "$type": "color" }, + "ink-mute": { "$value": "#6B6B74", "$type": "color" }, + "paper": { "$value": "#F2EFE7", "$type": "color", "$description": "Background. Never #FFF." }, + "paper-deep": { "$value": "#E8E4D8", "$type": "color" }, + "stone": { "$value": "#D9D5CB", "$type": "color", "$description": "UI lines, dividers, tracks." }, + "acid": { "$value": "#D6FF3D", "$type": "color", "$description": "Eccentric counterpoint. Use sparingly." } + }, + "semantic": { + "bg": { "$value": "{color.bit.paper}", "$type": "color" }, + "bg-alt": { "$value": "{color.bit.paper-deep}", "$type": "color" }, + "fg": { "$value": "{color.bit.ink}", "$type": "color" }, + "fg-soft": { "$value": "{color.bit.ink-soft}", "$type": "color" }, + "fg-mute": { "$value": "{color.bit.ink-mute}", "$type": "color" }, + "rule": { "$value": "{color.bit.ink}", "$type": "color" }, + "rule-soft": { "$value": "{color.bit.stone}", "$type": "color" }, + "accent": { "$value": "{color.bit.pink}", "$type": "color" }, + "highlight": { "$value": "{color.bit.acid}", "$type": "color" } + } + }, + "font": { + "family": { + "sans": { "$value": "Space Grotesk, ui-sans-serif, system-ui, sans-serif", "$type": "fontFamily" }, + "mono": { "$value": "Space Mono, ui-monospace, JetBrains Mono, monospace", "$type": "fontFamily" } + }, + "weight": { + "light": { "$value": 300, "$type": "fontWeight" }, + "regular": { "$value": 400, "$type": "fontWeight" }, + "medium": { "$value": 500, "$type": "fontWeight" }, + "semi": { "$value": 600, "$type": "fontWeight" }, + "bold": { "$value": 700, "$type": "fontWeight" } + }, + "size": { + "001": { "$value": "11px", "$type": "dimension", "$description": "micro label" }, + "00": { "$value": "13px", "$type": "dimension", "$description": "caption" }, + "0": { "$value": "15px", "$type": "dimension" }, + "1": { "$value": "17px", "$type": "dimension", "$description": "body" }, + "2": { "$value": "21px", "$type": "dimension", "$description": "lead" }, + "3": { "$value": "28px", "$type": "dimension" }, + "4": { "$value": "40px", "$type": "dimension" }, + "5": { "$value": "60px", "$type": "dimension" }, + "6": { "$value": "96px", "$type": "dimension" }, + "7": { "$value": "148px","$type": "dimension", "$description": "hero / poster" } + }, + "tracking": { + "display": { "$value": "-0.035em", "$type": "string" }, + "heading": { "$value": "-0.018em", "$type": "string" }, + "body": { "$value": "0", "$type": "string" }, + "label": { "$value": "0.16em", "$type": "string", "$description": "UPPERCASE only" } + }, + "leading": { + "tight": { "$value": 0.95, "$type": "number" }, + "snug": { "$value": 1.08, "$type": "number" }, + "body": { "$value": 1.55, "$type": "number" }, + "loose": { "$value": 1.72, "$type": "number" } + } + }, + "space": { + "1": { "$value": "4px", "$type": "dimension" }, + "2": { "$value": "8px", "$type": "dimension" }, + "3": { "$value": "12px", "$type": "dimension" }, + "4": { "$value": "16px", "$type": "dimension" }, + "5": { "$value": "24px", "$type": "dimension" }, + "6": { "$value": "32px", "$type": "dimension" }, + "7": { "$value": "48px", "$type": "dimension" }, + "8": { "$value": "72px", "$type": "dimension" }, + "9": { "$value": "112px", "$type": "dimension" }, + "10": { "$value": "168px", "$type": "dimension" } + }, + "shape": { + "radius": { + "none": { "$value": "0", "$type": "dimension" }, + "dot": { "$value": "999px", "$type": "dimension", "$description": "Circle only. Never on rectangles." } + }, + "stroke": { + "hair": { "$value": "1px", "$type": "dimension" }, + "thin": { "$value": "1.5px", "$type": "dimension" }, + "med": { "$value": "2px", "$type": "dimension" }, + "thick": { "$value": "4px", "$type": "dimension" } + } + }, + "motion": { + "ease": { + "snap": { "$value": "cubic-bezier(0.2, 0.8, 0.2, 1)", "$type": "cubicBezier" }, + "flat": { "$value": "linear", "$type": "string" } + }, + "duration": { + "fast": { "$value": "120ms", "$type": "duration" }, + "slow": { "$value": "480ms", "$type": "duration" } + } + } +} diff --git a/site/.nojekyll b/site/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/site/README.md b/site/README.md new file mode 100644 index 0000000..c40dadc --- /dev/null +++ b/site/README.md @@ -0,0 +1,151 @@ +# bit — documentation site + +The static documentation site for **bit**. Deployed to GitHub Pages from this +folder. Markdown content under [`content/`](content/) is rendered to HTML at +build time using [`mizchi/markdown`](https://github.com/mizchi/markdown.mbt) +(MoonBit) wrapped by a tiny renderer in +[`tools/docs-build-mbt/`](../tools/docs-build-mbt/) and orchestrated by +[`tools/build-docs.mjs`](../tools/build-docs.mjs). + +## Layout + +``` +site/ +├── index.html ← landing page (hand-written, brand-heavy) +├── content/ ← MARKDOWN SOURCE — author here +│ ├── learn/ +│ │ ├── index.md +│ │ ├── concept.md +│ │ ├── install.md +│ │ ├── first-commit.md +│ │ └── distributed.md +│ └── reference/ +│ ├── index.md +│ ├── cli.md +│ ├── library.md +│ └── env.md +├── learn/ ← BUILD OUTPUT (overwritten by build-docs.mjs) +├── reference/ ← BUILD OUTPUT +├── assets/ +│ ├── tokens.css ← brand design tokens (copied from /brand) +│ ├── site.css ← site-level layout + components +│ ├── syntax.js ← MoonBit / bash syntax highlighter +│ ├── site.js ← nav toggle, copy buttons +│ └── img/ ← logo, wordmark, lockups (copied from /brand) +├── .nojekyll ← tell GitHub Pages not to run Jekyll +└── README.md ← you are here +``` + +## Build + +The pipeline is two steps. The MoonBit renderer compiles once and is reused +on every content change. + +```sh +# 1. build the MoonBit renderer (one-time, or after dep bumps) +cd tools/docs-build-mbt && moon update && moon build --target js --release && cd - + +# 2. render markdown to HTML +node tools/build-docs.mjs +``` + +This writes `site/learn/*.html` and `site/reference/*.html`. The +hand-written `site/index.html` is left alone. + +## Preview locally + +After building, open `site/index.html` in a browser or serve from a real +HTTP server (recommended — the clipboard API needs a secure context): + +```sh +python3 -m http.server -d site 8080 +# open http://localhost:8080 +``` + +## Deploy + +Pushed automatically by [`.github/workflows/pages.yml`](../.github/workflows/pages.yml) +on every push to `main` or `brand` that touches `site/`, `brand/`, +`tools/build-docs.mjs`, or `tools/docs-build-mbt/`. + +The workflow: + +1. Installs the MoonBit toolchain. +2. Builds the MoonBit renderer (`moon build --target js --release`). +3. Re-syncs brand assets (`tokens.css` + logo SVGs) from `brand/` into + `site/assets/`. +4. Runs `node tools/build-docs.mjs` to render markdown to HTML. +5. Uploads the `site/` folder to GitHub Pages. + +To enable Pages: in the repo's **Settings → Pages**, set the source to +**GitHub Actions**. The workflow handles the rest. + +## Authoring content + +Every chapter is a markdown file under `site/content/
/.md` +with a frontmatter block. Frontmatter keys are flat (no nesting); structure +is encoded by named fields like `prev_href` and `next_label`. + +### Frontmatter + +```yaml +--- +title: Install bit # and H1 fallback +section: learn # learn | reference +slug: install # filename without .md +order: 1 # sort order in sidenav and chapter list +nav_label: Install bit # label in sidenav (optional, defaults to title) +summary: One-line install… # shown on the section index TOC +meta: ~5 min # right-column meta on the index TOC +kicker: // guide · 01 # mono kicker above the H1 +h1: Install bit. # H1 text (defaults to title) +lead: Two install paths … # lead paragraph — markdown is rendered inline +prev_href: ./ # pager left +prev_kicker: back +prev_label: Learning Guide +next_href: first-commit.html +next_kicker: next · 02 +next_label: Your first commit +--- +``` + +For section index pages, add `template: index` and omit `prev_*` / `next_*`. +The chapter list is auto-generated from sibling pages. + +### Code blocks + +Fenced code blocks are post-processed into our `.codeblock` structure with +a language label and copy button. Supported `data-lang` values for the +syntax highlighter: + +- `moonbit` / `mbt` — MoonBit syntax +- `bash` / `sh` — shell prompts and comments +- anything else — plain text + +### Callouts + +Inline HTML is the simplest path: + +```html +<div class="callout"> +<p class="kicker">// note</p> +<p>Body text. Inline HTML inside is plain HTML — markdown is not parsed here.</p> +</div> + +<div class="callout callout--acid"> +<p class="kicker">// caveat</p> +<p>Acid variant for warnings.</p> +</div> +``` + +## Updating brand assets + +Don't edit `site/assets/tokens.css` or `site/assets/img/*.svg` directly — +edit the originals in [`brand/`](../brand/) and re-sync: + +```sh +cp brand/tokens/tokens.css site/assets/tokens.css +cp brand/logo/*.svg site/assets/img/ +``` + +The Pages workflow does this automatically on every deploy. diff --git a/site/assets/img/lockup-horizontal.svg b/site/assets/img/lockup-horizontal.svg new file mode 100644 index 0000000..c482891 --- /dev/null +++ b/site/assets/img/lockup-horizontal.svg @@ -0,0 +1,28 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 540 160" role="img" aria-label="bit — horizontal lockup"> + <title>bit horizontal lockup + Logo and wordmark side by side. Optical alignment: source node baseline aligned to wordmark cap height. + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/site/assets/img/lockup-stack.svg b/site/assets/img/lockup-stack.svg new file mode 100644 index 0000000..4127f11 --- /dev/null +++ b/site/assets/img/lockup-stack.svg @@ -0,0 +1,28 @@ + + bit stacked lockup + Logo above wordmark, both centered. For square placements: avatars, app icons, cover composition. + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/site/assets/img/logo-inverse.svg b/site/assets/img/logo-inverse.svg new file mode 100644 index 0000000..1205b9f --- /dev/null +++ b/site/assets/img/logo-inverse.svg @@ -0,0 +1,12 @@ + + bit logo, inverse + Logomark for dark surfaces. Paper-colored peers, pink source unchanged. + + + + + + + + + diff --git a/site/assets/img/logo-mono.svg b/site/assets/img/logo-mono.svg new file mode 100644 index 0000000..d498733 --- /dev/null +++ b/site/assets/img/logo-mono.svg @@ -0,0 +1,12 @@ + + bit logo, monochrome + Single-color variant for embossing, single-channel printing, and reductive contexts. + + + + + + + + + diff --git a/site/assets/img/logo.svg b/site/assets/img/logo.svg new file mode 100644 index 0000000..8f30f8c --- /dev/null +++ b/site/assets/img/logo.svg @@ -0,0 +1,16 @@ + + bit logo + A triad of nodes — one pink source, two ink peers — connected by edges. Distributed communication. + + + + + + + + + + + + + diff --git a/site/assets/img/wordmark-inverse.svg b/site/assets/img/wordmark-inverse.svg new file mode 100644 index 0000000..a7bba21 --- /dev/null +++ b/site/assets/img/wordmark-inverse.svg @@ -0,0 +1,13 @@ + + bit wordmark, inverse + Paper-colored letterforms for dark surfaces. Pink tittle unchanged. + + + + + + + + + + diff --git a/site/assets/img/wordmark.svg b/site/assets/img/wordmark.svg new file mode 100644 index 0000000..36208fc --- /dev/null +++ b/site/assets/img/wordmark.svg @@ -0,0 +1,16 @@ + + bit wordmark + "bit" constructed from geometric primitives: rectangles and circles. The pink dot above the i is the brand signature — a single node, a single source, a single bit. + + + + + + + + + + + + + diff --git a/site/assets/site.css b/site/assets/site.css new file mode 100644 index 0000000..427ba74 --- /dev/null +++ b/site/assets/site.css @@ -0,0 +1,579 @@ +/* + * bit — docs site styles (built on brand tokens.css) + * No gradients. No shadows. Rectangles only. + */ + +*, *::before, *::after { box-sizing: border-box; } +html, body { margin: 0; padding: 0; } +html { scroll-behavior: smooth; scroll-padding-top: 88px; } +body { + background: var(--bg); + color: var(--fg); + font-family: var(--font-sans); + font-size: var(--type-1); + line-height: var(--leading-body); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +img, svg { max-width: 100%; } + +/* ── links ────────────────────────────────────────────────────── */ +a { color: var(--bit-pink); text-decoration: none; border-bottom: 1px solid var(--bit-pink); } +a:hover { background: var(--bit-pink); color: var(--bit-paper); } +a.plain { color: inherit; border: 0; } +a.plain:hover { background: transparent; color: var(--bit-pink); } + +/* ── topbar ───────────────────────────────────────────────────── */ +.topbar { + position: sticky; top: 0; z-index: 10; + background: var(--bg); + border-bottom: 1px solid var(--rule); + padding: var(--space-4) var(--space-6); + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--space-5); +} +.topbar__brand { display: flex; align-items: center; gap: var(--space-3); } +.topbar__brand a { border: 0; } +.topbar__brand img { height: 26px; display: block; } +.topbar__brand .ver { + font-family: var(--font-mono); + font-size: var(--type-001); + letter-spacing: var(--track-label); + text-transform: uppercase; + color: var(--fg-mute); + padding-left: var(--space-3); + border-left: 1px solid var(--rule-soft); +} +.topbar nav { display: flex; gap: var(--space-5); } +.topbar nav a { + color: var(--fg); + text-decoration: none; + border: none; + font-family: var(--font-mono); + font-size: var(--type-001); + letter-spacing: var(--track-label); + text-transform: uppercase; + padding-bottom: 4px; + border-bottom: 1px solid transparent; + background: transparent; +} +.topbar nav a:hover, .topbar nav a.active { + color: var(--bit-pink); + border-bottom-color: var(--bit-pink); + background: transparent; +} +.topbar__menu { display: none; background: transparent; border: 1.5px solid var(--rule); padding: 6px 10px; cursor: pointer; } +.topbar__menu span { display: block; width: 18px; height: 1.5px; background: var(--fg); margin: 4px 0; } + +@media (max-width: 760px) { + .topbar nav { display: none; position: absolute; top: 100%; left: 0; right: 0; flex-direction: column; background: var(--bg); border-bottom: 1px solid var(--rule); padding: var(--space-5); gap: var(--space-4); } + .topbar nav.open { display: flex; } + .topbar__menu { display: block; } +} + +/* ── footer ───────────────────────────────────────────────────── */ +footer.site { + border-top: 1px solid var(--rule); + padding: var(--space-6); + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--space-5); + font-family: var(--font-mono); + font-size: var(--type-001); + letter-spacing: var(--track-label); + text-transform: uppercase; + color: var(--fg-mute); + flex-wrap: wrap; +} +footer.site a { color: var(--fg-mute); border: 0; } +footer.site a:hover { color: var(--bit-pink); background: transparent; } + +/* ── shared kicker ────────────────────────────────────────────── */ +.kicker { + font-family: var(--font-mono); + font-size: var(--type-001); + letter-spacing: var(--track-label); + text-transform: uppercase; + color: var(--fg-mute); + margin: 0 0 var(--space-4); +} +.kicker .pink { color: var(--bit-pink); } + +/* ── hero (landing) ───────────────────────────────────────────── */ +.landing { + display: flex; + flex-direction: column; +} +.hero { + padding: var(--space-8) var(--space-6) var(--space-9); + max-width: 1280px; + margin: 0 auto; + width: 100%; +} +.hero__tag { + display: flex; + justify-content: space-between; + align-items: baseline; + border-bottom: 1px solid var(--rule); + padding-bottom: var(--space-3); + margin-bottom: var(--space-6); + font-family: var(--font-mono); + font-size: var(--type-001); + letter-spacing: var(--track-label); + text-transform: uppercase; + color: var(--fg-mute); +} +.hero__tag strong { color: var(--fg); font-weight: 400; } +.hero__grid { display: grid; grid-template-columns: 1.4fr 1fr; gap: var(--space-7); align-items: end; } +@media (max-width: 900px) { .hero__grid { grid-template-columns: 1fr; gap: var(--space-6); } } +.hero__head { + font-family: var(--font-sans); + font-weight: 700; + font-size: clamp(56px, 11vw, 132px); + line-height: var(--leading-tight); + letter-spacing: var(--track-display); + margin: 0 0 var(--space-5); +} +.hero__head .dot { + display: inline-block; + width: 0.6em; height: 0.6em; + border-radius: var(--radius-dot); + background: var(--bit-pink); + vertical-align: 0.06em; + margin: 0 0.03em; +} +.hero__sub { + font-size: var(--type-2); + line-height: var(--leading-snug); + margin: 0 0 var(--space-6); + max-width: 38ch; + color: var(--fg-soft); +} +.hero__cta { display: flex; flex-wrap: wrap; gap: var(--space-3); margin: 0 0 var(--space-6); } +.hero__install { + background: var(--bit-ink); + color: var(--bit-paper); + padding: var(--space-4) var(--space-5); + font-family: var(--font-mono); + font-size: var(--type-0); + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--space-3); +} +.hero__install .prompt { color: var(--bit-pink); } +.hero__install button { + background: transparent; + color: var(--bit-paper); + border: 1px solid var(--bit-paper); + padding: 4px 10px; + font-family: var(--font-mono); + font-size: var(--type-001); + letter-spacing: var(--track-label); + text-transform: uppercase; + cursor: pointer; +} +.hero__install button:hover { background: var(--bit-paper); color: var(--bit-ink); } +.hero__art { display: flex; align-items: center; justify-content: center; } +.hero__art img { max-width: 320px; width: 100%; } + +/* ── buttons ──────────────────────────────────────────────────── */ +.btn { + display: inline-flex; + align-items: center; + gap: var(--space-2); + padding: 14px 22px; + border: 0; + font-family: var(--font-sans); + font-weight: 500; + font-size: var(--type-0); + letter-spacing: var(--track-heading); + cursor: pointer; + text-decoration: none; + transition: background var(--dur-fast) var(--ease-snap), color var(--dur-fast) var(--ease-snap); + border-bottom: 0 !important; +} +.btn--primary { background: var(--bit-pink); color: var(--bit-paper); } +.btn--primary:hover { background: var(--bit-pink-deep); color: var(--bit-paper); } +.btn--ghost { background: transparent; color: var(--fg); border: 1.5px solid var(--rule); } +.btn--ghost:hover { background: var(--fg); color: var(--bg); } + +/* ── manifesto strip ─────────────────────────────────────────── */ +.strip { + background: var(--bit-ink); + color: var(--bit-paper); + padding: var(--space-8) var(--space-6); + border-top: 1px solid var(--rule); + border-bottom: 1px solid var(--rule); +} +.strip__wrap { max-width: 1280px; margin: 0 auto; } +.strip .kicker { color: var(--bit-paper-deep); } +.strip__head { + font-family: var(--font-sans); + font-weight: 700; + font-size: clamp(40px, 7vw, 84px); + line-height: var(--leading-tight); + letter-spacing: var(--track-display); + margin: 0; +} +.strip__head .acid { color: var(--bit-acid); } + +/* ── feature grid ─────────────────────────────────────────────── */ +.section { + padding: var(--space-8) var(--space-6); + max-width: 1280px; + margin: 0 auto; + width: 100%; +} +.section__head { + display: grid; + grid-template-columns: 80px 1fr; + align-items: baseline; + gap: var(--space-5); + margin-bottom: var(--space-7); +} +.section__num { + font-family: var(--font-mono); + font-size: var(--type-001); + letter-spacing: var(--track-label); + text-transform: uppercase; + color: var(--bit-pink); +} +.section__title { + font-family: var(--font-sans); + font-weight: 700; + font-size: clamp(28px, 5vw, var(--type-4)); + letter-spacing: var(--track-heading); + line-height: var(--leading-snug); + margin: 0; +} + +.feature-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-5); } +@media (max-width: 760px) { .feature-grid { grid-template-columns: 1fr; } } +.feature { + border: 1px solid var(--rule); + padding: var(--space-6) var(--space-5); + display: flex; + flex-direction: column; + gap: var(--space-3); + background: var(--bg); +} +.feature__no { + font-family: var(--font-mono); + font-size: var(--type-001); + letter-spacing: var(--track-label); + text-transform: uppercase; + color: var(--bit-pink); + margin: 0; +} +.feature__h { + font-family: var(--font-sans); + font-weight: 600; + font-size: var(--type-3); + letter-spacing: var(--track-heading); + line-height: var(--leading-snug); + margin: 0; +} +.feature__p { margin: 0; color: var(--fg-soft); font-size: var(--type-1); line-height: var(--leading-body); } +.feature__link { margin-top: auto; padding-top: var(--space-3); } +.feature__link a { + color: var(--fg); + border-bottom: 1px solid var(--fg); + font-family: var(--font-mono); + font-size: var(--type-001); + letter-spacing: var(--track-label); + text-transform: uppercase; +} +.feature__link a:hover { background: var(--bit-pink); color: var(--bit-paper); border-bottom-color: var(--bit-pink); } + +/* ── docs layout (Learn + Reference pages) ────────────────────── */ +.layout { + display: grid; + grid-template-columns: 240px 1fr 220px; + gap: var(--space-7); + max-width: 1280px; + margin: 0 auto; + padding: var(--space-7) var(--space-6) var(--space-9); +} +@media (max-width: 1080px) { .layout { grid-template-columns: 220px 1fr; } aside.toc { display: none; } } +@media (max-width: 760px) { .layout { grid-template-columns: 1fr; padding: var(--space-5); } aside.side { display: none; } } + +aside.side { + position: sticky; top: 88px; align-self: start; + font-family: var(--font-sans); font-size: var(--type-0); + max-height: calc(100vh - 100px); overflow-y: auto; +} +aside.side h4 { + font-family: var(--font-mono); + font-size: var(--type-001); + letter-spacing: var(--track-label); + text-transform: uppercase; + color: var(--fg-mute); + margin: 0 0 var(--space-3); + font-weight: 400; +} +aside.side ul { list-style: none; padding: 0; margin: 0 0 var(--space-6); } +aside.side li { padding: 4px 0; } +aside.side a { + color: var(--fg); + text-decoration: none; + border: none; + display: block; + padding: 2px 0 2px var(--space-3); + border-left: 1px solid var(--rule-soft); + background: transparent; +} +aside.side a:hover { color: var(--bit-pink); border-left-color: var(--bit-pink); background: transparent; } +aside.side a.active { color: var(--bit-pink); border-left-color: var(--bit-pink); font-weight: 500; } + +aside.toc { + position: sticky; top: 88px; align-self: start; + font-family: var(--font-mono); + font-size: var(--type-001); + letter-spacing: var(--track-label); + text-transform: uppercase; +} +aside.toc h4 { color: var(--fg-mute); margin: 0 0 var(--space-3); font-weight: 400; } +aside.toc ul { list-style: none; padding: 0; margin: 0; } +aside.toc li { padding: 6px 0; } +aside.toc a { color: var(--fg); text-decoration: none; border: none; display: flex; gap: var(--space-2); background: transparent; } +aside.toc a:hover { color: var(--bit-pink); } +aside.toc .num { color: var(--bit-pink); } + +/* ── article ──────────────────────────────────────────────────── */ +article { min-width: 0; } +article header.title { + border-bottom: 1px solid var(--rule); + padding-bottom: var(--space-6); + margin-bottom: var(--space-7); +} +article .kicker { margin: 0 0 var(--space-4); } +article h1 { + font-family: var(--font-sans); + font-weight: 700; + font-size: clamp(40px, 6vw, var(--type-5)); + line-height: var(--leading-tight); + letter-spacing: var(--track-display); + margin: 0 0 var(--space-4); +} +article .lead { font-size: var(--type-2); line-height: var(--leading-snug); color: var(--fg-soft); max-width: 56ch; margin: 0; } + +article h2 { + font-family: var(--font-sans); + font-weight: 700; + font-size: var(--type-4); + line-height: var(--leading-snug); + letter-spacing: var(--track-heading); + margin: var(--space-8) 0 var(--space-4); + padding-top: var(--space-3); + border-top: 1px solid var(--rule); +} +article h2[data-num]::before { + content: attr(data-num); + display: block; + font-family: var(--font-mono); + font-size: var(--type-001); + letter-spacing: var(--track-label); + text-transform: uppercase; + color: var(--bit-pink); + margin-bottom: var(--space-3); + font-weight: 400; +} +article h3 { + font-family: var(--font-sans); + font-weight: 600; + font-size: var(--type-2); + margin: var(--space-6) 0 var(--space-3); + letter-spacing: var(--track-heading); +} +article h4 { + font-family: var(--font-sans); + font-weight: 600; + font-size: var(--type-1); + margin: var(--space-5) 0 var(--space-2); + letter-spacing: var(--track-heading); +} +article p { margin: 0 0 var(--space-4); max-width: 72ch; } +article ul, article ol { max-width: 72ch; padding-left: var(--space-5); margin: 0 0 var(--space-4); } +article li { padding: 4px 0; } +article li::marker { color: var(--bit-pink); } +article strong { font-weight: 600; } +article hr { border: 0; border-top: 1px solid var(--rule-soft); margin: var(--space-6) 0; } + +article code { + font-family: var(--font-mono); + font-size: 0.94em; + background: var(--bit-paper-deep); + padding: 2px 6px; + border: 1px solid var(--rule-soft); +} + +/* ── code blocks ──────────────────────────────────────────────── */ +.codeblock { + position: relative; + margin: 0 0 var(--space-5); +} +.codeblock__bar { + display: flex; + justify-content: space-between; + align-items: center; + background: var(--bit-ink); + color: var(--bit-paper-deep); + padding: 8px var(--space-4); + border-bottom: 1px solid #2A2A30; + font-family: var(--font-mono); + font-size: var(--type-001); + letter-spacing: var(--track-label); + text-transform: uppercase; +} +.codeblock__bar .lang { color: var(--bit-pink); } +.codeblock__copy { + background: transparent; + color: var(--bit-paper-deep); + border: 1px solid var(--bit-paper-deep); + padding: 2px 8px; + font-family: var(--font-mono); + font-size: var(--type-001); + letter-spacing: var(--track-label); + text-transform: uppercase; + cursor: pointer; +} +.codeblock__copy:hover { background: var(--bit-paper); color: var(--bit-ink); } +.codeblock__copy.copied { background: var(--bit-acid); color: var(--bit-ink); border-color: var(--bit-acid); } +.codeblock pre { + background: var(--bit-ink); + color: var(--bit-paper); + padding: var(--space-5); + overflow-x: auto; + margin: 0; + font-family: var(--font-mono); + font-size: var(--type-0); + line-height: 1.7; +} +.codeblock pre code { background: transparent; border: 0; padding: 0; color: inherit; font-size: inherit; } + +/* syntax token colors */ +.tok-cm { color: var(--bit-ink-mute); } /* comment */ +.tok-kw { color: var(--bit-pink); } /* keyword */ +.tok-ty { color: var(--bit-acid); } /* type */ +.tok-str { color: #FFB28A; } /* string — warm peach (still no gradient) */ +.tok-num { color: var(--bit-paper); } /* number */ +.tok-fn { color: var(--bit-paper); font-weight: 500; } /* function name */ +.tok-pm { color: #B89CFF; } /* preamble: pub, extern, import — soft violet */ + +/* ── tables ───────────────────────────────────────────────────── */ +table { + width: 100%; + border-collapse: collapse; + margin: var(--space-5) 0; + font-size: var(--type-0); +} +thead th { + text-align: left; + font-family: var(--font-mono); + font-size: var(--type-001); + letter-spacing: var(--track-label); + text-transform: uppercase; + color: var(--fg-mute); + font-weight: 400; + padding: var(--space-2) var(--space-3); + border-bottom: 1px solid var(--rule); +} +tbody td { padding: var(--space-3); border-bottom: 1px solid var(--rule-soft); vertical-align: top; } +tbody td:first-child { font-family: var(--font-mono); white-space: nowrap; } +tbody tr:hover { background: var(--bit-paper-deep); } + +/* ── callouts ─────────────────────────────────────────────────── */ +.callout { + border: 1px solid var(--rule); + border-left: 6px solid var(--bit-pink); + padding: var(--space-4) var(--space-5); + margin: var(--space-5) 0; +} +.callout .kicker { margin: 0 0 var(--space-2); color: var(--bit-pink); } +.callout p:last-child { margin-bottom: 0; } +.callout--acid { border-left-color: var(--bit-ink); background: var(--bit-acid); } +.callout--acid .kicker { color: var(--bit-ink); } +.callout--acid a { color: var(--bit-ink); border-bottom-color: var(--bit-ink); } +.callout--acid a:hover { background: var(--bit-ink); color: var(--bit-acid); } + +/* ── chapter grid (Learn / Reference index) ───────────────────── */ +.chapters { display: grid; grid-template-columns: 1fr; gap: 0; border-top: 1px solid var(--rule); } +.chapter { + display: grid; + grid-template-columns: 80px 1fr 100px; + gap: var(--space-5); + padding: var(--space-6) var(--space-3); + border-bottom: 1px solid var(--rule); + align-items: baseline; + text-decoration: none; + color: inherit; + border-bottom: 1px solid var(--rule); + background: transparent; +} +a.chapter { border: 0; border-bottom: 1px solid var(--rule); } +.chapter:hover { background: var(--bit-paper-deep); color: var(--bit-ink); } +.chapter:hover .chapter__h { color: var(--bit-pink); } +.chapter__num { + font-family: var(--font-mono); + font-size: var(--type-001); + letter-spacing: var(--track-label); + text-transform: uppercase; + color: var(--bit-pink); +} +.chapter__h { + font-family: var(--font-sans); + font-weight: 600; + font-size: var(--type-3); + letter-spacing: var(--track-heading); + line-height: var(--leading-snug); + margin: 0 0 var(--space-2); + transition: color var(--dur-fast) var(--ease-snap); +} +.chapter__p { margin: 0; color: var(--fg-soft); font-size: var(--type-1); } +.chapter__meta { + font-family: var(--font-mono); + font-size: var(--type-001); + letter-spacing: var(--track-label); + text-transform: uppercase; + color: var(--fg-mute); + text-align: right; +} +@media (max-width: 760px) { + .chapter { grid-template-columns: 60px 1fr; } + .chapter__meta { display: none; } +} + +/* ── pager (prev/next at chapter end) ─────────────────────────── */ +.pager { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--space-4); + margin-top: var(--space-8); + padding-top: var(--space-5); + border-top: 1px solid var(--rule); +} +.pager a { + border: 1px solid var(--rule); + padding: var(--space-4) var(--space-5); + color: inherit; + background: transparent; + display: flex; + flex-direction: column; + gap: 4px; +} +.pager a:hover { background: var(--fg); color: var(--bg); border-color: var(--fg); } +.pager a:hover .pager__t { color: var(--bg); } +.pager .pager__k { + font-family: var(--font-mono); + font-size: var(--type-001); + letter-spacing: var(--track-label); + text-transform: uppercase; + color: var(--fg-mute); +} +.pager .pager__t { font-family: var(--font-sans); font-weight: 500; font-size: var(--type-1); } +.pager .next { text-align: right; align-items: flex-end; } +@media (max-width: 760px) { .pager { grid-template-columns: 1fr; } .pager .next { text-align: left; align-items: flex-start; } } diff --git a/site/assets/site.js b/site/assets/site.js new file mode 100644 index 0000000..271f582 --- /dev/null +++ b/site/assets/site.js @@ -0,0 +1,23 @@ +// bit — site-level interactivity +(function () { + // mobile nav toggle + const menu = document.querySelector(".topbar__menu"); + const nav = document.querySelector(".topbar nav"); + if (menu && nav) { + menu.addEventListener("click", () => nav.classList.toggle("open")); + } + + // hero install copy (single button) + const heroCopy = document.querySelector(".hero__install button"); + if (heroCopy) { + heroCopy.addEventListener("click", () => { + const cmd = heroCopy.parentElement.querySelector(".cmd"); + if (!cmd) return; + navigator.clipboard.writeText(cmd.textContent).then(() => { + const original = heroCopy.textContent; + heroCopy.textContent = "copied"; + setTimeout(() => { heroCopy.textContent = original; }, 1400); + }); + }); + } +})(); diff --git a/site/assets/syntax.js b/site/assets/syntax.js new file mode 100644 index 0000000..7d751e1 --- /dev/null +++ b/site/assets/syntax.js @@ -0,0 +1,100 @@ +// bit — minimal MoonBit syntax highlighter +// Vanilla JS, no deps. Tokenizes via a single ordered regex (Crockford-style). +// Classes match site.css: tok-cm, tok-kw, tok-ty, tok-str, tok-num, tok-fn, tok-pm +(function () { + const KEYWORDS = new Set([ + "let", "fn", "match", "if", "else", "for", "while", "return", "mut", + "loop", "break", "continue", "as", "in", "with", "struct", "enum", + "trait", "impl", "type", "typealias", "test", "guard", "raise", + "try", "catch", "self", "and", "or", "not", "where", "is", + "derive", "init", "new" + ]); + const PREAMBLE = new Set([ + "pub", "priv", "extern", "import", "package", "fnalias", "traitalias", + "typealias", "async" + ]); + const TYPES = new Set([ + "Int", "Int64", "UInt", "UInt64", "Double", "Float", "Bool", "String", + "Char", "Bytes", "Unit", "Array", "FixedArray", "Map", "HashMap", + "HashSet", "Option", "Result", "Iter", "Ref", "BigInt", "Json" + ]); + const CONSTS = new Set(["true", "false", "None", "Some", "Ok", "Err", "Nil"]); + + // ordered token regex — first hit wins + const RE = new RegExp([ + "(\\/\\/[^\\n]*)", // 1 line comment + "(\\/\\*[\\s\\S]*?\\*\\/)", // 2 block comment + "(b?\"(?:\\\\.|[^\"\\\\])*\")", // 3 string / b-string + "('(?:\\\\.|[^'\\\\])')", // 4 char literal + "(0x[0-9a-fA-F_]+|0b[01_]+|0o[0-7_]+|[0-9][0-9_]*(?:\\.[0-9_]+)?(?:[eE][+-]?[0-9_]+)?[a-zA-Z]*)", // 5 number + "([A-Za-z_][A-Za-z0-9_]*)", // 6 ident + "([^\\s\\w]+)" // 7 punct (passthrough) + ].join("|"), "g"); + + function esc(s) { + return s.replace(/&/g, "&").replace(//g, ">"); + } + + function highlight(src) { + let out = ""; + let lastIndex = 0; + let m; + RE.lastIndex = 0; + while ((m = RE.exec(src)) !== null) { + if (m.index > lastIndex) out += esc(src.slice(lastIndex, m.index)); + const [_, lc, bc, str, ch, num, id, punct] = m; + if (lc) out += `${esc(lc)}`; + else if (bc) out += `${esc(bc)}`; + else if (str)out += `${esc(str)}`; + else if (ch) out += `${esc(ch)}`; + else if (num)out += `${esc(num)}`; + else if (id) { + if (PREAMBLE.has(id)) out += `${id}`; + else if (KEYWORDS.has(id)) out += `${id}`; + else if (TYPES.has(id)) out += `${id}`; + else if (CONSTS.has(id)) out += `${id}`; + else { + // function call detection: ident followed by '(' + const next = src[m.index + id.length]; + if (next === "(") out += `${id}`; + else out += id; + } + } else if (punct) out += esc(punct); + lastIndex = RE.lastIndex; + } + if (lastIndex < src.length) out += esc(src.slice(lastIndex)); + return out; + } + + // bash/shell minimal: comments + strings + prompt + function shellHighlight(src) { + return esc(src) + .replace(/(^|\n)(\$|#) /g, (_, nl, sigil) => `${nl}${sigil} `) + .replace(/(#[^\n]*)/g, '$1') + .replace(/("[^"]*"|'[^']*')/g, '$1'); + } + + function init() { + document.querySelectorAll('pre code[data-lang]').forEach((el) => { + const lang = el.dataset.lang; + const src = el.textContent; + if (lang === "moonbit" || lang === "mbt") el.innerHTML = highlight(src); + else if (lang === "bash" || lang === "sh") el.innerHTML = shellHighlight(src); + }); + + document.querySelectorAll('.codeblock__copy').forEach((btn) => { + btn.addEventListener("click", () => { + const code = btn.closest('.codeblock').querySelector('pre code'); + if (!code) return; + navigator.clipboard.writeText(code.textContent).then(() => { + btn.textContent = "copied"; + btn.classList.add("copied"); + setTimeout(() => { btn.textContent = "copy"; btn.classList.remove("copied"); }, 1400); + }); + }); + }); + } + + if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init); + else init(); +})(); diff --git a/site/assets/tokens.css b/site/assets/tokens.css new file mode 100644 index 0000000..b47b9ac --- /dev/null +++ b/site/assets/tokens.css @@ -0,0 +1,164 @@ +/* + * bit — Design Tokens (CSS) + * Surface-level tokens for color, typography, spacing, motion, and shape. + * No gradients. No shadows. Strict palette. Geometric primitives only. + */ + +@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Space+Mono:wght@400;700&display=swap"); + +:root { + /* ────────────────────────────────────────────────────────────── + * COLOR — strict 5-token palette. Never blend, never gradient. + * ────────────────────────────────────────────────────────────── */ + + /* Primary — Bit Pink. The node, the source, the single bit. */ + --bit-pink: #FF2D6F; + --bit-pink-deep: #C7124F; /* for press/hover states only */ + --bit-pink-tint: #FFD9E6; /* for filled tag/chip backgrounds */ + + /* Ink — off-black with cool tint. Editorial. Never #000. */ + --bit-ink: #0E0E12; + --bit-ink-soft: #2B2B33; /* secondary text */ + --bit-ink-mute: #6B6B74; /* meta / captions */ + + /* Paper — warm cream. Never #FFF. */ + --bit-paper: #F2EFE7; + --bit-paper-deep: #E8E4D8; /* alt surface, panels */ + + /* Stone — desaturated neutral. UI lines, dividers, tracks. */ + --bit-stone: #D9D5CB; + + /* Acid — the eccentric counterpoint. Use SPARINGLY. */ + --bit-acid: #D6FF3D; + + /* ────────────────────────────────────────────────────────────── + * SEMANTIC ALIASES + * ────────────────────────────────────────────────────────────── */ + + --bg: var(--bit-paper); + --bg-alt: var(--bit-paper-deep); + --fg: var(--bit-ink); + --fg-soft: var(--bit-ink-soft); + --fg-mute: var(--bit-ink-mute); + --rule: var(--bit-ink); /* rules are ink, full strength */ + --rule-soft: var(--bit-stone); + --accent: var(--bit-pink); + --accent-press: var(--bit-pink-deep); + --highlight: var(--bit-acid); + + /* ────────────────────────────────────────────────────────────── + * TYPOGRAPHY + * Sans → Space Grotesk (display + body) + * Mono → Space Mono (machine voice, captions, code) + * No italic. Ever. + * ────────────────────────────────────────────────────────────── */ + + --font-sans: "Space Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; + --font-mono: "Space Mono", ui-monospace, "JetBrains Mono", "SFMono-Regular", Menlo, monospace; + + /* Type scale — modular, tight at display, loose at body */ + --type-001: 11px; /* micro label */ + --type-00: 13px; /* caption, meta */ + --type-0: 15px; /* small body */ + --type-1: 17px; /* body */ + --type-2: 21px; /* lead */ + --type-3: 28px; /* h4 */ + --type-4: 40px; /* h3 */ + --type-5: 60px; /* h2 */ + --type-6: 96px; /* h1 */ + --type-7: 148px; /* hero, poster */ + + /* Line heights — display tight, body comfortable */ + --leading-tight: 0.95; + --leading-snug: 1.08; + --leading-body: 1.55; + --leading-loose: 1.72; + + /* Tracking — display tightens, micro labels open up */ + --track-display: -0.035em; + --track-heading: -0.018em; + --track-body: 0; + --track-label: 0.16em; /* UPPERCASE labels only */ + --track-mono: 0; + + /* ────────────────────────────────────────────────────────────── + * SHAPE — geometric primitives, no soft rounding. + * ────────────────────────────────────────────────────────────── */ + + --radius-none: 0; + --radius-dot: 999px; /* circles only — never used on rectangles */ + --stroke-hair: 1px; + --stroke-thin: 1.5px; + --stroke-med: 2px; + --stroke-thick: 4px; + + /* ────────────────────────────────────────────────────────────── + * SPACE — 4px base, prefer larger jumps for editorial whitespace. + * ────────────────────────────────────────────────────────────── */ + + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --space-4: 16px; + --space-5: 24px; + --space-6: 32px; + --space-7: 48px; + --space-8: 72px; + --space-9: 112px; + --space-10: 168px; + + /* ────────────────────────────────────────────────────────────── + * MOTION — instant or slow. No "smooth-default". + * ────────────────────────────────────────────────────────────── */ + + --ease-snap: cubic-bezier(0.2, 0.8, 0.2, 1); + --ease-flat: linear; + --dur-fast: 120ms; + --dur-slow: 480ms; + + color-scheme: light; +} + +/* ────────────────────────────────────────────────────────────── + * DARK MODE — invert paper/ink, keep pink and acid identical. + * ────────────────────────────────────────────────────────────── */ +@media (prefers-color-scheme: dark) { + :root { + --bg: var(--bit-ink); + --bg-alt: #17171C; + --fg: var(--bit-paper); + --fg-soft: #C9C5BB; + --fg-mute: #8A867D; + --rule: var(--bit-paper); + --rule-soft: #3A3A42; + + color-scheme: dark; + } +} + +/* Forced dark utility (e.g. previews) */ +[data-theme="dark"] { + --bg: var(--bit-ink); + --bg-alt: #17171C; + --fg: var(--bit-paper); + --fg-soft: #C9C5BB; + --fg-mute: #8A867D; + --rule: var(--bit-paper); + --rule-soft: #3A3A42; + + color-scheme: dark; +} + +/* ────────────────────────────────────────────────────────────── + * BRAND RULE — NO ITALIC. + * Browser defaults render , , , ,
, + * in italic. We do not ship italic fonts, so the browser would + * synthesize a slanted face — on a geometric grotesque that reads + * as a deformed serif. We override globally and carry emphasis + * through weight + color instead. + * ────────────────────────────────────────────────────────────── */ +em, i, cite, dfn, address, var, q { + font-style: normal; + font-weight: 600; + color: var(--fg); +} diff --git a/site/content/learn/concept.md b/site/content/learn/concept.md new file mode 100644 index 0000000..ec327da --- /dev/null +++ b/site/content/learn/concept.md @@ -0,0 +1,91 @@ +--- +title: Concept +section: learn +slug: concept +order: 0 +nav_label: Concept +summary: What bit is, what is interesting about it, and the five-concept mental model that makes everything else fall into place. +meta: ~7 min +kicker: // guide · 00 — the idea +h1: What bit is. +lead: bit is a Git implementation in MoonBit. It speaks the Git wire protocol, it produces a normal `.git/`, and any Git client can read what it writes. Underneath, bit treats distribution as the default — not a feature bolted onto a central server, but the entire premise. +prev_href: ./ +prev_kicker: back +prev_label: Learning Guide +next_href: install.html +next_kicker: next · 01 +next_label: Install bit +--- + +## What's interesting + +Three things distinguish bit from "yet another Git client." + +**PRs and issues live inside the repo.** Pull requests, reviews, and issues are written as Git objects under `refs/notes/bit-hub`. Replicate the repo and you replicate the work. GitHub can go away. Your project history — and the conversations around it — survive intact. + +**bit is a library before it is a tool.** Every CLI command is a thin shell over a MoonBit function. Drop bit into an agent, an editor plugin, or a CI runner — you do not shell out, you call it. Mount an in-memory filesystem and you have a working repository in five lines. + +**Convergence is gossip, not consensus.** Peers reconcile by walking object graphs until they meet a common ancestor. No election, no quorum, no leader. A laptop offline for a week catches up the moment it touches a peer. + +## A five-concept mental model + +These five words carry the whole system. If you have them straight, the CLI and the library both stop surprising you. + +### Node + +A node is any process holding a copy of the repository. Your laptop is a node. A CI runner is a node. A relay host is a node. Nodes are symmetric — there is no "primary" — and they are addressed by URI: + +```text +bit://aurora.local:9418/repo +relay://bit.example.com/u/mizchi/bit +https://github.com/mizchi/bit.git +``` + +### Source + +Source is whoever wrote the commit you are looking at. It is a fact, not a place. The pink dot in the bit logo represents the source node; in any given moment, *any* node can become it. + +```moonbit +let commit = @bit.head(fs, ".git")? +println("author: \{commit.author}") +println("source: \{commit.committer.tz}@\{commit.committer.email}") +``` + +### Bit + +A bit is a single edge between two peers — one unit of exchange. Communication is content-addressed: peers ask for object hashes, never filenames. The protocol is therefore naturally idempotent and naturally deduplicating. + +### Convergence + +Two peers handshake, exchange ref tips, and walk the graph backwards until they meet. Bytes flow in both directions. Disagreement persists as parallel heads until a human merges them. There is no server-side reconciliation step. There is no server. + +### Hub + +PRs and issues are stored as Git objects. The hub is not a service — it is a particular shape of commit graph. `bit pr create` writes objects. `bit relay sync push` replicates them. The hub survives forks because it has no "outside" to survive against. + +## What follows from this + +Implications you can feel as soon as you start using it. + +- The hub survives forks. Fork a repo, get every PR and issue with it. +- Agents drive bit without a shell. The same MoonBit functions the CLI calls, your agent can call directly. +- You can clone a single subdirectory of a 50 GB monorepo with `bit clone user/repo:src/lib`. +- A merge conflict resolved by AI is just commits plus a note — no third-party state, no external dashboard. +- You can write your own bit-aware tool in MoonBit in an afternoon. The library API is the surface. + +## What bit is not + +Equally important — bit refuses several adjacent identities. + +- **Not a Git replacement.** It *is* Git. The bytes on disk are valid Git, the wire protocol is Git's, the SHA-1 history is interchangeable. Use it next to vanilla `git` if you want. +- **Not a blockchain.** No consensus, no tokens, no proof-of-anything. Convergence is gossip. +- **Not a P2P file share.** bit is collaboration software that happens to be distributed. The unit of meaning is a commit, not a file. +- **Not an LLM agent.** AI assist is one optional command (`bit ai rebase`), not the architecture. + +## What's next + +Now that you have the model, the rest of the guide is mostly muscle memory. + +- Read [Install bit](install.html) and get the binary on PATH. +- Walk through [Your first commit](first-commit.html) for the porcelain refresher. +- Hit [Going distributed](distributed.html) to see two peers converge. diff --git a/site/content/learn/distributed.md b/site/content/learn/distributed.md new file mode 100644 index 0000000..b8d0336 --- /dev/null +++ b/site/content/learn/distributed.md @@ -0,0 +1,111 @@ +--- +title: Going distributed +section: learn +slug: distributed +order: 3 +nav_label: Going distributed +summary: Clone over HTTP and over a bit relay. Push, fetch, and watch gossip converge two peers. +meta: ~10 min +kicker: // guide · 03 +h1: Going distributed. +lead: bit was built on the premise that source control should not depend on a central host. In this chapter you will clone a repository over HTTP and over a bit *relay*, push commits in both directions, and watch two peers converge through gossip. +prev_href: first-commit.html +prev_kicker: prev · 02 +prev_label: Your first commit +next_href: hub.html +next_kicker: next · 04 +next_label: PRs & issues +--- + +## Mental model + +A bit **node** is any process holding a copy of the repository. Your laptop, a CI runner, a relay host — all peers, no master. Two peers reconcile by exchanging ref tips and walking the object graph until they meet a common ancestor. + +| Mode | Trigger | Direction | +| --- | --- | --- | +| push | explicit | local → remote | +| fetch | explicit | remote → local | +| gossip | periodic, on handshake | both | +| relay sync | via relay host | both, async | + +## Clone over HTTP + +This is the standard Git path. bit speaks `git-upload-pack` and `git-receive-pack` over smart HTTP. + +```bash +$ bit clone https://github.com/user/repo +$ cd repo +$ bit remote -v +origin https://github.com/user/repo (fetch) +origin https://github.com/user/repo (push) +``` + +## Clone over a relay + +A *relay* is a lightweight bit-aware host. It speaks the bit protocol over HTTP and supports async sync — useful when peers are not online at the same time. + +```bash +$ bit clone relay://bit.example.com/u/me/repo +$ cd repo +``` + +## Two peers, one truth + +Open two terminals. In the first, simulate node A: + +```bash +$ bit clone relay://bit.example.com/u/me/repo a +$ cd a +$ echo "from A" >> note.txt +$ bit add . && bit commit -m "A writes" +$ bit push origin main +``` + +In the second terminal, node B: + +```bash +$ bit clone relay://bit.example.com/u/me/repo b +$ cd b +$ bit fetch origin +$ bit log --oneline +a8c2f6e (origin/main) A writes +1d0e7c2 root commit +``` + +B now holds A's history without ever talking to A directly. The relay carried the bytes asynchronously. + +## Concurrent writes + +Two peers writing at the same time produce two heads. bit's history graph is content-addressed, so collisions are deterministic — both heads are valid until a merge or rebase reconciles them. There is no server vote. + +
+

// recommendation

+

Use bit ai rebase when the conflict is mechanical (formatting drift, import re-orderings). For semantic conflicts, do the merge by hand — the AI assist is wrong often enough that you want to read what it proposes.

+
+ +## Inspect the graph + +```bash +$ bit log --graph --oneline --all +* 1f02776 (HEAD -> main) merge: reconcile A and B +|\ +| * e871d71 B writes +* | a8c2f6e A writes +|/ +* 1d0e7c2 root commit +``` + +## From MoonBit + +The same flow scripted against the library. `Node::from_uri` opens a peer session; `fetch` walks the graph diff. + +```moonbit +let peer = @bit.Node::from_uri("bit://aurora.local:9418/repo") +peer.handshake(timeout=2.s)? + +let local_refs = @bit.refs(fs, ".git") +let wants = local_refs.diff(peer.refs()) +peer.fetch(wants)? + +println("now at: \{peer.refs().get(\"refs/heads/main\")}") +``` diff --git a/site/content/learn/first-commit.md b/site/content/learn/first-commit.md new file mode 100644 index 0000000..41645a0 --- /dev/null +++ b/site/content/learn/first-commit.md @@ -0,0 +1,91 @@ +--- +title: Your first commit +section: learn +slug: first-commit +order: 2 +nav_label: Your first commit +summary: Initialize a repo, write a file, stage it, commit it, read the log. Git muscle memory works — bit is a drop-in. +meta: ~5 min +kicker: // guide · 02 +h1: Your first commit. +lead: If you have ever used Git, you already know this. bit reuses the porcelain — `init`, `add`, `commit`, `log` all behave the way you expect. The first commit is a sanity check that the binary works. +prev_href: install.html +prev_kicker: prev · 01 +prev_label: Install bit +next_href: distributed.html +next_kicker: next · 03 +next_label: Going distributed +--- + +## Initialize a repository + +```bash +$ mkdir hello-bit +$ cd hello-bit +$ bit init +Initialized empty bit repository in .git/ +``` + +bit writes a standard `.git/` directory. Any existing Git tool can read it. There is no parallel `.bit/` store. + +## Write and stage + +```bash +$ echo "hello, peer" > note.txt +$ bit add note.txt +$ bit status +On branch main + +Changes to be committed: + new file: note.txt +``` + +
+

// note

+

The status output is intentionally identical to Git's. bit emits the same line shapes so your editor's plugins, your hooks, and your grep incantations keep working.

+
+ +## Commit + +```bash +$ bit commit -m "first node speaks" +[main (root-commit) a8c2f6e] first node speaks + 1 file changed, 1 insertion(+) + create mode 100644 note.txt +``` + +## Read the log + +```bash +$ bit log --graph --oneline +* a8c2f6e (HEAD -> main) first node speaks +``` + +The graph renderer is native — `--graph`, `--stat`, `--name-only`, `--name-status`, and `--topo-order` all run inside bit without shelling out. + +## What just happened + +bit wrote three Git objects under `.git/objects/`: + +- a **blob** for `note.txt` (the file contents) +- a **tree** describing the directory snapshot +- a **commit** pointing at the tree, with your author identity and message + +These are the same object types every Git client speaks. Anything you commit with bit can be pushed to a Git server, and anything cloned from a Git server can be operated on by bit. + +## Try it from MoonBit + +The same flow, scripted against an in-memory filesystem — no shell involved. This is how agents drive bit. + +```moonbit +let fs = @bit.TestFs::new() +let root = "/hello-bit" + +run_storage_command(fs, fs, root, "init", ["-q"]) +fs.write_string(root + "/note.txt", "hello, peer") +run_storage_command(fs, fs, root, "add", ["note.txt"]) +run_storage_command(fs, fs, root, "commit", ["-m", "first node speaks"]) + +let head = run_storage_command(fs, fs, root, "rev-parse", ["HEAD"]) +println("commit: \{head}") +``` diff --git a/site/content/learn/index.md b/site/content/learn/index.md new file mode 100644 index 0000000..fd59b84 --- /dev/null +++ b/site/content/learn/index.md @@ -0,0 +1,11 @@ +--- +title: Learning Guide +section: learn +slug: index +order: 0 +template: index +hero_tag: est. 35 min — six chapters +kicker: // guide +h1: Learning Guide. +lead: A guided path through bit. By the end you will understand the model, install the binary, make commits against a real repository, replicate history to a second peer, and open a local pull request — all without leaving your terminal. +--- diff --git a/site/content/learn/install.md b/site/content/learn/install.md new file mode 100644 index 0000000..5079619 --- /dev/null +++ b/site/content/learn/install.md @@ -0,0 +1,84 @@ +--- +title: Install bit +section: learn +slug: install +order: 1 +nav_label: Install bit +summary: One-line install on macOS & Linux, MoonBit toolchain, shell completion. The 30-second on-ramp. +meta: ~5 min +kicker: // guide · 01 +h1: Install bit. +lead: Two install paths — a single curl line, or via the MoonBit toolchain if you already have one. Both produce a `bit` binary on PATH. +prev_href: ./ +prev_kicker: back +prev_label: Learning Guide +next_href: first-commit.html +next_kicker: next · 02 +next_label: Your first commit +--- + +## Supported platforms + +- Linux x86_64 +- macOS arm64 & x86_64 + +Windows is not supported yet. WSL works. + +## One-line install + +```bash +$ curl -fsSL https://raw.githubusercontent.com/mizchi/bit-vcs/main/install.sh | bash +``` + +The script detects your OS and architecture, downloads the matching release, and drops the binary at `~/.local/bin/bit`. Add it to your shell's `PATH` if it isn't already. + +
+

// note

+

Read the script before you pipe it to bash — that's the rule for every install script on the internet, not just this one. Mirror at github.com/mizchi/bit-vcs.

+
+ +## Via the MoonBit toolchain + +If you have `moon` installed: + +```bash +$ moon install mizchi/bit/cmd/bit +``` + +This builds bit from source against your toolchain. Useful if you want to track main or contribute patches. + +## Verify + +```bash +$ bit --version +bit 0.1.0 — distributed git in moonbit +``` + +## Shell completion + +bit ships its own completion generator. Pick your shell and source the output from your rc file. + +### bash + +```bash +eval "$(bit completion bash)" +``` + +### zsh + +```bash +eval "$(bit completion zsh)" +``` + +## Uninstall + +```bash +$ rm ~/.local/bin/bit +``` + +That's it. bit has no daemons, no system services, no cache outside your repos. + +
+

// caveat

+

bit is experimental. Data corruption is possible in worst-case scenarios. Always keep a Git backup of important repositories until you have stress-tested your workflow.

+
diff --git a/site/content/reference/cli.md b/site/content/reference/cli.md new file mode 100644 index 0000000..f4264a5 --- /dev/null +++ b/site/content/reference/cli.md @@ -0,0 +1,124 @@ +--- +title: CLI commands +section: reference +slug: cli +order: 1 +nav_label: CLI commands +summary: Every subcommand grouped by surface — porcelain, plumbing, hub, relay, extensions, AI. Flags, exit codes, examples. +meta: 108 commands +kicker: // reference · R1 +h1: CLI commands. +lead: bit implements 108 Git commands natively. Anything not listed here falls back to Git's surface — read [going distributed](../learn/distributed.html) for the protocol-level differences. +prev_href: ./ +prev_kicker: back +prev_label: Reference +next_href: library.html +next_kicker: next · R2 +next_label: Library API +--- + +## Porcelain + +Everyday commands. Identical surface to Git. + +| Command | Description | +| --- | --- | +| `bit init` | Create an empty `.git/` in the current directory. | +| `bit clone ` | Clone over HTTP, relay, or a subdirectory URL (see extensions). | +| `bit add …` | Stage files. Supports `-A`, `-p`, `--update`. | +| `bit commit -m ` | Record staged changes. `--amend`, `--no-edit`, `--allow-empty`. | +| `bit status` | Working tree state in Git's exact line format. | +| `bit log` | Native graph renderer. `--graph`, `--stat`, `--name-only`, `--topo-order`. | +| `bit diff` | Worktree vs index, or any two trees / commits. | +| `bit checkout` | Switch branches, restore files. `-b` creates. | +| `bit branch` | List, create, delete branches. | +| `bit merge` | Three-way merge with conflict markers. | +| `bit rebase` | Native `-i` with editor injection. | +| `bit cherry-pick ` | Apply commits onto HEAD. | +| `bit revert ` | Reverse a commit. | +| `bit fetch` | Download refs and objects from a remote. | +| `bit pull` | Fetch and merge / rebase. | +| `bit push` | Upload refs and objects. HTTPS, relay, LFS upload. | +| `bit remote` | Manage remotes. | +| `bit tag` | Lightweight and annotated tags. | +| `bit stash` | Shelve worktree state. | +| `bit reset` | `--soft`, `--mixed`, `--hard`. | + +## Plumbing + +Low-level operations on Git objects. Same surface as Git's plumbing. + +| Command | Description | +| --- | --- | +| `bit hash-object` | Compute the SHA-1 of a file (optionally write to objects). | +| `bit cat-file` | Print object type, size, or contents. | +| `bit rev-parse` | Resolve refs and revisions to SHAs. | +| `bit ls-tree` | List tree contents. | +| `bit ls-files` | List files in the index. | +| `bit update-ref` | Move a ref atomically. | +| `bit pack-objects` | Build a packfile. | +| `bit unpack-objects` | Explode a packfile into loose objects. | +| `bit symbolic-ref` | Read or write a symbolic ref like `HEAD`. | + +## Hub — PRs and issues + +Local, server-less collaboration backed by Git objects. Sync via relay. + +| Command | Description | +| --- | --- | +| `bit pr init` | Initialize hub metadata in the current repo (writes `.git/hub/policy.toml`). | +| `bit pr create` | `--title`, `--body`, `--head`, `--base`. Opens a local PR. | +| `bit pr list` | `--open`, `--closed`, `--all`. | +| `bit pr review ` | `--approve`, `--request-changes`, `--commit `. | +| `bit pr merge ` | Merge into base. Honors policy. | +| `bit pr search ` | Full-text over PR titles and bodies. | +| `bit issue create` | `--title`, `--body`, `--parent `. | +| `bit issue list` | `--open`, `--tree`, `--all`, `--parent`. | +| `bit issue link ` | Cross-link issue ↔ PR. | +| `bit issue search ` | Full-text over issues. | + +## Relay + +Sync PR and issue metadata between peers via an HTTP relay. + +| Command | Description | +| --- | --- | +| `bit relay sync push ` | Upload hub notes to a relay. | +| `bit relay sync fetch ` | Download hub notes from a relay. | +| `bit relay serve` | Run a relay host locally. Supports LFS Batch API. | + +## Extensions + +| Command | Description | +| --- | --- | +| `bit subdir-clone ` | Clone a subdirectory as an independent repo. | +| `bit clone user/repo:path` | Shorthand for subdir-clone via the standard clone command. | +| `bit hq get ` | `ghq`-compatible repo manager. Default root: `~/bhq`. | +| `bit hq list` | List all repos under hq. | +| `bit workspace flow ` | Workspace fingerprint-based task runner. | +| `bit fingerprint` | Workspace / PR fingerprint helpers. | +| `bit completion ` | Emit shell completion script. `bash` or `zsh`. | + +## AI assist + +AI-assisted conflict resolution via OpenRouter. Default model: `moonshotai/kimi-k2`. Requires `OPENROUTER_API_KEY`. + +| Command | Description | +| --- | --- | +| `bit ai rebase ` | Rebase with AI resolving conflicts. `--continue`, `--abort`, `--skip`. | +| `bit ai merge ` | Merge with AI assist. | +| `bit ai commit` | AI commit message (`--split` for multi-commit suggestion). | +| `bit ai cherry-pick ` | Cherry-pick with AI conflict resolution. | +| `bit ai revert ` | Revert with AI. | + +### Common AI flags + +- `--model ` — override the OpenRouter model. +- `--max-ai-rounds ` — bound conflict-resolution turns. +- `--agent-loop` — give the AI an agent loop, not single-shot. +- `--agent-max-steps ` — bound the agent loop. + +
+

// see also

+

The Library API exposes the same surface to MoonBit code — drive any of the above from an agent without spawning a process.

+
diff --git a/site/content/reference/env.md b/site/content/reference/env.md new file mode 100644 index 0000000..2c2be5b --- /dev/null +++ b/site/content/reference/env.md @@ -0,0 +1,53 @@ +--- +title: Environment +section: reference +slug: env +order: 3 +nav_label: Environment +summary: Every `BIT_*` variable bit reads, and the Git-compat variables it honors (`GIT_EDITOR`, `GIT_CONFIG_GLOBAL`, …). +meta: 12 vars +kicker: // reference · R3 +h1: Environment variables. +lead: Every `BIT_*` variable bit reads, plus the Git-compat variables it honors. Set them in your shell rc, in `.envrc`, or per-command. +prev_href: library.html +prev_kicker: prev · R2 +prev_label: Library API +next_href: ../ +next_kicker: end +next_label: Back to home +--- + +## bit-native + +| Variable | Default | Effect | +| --- | --- | --- | +| `BIT_BENCH_GIT_DIR` | — | Override `.git` path for `bench_real` (vfs benchmarks). | +| `BIT_PACK_CACHE_LIMIT` | `2` | Max packfiles kept in memory. `0` disables the cache. | +| `BIT_RACY_GIT` | unset | When set, rehash even if `stat` matches — avoids racy-git false negatives on fast filesystems. | +| `BIT_WORKSPACE_FINGERPRINT_MODE` | `git` | `git` for add-all-style snapshots, `fast` for per-node directory hashes. | +| `BIT_HUB_INIT_PROMPT` | `1` (interactive) | Force-on (`1`) or force-off (`0`) the `bit pr init` / `bit issue init` prompts. | + +## Git-compat + +bit honors the standard Git environment so existing tooling, hooks, and editors keep working. + +| Variable | Effect | +| --- | --- | +| `GIT_DIR` | Override the location of the repository's `.git` directory. | +| `GIT_WORK_TREE` | Override the working tree root. | +| `GIT_CONFIG_GLOBAL` | Path to global config (default `~/.gitconfig`). | +| `GIT_EDITOR` | Editor used for commit messages, rebase conflict markers. | +| `GIT_SEQUENCE_EDITOR` | Editor used for the rebase todo list. | +| `GIT_AUTHOR_NAME` / `GIT_AUTHOR_EMAIL` | Override commit author identity. | +| `GIT_COMMITTER_NAME` / `GIT_COMMITTER_EMAIL` | Override committer identity. | + +## AI assist + +| Variable | Effect | +| --- | --- | +| `OPENROUTER_API_KEY` | Required for any `bit ai *` subcommand. | + +
+

// note

+

bit never reads secrets from the repository — keys live only in the environment. If you set OPENROUTER_API_KEY via .envrc, make sure that file is git-ignored.

+
diff --git a/site/content/reference/index.md b/site/content/reference/index.md new file mode 100644 index 0000000..3708816 --- /dev/null +++ b/site/content/reference/index.md @@ -0,0 +1,11 @@ +--- +title: Reference +section: reference +slug: index +order: 0 +template: index +hero_tag: complete · authoritative +kicker: // reference +h1: Reference. +lead: Everything bit exposes — every command, every public type, every environment variable. Sorted, terse, link-anchored. Reach for this when you know what you are looking for. +--- diff --git a/site/content/reference/library.md b/site/content/reference/library.md new file mode 100644 index 0000000..89fa3dc --- /dev/null +++ b/site/content/reference/library.md @@ -0,0 +1,178 @@ +--- +title: Library API +section: reference +slug: library +order: 2 +nav_label: Library API +summary: MoonBit types and functions for embedding bit in your own programs — agents, CI runners, editor plugins. +meta: moonbit +kicker: // reference · R2 +h1: Library API. +lead: bit is a MoonBit library first, a CLI second. Every CLI command is a thin shell around a public function. Embed bit directly in your agent, editor plugin, or CI runner — no subprocess required. +prev_href: cli.html +prev_kicker: prev · R1 +prev_label: CLI commands +next_href: env.html +next_kicker: next · R3 +next_label: Environment +--- + +## Package layout + +Source is organized in five layers, dependencies flow downward only. + +| Layer | Path | What lives here | +| --- | --- | --- | +| core | `src/core/` | Object types, SHA, ref store primitives. | +| mid | `src/mid/` | Index, pack, merge, diff algorithms. | +| high | `src/high/` | Porcelain operations (commit, rebase, …). | +| ext | `src/ext/` | Hub, relay, LFS, AI, workspace. | +| cmd | `src/cmd/` | CLI entry points. | + +## Storage runtime + +The entry point for embedding. Any storage that implements `FileSystem` and `RepoFileSystem` can host a repository. + +### Traits + +```moonbit +pub trait FileSystem { + write_string(Self, String, String) -> Unit + write_bytes(Self, String, Bytes) -> Unit + mkdir_p(Self, String) -> Unit + remove(Self, String) -> Unit + rename(Self, String, String) -> Unit +} + +pub trait RepoFileSystem { + read_string(Self, String) -> String? + read_bytes(Self, String) -> Bytes? + exists(Self, String) -> Bool + readdir(Self, String) -> Array[String] + stat(Self, String) -> FileStat? +} +``` + +### Entry function + +```moonbit +pub fn run_storage_command[F : FileSystem + RepoFileSystem]( + fs : F, + rfs : F, + root : String, + cmd : String, + args : Array[String] +) -> String +``` + +### In-memory backend + +`TestFs` is the reference implementation. Use it for agents, tests, and ephemeral repos. + +```moonbit +let fs = @bit.TestFs::new() +let root = "/agent-repo" + +run_storage_command(fs, fs, root, "init", ["-q"]) +fs.write_string(root + "/note.txt", "hello, peer") +run_storage_command(fs, fs, root, "add", ["note.txt"]) +run_storage_command(fs, fs, root, "commit", ["-m", "agent snapshot"]) + +let head = run_storage_command(fs, fs, root, "rev-parse", ["HEAD"]) +println("head: \{head}") +``` + +## Fs — virtual filesystem + +Mount any commit as a read-only filesystem with lazy blob loading. + +```moonbit +let mounted = @bit.Fs::from_commit(fs, ".git", commit_id) +let entries = mounted.readdir(fs, "src") +let content = mounted.read_file(fs, "src/main.mbt")? + +for name in entries { + println("- \{name}") +} +``` + +| Method | Description | +| --- | --- | +| `Fs::from_commit(fs, gitdir, sha)` | Open a virtual FS rooted at a commit's tree. | +| `Fs::from_tree(fs, gitdir, sha)` | Open from a tree object directly. | +| `readdir(self, fs, path)` | List entries in a directory. | +| `read_file(self, fs, path)` | Read a blob lazily. | +| `stat(self, fs, path)` | Get entry metadata (mode, size). | + +## Kv — distributed KV store + +Git-backed key/value store with gossip sync. Useful when you want a small, replicated config alongside the repository. + +```moonbit +let db = @bit.Kv::init(fs, fs, git_dir, node_id="aurora.local") + +db.set(fs, fs, "users/alice/profile", profile_bytes, ts=now()) +let value = db.get(fs, fs, "users/alice/profile")? + +db.sync_with_peer(fs, fs, "relay://bit.example.com/u/me/kv") +``` + +## Hub — PR / Issue API + +Same data the `bit pr` and `bit issue` CLI commands operate on. Drive PRs from an agent, write a custom triage UI, or wire it into your editor. + +```moonbit +let hub = @bit.Hub::init(fs, fs, git_dir) + +let pr = hub.create_pr( + fs, fs, + title = "Replace gossip backoff with token bucket", + body = "fixes thrashing under high churn", + source_branch = "feature/token-bucket", + target_branch = "main", + author = "ubugeeei", + ts = now() +) + +hub.review(fs, fs, pr.id, status=Approve, commit=Some(pr.head))? +hub.merge(fs, fs, pr.id)? +``` + +| Function | Returns | +| --- | --- | +| `Hub::init(fs, rfs, gitdir)` | `Hub` | +| `hub.create_pr(...)` | `Pr` | +| `hub.list_prs(state)` | `Array[Pr]` | +| `hub.review(id, status, commit?)` | `Result[Review, Error]` | +| `hub.merge(id)` | `Result[Sha, Error]` | +| `hub.create_issue(...)` | `Issue` | +| `hub.link(issue_id, pr_id)` | `Unit` | + +## Node — peer protocol + +Open sessions with other peers, exchange refs, fetch object graphs. + +```moonbit +let peer = @bit.Node::from_uri("bit://aurora.local:9418/repo") +peer.handshake(timeout=2.s)? + +let local_refs = @bit.refs(fs, ".git") +let wants = local_refs.diff(peer.refs()) +peer.fetch(wants)? + +println("now at: \{peer.refs().get(\"refs/heads/main\")}") +``` + +| Function | Description | +| --- | --- | +| `Node::from_uri(uri)` | Create a peer session (lazy connect). | +| `peer.handshake(timeout?)` | Open the session, exchange capabilities. | +| `peer.refs()` | Snapshot of remote refs. | +| `peer.fetch(wants)` | Download objects reachable from `wants`. | +| `peer.send(bit)` | Push a single object or pack. | +| `peer.close()` | Drop the session. | + +
+

// see also

+

The CLI Reference documents the same surface, packaged as commands. The mapping is one-to-one: bit pr createHub::create_pr, bit fetchNode::fetch, and so on.

+
diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000..7ed4d9e --- /dev/null +++ b/site/index.html @@ -0,0 +1,151 @@ + + + + + +bit — distributed Git in MoonBit + + + + + + + +
+
+ bit + v0.1 — 2026 +
+ + +
+ + +
+
+ bit / 2026 + distributed source control · written in moonbit +
+ +
+
+

// 001 — a manifesto in three lines

+

A node.
A source.
A bt.

+

bit is a Git implementation in MoonBit. Every clone is a peer. Every commit is a bit of evidence. The graph converges through gossip, not consensus.

+ + + +
+ $ curl -fsSL https://raw.githubusercontent.com/mizchi/bit-vcs/main/install.sh | bash + +
+
+ +
+ +
+
+
+ + +
+
+
// 002
+

Three principles, never bend.

+
+ +
+
+

— principle 01

+

No center.

+

No central server. No master. Every node holds the full history and can become the source. Peers converge by gossip, asymmetric on purpose.

+ +
+ +
+

— principle 02

+

No service.

+

PRs, issues, and reviews live as Git objects in the repo. bit pr and bit issue work offline, sync through relays, and never depend on GitHub.

+ +
+ +
+

— principle 03

+

No magic.

+

Written in MoonBit. Compiles to native and JS. The MoonBit API is the runtime — agents and CI can drive bit storage directly without going through a CLI.

+ +
+
+
+ + +
+
+
// 003
+

Quick start.

+
+ +
+
// shell
+
$ bit clone https://github.com/user/repo
+$ bit checkout -b feature
+$ bit add .
+$ bit commit -m "first node speaks"
+$ bit push origin feature
+
+ +

That's Git. Now flip on the distributed layer:

+ +
+
// shell
+
$ bit pr init
+$ bit pr create --title "first node speaks" --head feature --base main
+$ bit relay sync push https://relay.example.com/u/me/repo
+
+
+ + +
+
+

// 004 — manifesto

+

no center.
no master.
only peers.

+
+
+ + +
+
+
// 005
+

Speaks MoonBit, natively.

+
+ +

bit's core operations run against any storage backend that implements @bit.FileSystem. Agents can drive a repository entirely in memory — no shell, no subprocess.

+ +
+
// moonbit
+
// spin up an in-memory repo and write a commit
+let fs = @bit.TestFs::new()
+let root = "/agent-repo"
+run_storage_command(fs, fs, root, "init", ["-q"])
+fs.write_string(root + "/note.txt", "hello, peer")
+run_storage_command(fs, fs, root, "add", ["note.txt"])
+run_storage_command(fs, fs, root, "commit", ["-m", "agent snapshot"])
+
+
+ + + + + + + diff --git a/site/learn/concept.html b/site/learn/concept.html new file mode 100644 index 0000000..13d05af --- /dev/null +++ b/site/learn/concept.html @@ -0,0 +1,151 @@ + + + + + +Concept + + + + + +
+
+ bit + docs · v0.1 +
+ + +
+ +
+ + + +
+
+

// guide · 00 — the idea

+

What bit is.

+

bit is a Git implementation in MoonBit. It speaks the Git wire protocol, it produces a normal .git/, and any Git client can read what it writes. Underneath, bit treats distribution as the default — not a feature bolted onto a central server, but the entire premise.

+
+ +

What's interesting

+

Three things distinguish bit from "yet another Git client."

+

PRs and issues live inside the repo. Pull requests, reviews, and issues are written as Git objects under refs/notes/bit-hub. Replicate the repo and you replicate the work. GitHub can go away. Your project history — and the conversations around it — survive intact.

+

bit is a library before it is a tool. Every CLI command is a thin shell over a MoonBit function. Drop bit into an agent, an editor plugin, or a CI runner — you do not shell out, you call it. Mount an in-memory filesystem and you have a working repository in five lines.

+

Convergence is gossip, not consensus. Peers reconcile by walking object graphs until they meet a common ancestor. No election, no quorum, no leader. A laptop offline for a week catches up the moment it touches a peer.

+

A five-concept mental model

+

These five words carry the whole system. If you have them straight, the CLI and the library both stop surprising you.

+

Node

+

A node is any process holding a copy of the repository. Your laptop is a node. A CI runner is a node. A relay host is a node. Nodes are symmetric — there is no "primary" — and they are addressed by URI:

+
// text
bit://aurora.local:9418/repo
+relay://bit.example.com/u/mizchi/bit
+https://github.com/mizchi/bit.git
+
+

Source

+

Source is whoever wrote the commit you are looking at. It is a fact, not a place. The pink dot in the bit logo represents the source node; in any given moment, any node can become it.

+
// moonbit
let commit = @bit.head(fs, ".git")?
+println("author: \{commit.author}")
+println("source: \{commit.committer.tz}@\{commit.committer.email}")
+
+

Bit

+

A bit is a single edge between two peers — one unit of exchange. Communication is content-addressed: peers ask for object hashes, never filenames. The protocol is therefore naturally idempotent and naturally deduplicating.

+

Convergence

+

Two peers handshake, exchange ref tips, and walk the graph backwards until they meet. Bytes flow in both directions. Disagreement persists as parallel heads until a human merges them. There is no server-side reconciliation step. There is no server.

+

Hub

+

PRs and issues are stored as Git objects. The hub is not a service — it is a particular shape of commit graph. bit pr create writes objects. bit relay sync push replicates them. The hub survives forks because it has no "outside" to survive against.

+

What follows from this

+

Implications you can feel as soon as you start using it.

+
    +
  • +

    The hub survives forks. Fork a repo, get every PR and issue with it.

    +
  • +
  • +

    Agents drive bit without a shell. The same MoonBit functions the CLI calls, your agent can call directly.

    +
  • +
  • +

    You can clone a single subdirectory of a 50 GB monorepo with bit clone user/repo:src/lib.

    +
  • +
  • +

    A merge conflict resolved by AI is just commits plus a note — no third-party state, no external dashboard.

    +
  • +
  • +

    You can write your own bit-aware tool in MoonBit in an afternoon. The library API is the surface.

    +
  • +
+

What bit is not

+

Equally important — bit refuses several adjacent identities.

+
    +
  • +

    Not a Git replacement. It is Git. The bytes on disk are valid Git, the wire protocol is Git's, the SHA-1 history is interchangeable. Use it next to vanilla git if you want.

    +
  • +
  • +

    Not a blockchain. No consensus, no tokens, no proof-of-anything. Convergence is gossip.

    +
  • +
  • +

    Not a P2P file share. bit is collaboration software that happens to be distributed. The unit of meaning is a commit, not a file.

    +
  • +
  • +

    Not an LLM agent. AI assist is one optional command (bit ai rebase), not the architecture.

    +
  • +
+

What's next

+

Now that you have the model, the rest of the guide is mostly muscle memory.

+ + + + +
+ + + +
+ + + + + + + diff --git a/site/learn/distributed.html b/site/learn/distributed.html new file mode 100644 index 0000000..1ff7945 --- /dev/null +++ b/site/learn/distributed.html @@ -0,0 +1,175 @@ + + + + + +Going distributed + + + + + +
+
+ bit + docs · v0.1 +
+ + +
+ +
+ + + +
+
+

// guide · 03

+

Going distributed.

+

bit was built on the premise that source control should not depend on a central host. In this chapter you will clone a repository over HTTP and over a bit relay, push commits in both directions, and watch two peers converge through gossip.

+
+ +

Mental model

+

A bit node is any process holding a copy of the repository. Your laptop, a CI runner, a relay host — all peers, no master. Two peers reconcile by exchanging ref tips and walking the object graph until they meet a common ancestor.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ModeTriggerDirection
pushexplicitlocal → remote
fetchexplicitremote → local
gossipperiodic, on handshakeboth
relay syncvia relay hostboth, async
+

Clone over HTTP

+

This is the standard Git path. bit speaks git-upload-pack and git-receive-pack over smart HTTP.

+
// bash
$ bit clone https://github.com/user/repo
+$ cd repo
+$ bit remote -v
+origin  https://github.com/user/repo (fetch)
+origin  https://github.com/user/repo (push)
+
+

Clone over a relay

+

A relay is a lightweight bit-aware host. It speaks the bit protocol over HTTP and supports async sync — useful when peers are not online at the same time.

+
// bash
$ bit clone relay://bit.example.com/u/me/repo
+$ cd repo
+
+

Two peers, one truth

+

Open two terminals. In the first, simulate node A:

+
// bash
$ bit clone relay://bit.example.com/u/me/repo a
+$ cd a
+$ echo "from A" >> note.txt
+$ bit add . && bit commit -m "A writes"
+$ bit push origin main
+
+

In the second terminal, node B:

+
// bash
$ bit clone relay://bit.example.com/u/me/repo b
+$ cd b
+$ bit fetch origin
+$ bit log --oneline
+a8c2f6e (origin/main) A writes
+1d0e7c2 root commit
+
+

B now holds A's history without ever talking to A directly. The relay carried the bytes asynchronously.

+

Concurrent writes

+

Two peers writing at the same time produce two heads. bit's history graph is content-addressed, so collisions are deterministic — both heads are valid until a merge or rebase reconciles them. There is no server vote.

+
+

// recommendation

+

Use bit ai rebase when the conflict is mechanical (formatting drift, import re-orderings). For semantic conflicts, do the merge by hand — the AI assist is wrong often enough that you want to read what it proposes.

+
+

Inspect the graph

+
// bash
$ bit log --graph --oneline --all
+*   1f02776 (HEAD -> main) merge: reconcile A and B
+|\
+| * e871d71 B writes
+* | a8c2f6e A writes
+|/
+* 1d0e7c2 root commit
+
+

From MoonBit

+

The same flow scripted against the library. Node::from_uri opens a peer session; fetch walks the graph diff.

+
// moonbit
let peer = @bit.Node::from_uri("bit://aurora.local:9418/repo")
+peer.handshake(timeout=2.s)?
+
+let local_refs = @bit.refs(fs, ".git")
+let wants      = local_refs.diff(peer.refs())
+peer.fetch(wants)?
+
+println("now at: \{peer.refs().get(\"refs/heads/main\")}")
+
+ + + +
+ + + +
+ + + + + + + diff --git a/site/learn/first-commit.html b/site/learn/first-commit.html new file mode 100644 index 0000000..af3cbcf --- /dev/null +++ b/site/learn/first-commit.html @@ -0,0 +1,143 @@ + + + + + +Your first commit + + + + + +
+
+ bit + docs · v0.1 +
+ + +
+ +
+ + + +
+
+

// guide · 02

+

Your first commit.

+

If you have ever used Git, you already know this. bit reuses the porcelain — init, add, commit, log all behave the way you expect. The first commit is a sanity check that the binary works.

+
+ +

Initialize a repository

+
// bash
$ mkdir hello-bit
+$ cd hello-bit
+$ bit init
+Initialized empty bit repository in .git/
+
+

bit writes a standard .git/ directory. Any existing Git tool can read it. There is no parallel .bit/ store.

+

Write and stage

+
// bash
$ echo "hello, peer" > note.txt
+$ bit add note.txt
+$ bit status
+On branch main
+
+Changes to be committed:
+        new file:   note.txt
+
+
+

// note

+

The status output is intentionally identical to Git's. bit emits the same line shapes so your editor's plugins, your hooks, and your grep incantations keep working.

+
+

Commit

+
// bash
$ bit commit -m "first node speaks"
+[main (root-commit) a8c2f6e] first node speaks
+ 1 file changed, 1 insertion(+)
+ create mode 100644 note.txt
+
+

Read the log

+
// bash
$ bit log --graph --oneline
+* a8c2f6e (HEAD -> main) first node speaks
+
+

The graph renderer is native — --graph, --stat, --name-only, --name-status, and --topo-order all run inside bit without shelling out.

+

What just happened

+

bit wrote three Git objects under .git/objects/:

+
    +
  • +

    a blob for note.txt (the file contents)

    +
  • +
  • +

    a tree describing the directory snapshot

    +
  • +
  • +

    a commit pointing at the tree, with your author identity and message

    +
  • +
+

These are the same object types every Git client speaks. Anything you commit with bit can be pushed to a Git server, and anything cloned from a Git server can be operated on by bit.

+

Try it from MoonBit

+

The same flow, scripted against an in-memory filesystem — no shell involved. This is how agents drive bit.

+
// moonbit
let fs = @bit.TestFs::new()
+let root = "/hello-bit"
+
+run_storage_command(fs, fs, root, "init", ["-q"])
+fs.write_string(root + "/note.txt", "hello, peer")
+run_storage_command(fs, fs, root, "add", ["note.txt"])
+run_storage_command(fs, fs, root, "commit", ["-m", "first node speaks"])
+
+let head = run_storage_command(fs, fs, root, "rev-parse", ["HEAD"])
+println("commit: \{head}")
+
+ + + +
+ + + +
+ + + + + + + diff --git a/site/learn/index.html b/site/learn/index.html new file mode 100644 index 0000000..540451b --- /dev/null +++ b/site/learn/index.html @@ -0,0 +1,83 @@ + + + + + +Learning Guide + + + + + +
+
+ bit + docs · v0.1 +
+ + +
+ +
+
+ bit / learn + est. 35 min — six chapters +
+ +

// guide

+

Learning Guide.

+

A guided path through bit. By the end you will understand the model, install the binary, make commits against a real repository, replicate history to a second peer, and open a local pull request — all without leaving your terminal.

+
+ +
+ +
+ + + + + + + + + diff --git a/site/learn/install.html b/site/learn/install.html new file mode 100644 index 0000000..03deabf --- /dev/null +++ b/site/learn/install.html @@ -0,0 +1,128 @@ + + + + + +Install bit + + + + + +
+
+ bit + docs · v0.1 +
+ + +
+ +
+ + + +
+
+

// guide · 01

+

Install bit.

+

Two install paths — a single curl line, or via the MoonBit toolchain if you already have one. Both produce a bit binary on PATH.

+
+ +

Supported platforms

+
    +
  • +

    Linux x86_64

    +
  • +
  • +

    macOS arm64 & x86_64

    +
  • +
+

Windows is not supported yet. WSL works.

+

One-line install

+
// bash
$ curl -fsSL https://raw.githubusercontent.com/mizchi/bit-vcs/main/install.sh | bash
+
+

The script detects your OS and architecture, downloads the matching release, and drops the binary at ~/.local/bin/bit. Add it to your shell's PATH if it isn't already.

+
+

// note

+

Read the script before you pipe it to bash — that's the rule for every install script on the internet, not just this one. Mirror at github.com/mizchi/bit-vcs.

+
+

Via the MoonBit toolchain

+

If you have moon installed:

+
// bash
$ moon install mizchi/bit/cmd/bit
+
+

This builds bit from source against your toolchain. Useful if you want to track main or contribute patches.

+

Verify

+
// bash
$ bit --version
+bit 0.1.0 — distributed git in moonbit
+
+

Shell completion

+

bit ships its own completion generator. Pick your shell and source the output from your rc file.

+

bash

+
// bash
eval "$(bit completion bash)"
+
+

zsh

+
// bash
eval "$(bit completion zsh)"
+
+

Uninstall

+
// bash
$ rm ~/.local/bin/bit
+
+

That's it. bit has no daemons, no system services, no cache outside your repos.

+
+

// caveat

+

bit is experimental. Data corruption is possible in worst-case scenarios. Always keep a Git backup of important repositories until you have stress-tested your workflow.

+
+ + + +
+ + + +
+ + + + + + + diff --git a/site/reference/cli.html b/site/reference/cli.html new file mode 100644 index 0000000..b06619b --- /dev/null +++ b/site/reference/cli.html @@ -0,0 +1,390 @@ + + + + + +CLI commands + + + + + +
+
+ bit + docs · v0.1 +
+ + +
+ +
+ + + +
+
+

// reference · R1

+

CLI commands.

+

bit implements 108 Git commands natively. Anything not listed here falls back to Git's surface — read going distributed for the protocol-level differences.

+
+ +

Porcelain

+

Everyday commands. Identical surface to Git.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CommandDescription
bit initCreate an empty .git/ in the current directory.
bit clone <url>Clone over HTTP, relay, or a subdirectory URL (see extensions).
bit add <path>…Stage files. Supports -A, -p, --update.
bit commit -m <msg>Record staged changes. --amend, --no-edit, --allow-empty.
bit statusWorking tree state in Git's exact line format.
bit logNative graph renderer. --graph, --stat, --name-only, --topo-order.
bit diffWorktree vs index, or any two trees / commits.
bit checkoutSwitch branches, restore files. -b creates.
bit branchList, create, delete branches.
bit mergeThree-way merge with conflict markers.
bit rebaseNative -i with editor injection.
bit cherry-pick <sha>Apply commits onto HEAD.
bit revert <sha>Reverse a commit.
bit fetchDownload refs and objects from a remote.
bit pullFetch and merge / rebase.
bit pushUpload refs and objects. HTTPS, relay, LFS upload.
bit remoteManage remotes.
bit tagLightweight and annotated tags.
bit stashShelve worktree state.
bit reset--soft, --mixed, --hard.
+

Plumbing

+

Low-level operations on Git objects. Same surface as Git's plumbing.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CommandDescription
bit hash-objectCompute the SHA-1 of a file (optionally write to objects).
bit cat-filePrint object type, size, or contents.
bit rev-parseResolve refs and revisions to SHAs.
bit ls-treeList tree contents.
bit ls-filesList files in the index.
bit update-refMove a ref atomically.
bit pack-objectsBuild a packfile.
bit unpack-objectsExplode a packfile into loose objects.
bit symbolic-refRead or write a symbolic ref like HEAD.
+

Hub — PRs and issues

+

Local, server-less collaboration backed by Git objects. Sync via relay.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CommandDescription
bit pr initInitialize hub metadata in the current repo (writes .git/hub/policy.toml).
bit pr create--title, --body, --head, --base. Opens a local PR.
bit pr list--open, --closed, --all.
bit pr review <id>--approve, --request-changes, --commit <sha>.
bit pr merge <id>Merge into base. Honors policy.
bit pr search <q>Full-text over PR titles and bodies.
bit issue create--title, --body, --parent <id>.
bit issue list--open, --tree, --all, --parent.
bit issue link <issue> <pr>Cross-link issue ↔ PR.
bit issue search <q>Full-text over issues.
+

Relay

+

Sync PR and issue metadata between peers via an HTTP relay.

+ + + + + + + + + + + + + + + + + + + + + +
CommandDescription
bit relay sync push <url>Upload hub notes to a relay.
bit relay sync fetch <url>Download hub notes from a relay.
bit relay serveRun a relay host locally. Supports LFS Batch API.
+

Extensions

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CommandDescription
bit subdir-clone <url> <path> <dst>Clone a subdirectory as an independent repo.
bit clone user/repo:pathShorthand for subdir-clone via the standard clone command.
bit hq get <user/repo>ghq-compatible repo manager. Default root: ~/bhq.
bit hq listList all repos under hq.
bit workspace flow <task>Workspace fingerprint-based task runner.
bit fingerprintWorkspace / PR fingerprint helpers.
bit completion <shell>Emit shell completion script. bash or zsh.
+

AI assist

+

AI-assisted conflict resolution via OpenRouter. Default model: moonshotai/kimi-k2. Requires OPENROUTER_API_KEY.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CommandDescription
bit ai rebase <base>Rebase with AI resolving conflicts. --continue, --abort, --skip.
bit ai merge <branch>Merge with AI assist.
bit ai commitAI commit message (--split for multi-commit suggestion).
bit ai cherry-pick <sha>Cherry-pick with AI conflict resolution.
bit ai revert <sha>Revert with AI.
+

Common AI flags

+
    +
  • +

    --model <id> — override the OpenRouter model.

    +
  • +
  • +

    --max-ai-rounds <n> — bound conflict-resolution turns.

    +
  • +
  • +

    --agent-loop — give the AI an agent loop, not single-shot.

    +
  • +
  • +

    --agent-max-steps <n> — bound the agent loop.

    +
  • +
+
+

// see also

+

The Library API exposes the same surface to MoonBit code — drive any of the above from an agent without spawning a process.

+
+ + + +
+ + + +
+ + + + + + + diff --git a/site/reference/env.html b/site/reference/env.html new file mode 100644 index 0000000..613af91 --- /dev/null +++ b/site/reference/env.html @@ -0,0 +1,177 @@ + + + + + +Environment + + + + + +
+
+ bit + docs · v0.1 +
+ + +
+ +
+ + + +
+
+

// reference · R3

+

Environment variables.

+

Every BIT_* variable bit reads, plus the Git-compat variables it honors. Set them in your shell rc, in .envrc, or per-command.

+
+ +

bit-native

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariableDefaultEffect
BIT_BENCH_GIT_DIROverride .git path for bench_real (vfs benchmarks).
BIT_PACK_CACHE_LIMIT2Max packfiles kept in memory. 0 disables the cache.
BIT_RACY_GITunsetWhen set, rehash even if stat matches — avoids racy-git false negatives on fast filesystems.
BIT_WORKSPACE_FINGERPRINT_MODEgitgit for add-all-style snapshots, fast for per-node directory hashes.
BIT_HUB_INIT_PROMPT1 (interactive)Force-on (1) or force-off (0) the bit pr init / bit issue init prompts.
+

Git-compat

+

bit honors the standard Git environment so existing tooling, hooks, and editors keep working.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariableEffect
GIT_DIROverride the location of the repository's .git directory.
GIT_WORK_TREEOverride the working tree root.
GIT_CONFIG_GLOBALPath to global config (default ~/.gitconfig).
GIT_EDITOREditor used for commit messages, rebase conflict markers.
GIT_SEQUENCE_EDITOREditor used for the rebase todo list.
GIT_AUTHOR_NAME / GIT_AUTHOR_EMAILOverride commit author identity.
GIT_COMMITTER_NAME / GIT_COMMITTER_EMAILOverride committer identity.
+

AI assist

+ + + + + + + + + + + + + +
VariableEffect
OPENROUTER_API_KEYRequired for any bit ai * subcommand.
+
+

// note

+

bit never reads secrets from the repository — keys live only in the environment. If you set OPENROUTER_API_KEY via .envrc, make sure that file is git-ignored.

+
+ + + +
+ + + +
+ + + + + + + diff --git a/site/reference/index.html b/site/reference/index.html new file mode 100644 index 0000000..0109cfe --- /dev/null +++ b/site/reference/index.html @@ -0,0 +1,75 @@ + + + + + +Reference + + + + + +
+
+ bit + docs · v0.1 +
+ + +
+ +
+
+ bit / reference + complete · authoritative +
+ +

// reference

+

Reference.

+

Everything bit exposes — every command, every public type, every environment variable. Sorted, terse, link-anchored. Reach for this when you know what you are looking for.

+
+ +
+ +
+ + + + + + + + + diff --git a/site/reference/library.html b/site/reference/library.html new file mode 100644 index 0000000..9e58f98 --- /dev/null +++ b/site/reference/library.html @@ -0,0 +1,316 @@ + + + + + +Library API + + + + + +
+
+ bit + docs · v0.1 +
+ + +
+ +
+ + + +
+
+

// reference · R2

+

Library API.

+

bit is a MoonBit library first, a CLI second. Every CLI command is a thin shell around a public function. Embed bit directly in your agent, editor plugin, or CI runner — no subprocess required.

+
+ +

Package layout

+

Source is organized in five layers, dependencies flow downward only.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
LayerPathWhat lives here
coresrc/core/Object types, SHA, ref store primitives.
midsrc/mid/Index, pack, merge, diff algorithms.
highsrc/high/Porcelain operations (commit, rebase, …).
extsrc/ext/Hub, relay, LFS, AI, workspace.
cmdsrc/cmd/CLI entry points.
+

Storage runtime

+

The entry point for embedding. Any storage that implements FileSystem and RepoFileSystem can host a repository.

+

Traits

+
// moonbit
pub trait FileSystem {
+  write_string(Self, String, String) -> Unit
+  write_bytes(Self, String, Bytes) -> Unit
+  mkdir_p(Self, String) -> Unit
+  remove(Self, String) -> Unit
+  rename(Self, String, String) -> Unit
+}
+
+pub trait RepoFileSystem {
+  read_string(Self, String) -> String?
+  read_bytes(Self, String) -> Bytes?
+  exists(Self, String) -> Bool
+  readdir(Self, String) -> Array[String]
+  stat(Self, String) -> FileStat?
+}
+
+

Entry function

+
// moonbit
pub fn run_storage_command[F : FileSystem + RepoFileSystem](
+  fs   : F,
+  rfs  : F,
+  root : String,
+  cmd  : String,
+  args : Array[String]
+) -> String
+
+

In-memory backend

+

TestFs is the reference implementation. Use it for agents, tests, and ephemeral repos.

+
// moonbit
let fs = @bit.TestFs::new()
+let root = "/agent-repo"
+
+run_storage_command(fs, fs, root, "init", ["-q"])
+fs.write_string(root + "/note.txt", "hello, peer")
+run_storage_command(fs, fs, root, "add", ["note.txt"])
+run_storage_command(fs, fs, root, "commit", ["-m", "agent snapshot"])
+
+let head = run_storage_command(fs, fs, root, "rev-parse", ["HEAD"])
+println("head: \{head}")
+
+

Fs — virtual filesystem

+

Mount any commit as a read-only filesystem with lazy blob loading.

+
// moonbit
let mounted = @bit.Fs::from_commit(fs, ".git", commit_id)
+let entries = mounted.readdir(fs, "src")
+let content = mounted.read_file(fs, "src/main.mbt")?
+
+for name in entries {
+  println("- \{name}")
+}
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodDescription
Fs::from_commit(fs, gitdir, sha)Open a virtual FS rooted at a commit's tree.
Fs::from_tree(fs, gitdir, sha)Open from a tree object directly.
readdir(self, fs, path)List entries in a directory.
read_file(self, fs, path)Read a blob lazily.
stat(self, fs, path)Get entry metadata (mode, size).
+

Kv — distributed KV store

+

Git-backed key/value store with gossip sync. Useful when you want a small, replicated config alongside the repository.

+
// moonbit
let db = @bit.Kv::init(fs, fs, git_dir, node_id="aurora.local")
+
+db.set(fs, fs, "users/alice/profile", profile_bytes, ts=now())
+let value = db.get(fs, fs, "users/alice/profile")?
+
+db.sync_with_peer(fs, fs, "relay://bit.example.com/u/me/kv")
+
+

Hub — PR / Issue API

+

Same data the bit pr and bit issue CLI commands operate on. Drive PRs from an agent, write a custom triage UI, or wire it into your editor.

+
// moonbit
let hub = @bit.Hub::init(fs, fs, git_dir)
+
+let pr = hub.create_pr(
+  fs, fs,
+  title          = "Replace gossip backoff with token bucket",
+  body           = "fixes thrashing under high churn",
+  source_branch  = "feature/token-bucket",
+  target_branch  = "main",
+  author         = "ubugeeei",
+  ts             = now()
+)
+
+hub.review(fs, fs, pr.id, status=Approve, commit=Some(pr.head))?
+hub.merge(fs, fs, pr.id)?
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FunctionReturns
Hub::init(fs, rfs, gitdir)Hub
hub.create_pr(...)Pr
hub.list_prs(state)Array[Pr]
hub.review(id, status, commit?)Result[Review, Error]
hub.merge(id)Result[Sha, Error]
hub.create_issue(...)Issue
hub.link(issue_id, pr_id)Unit
+

Node — peer protocol

+

Open sessions with other peers, exchange refs, fetch object graphs.

+
// moonbit
let peer = @bit.Node::from_uri("bit://aurora.local:9418/repo")
+peer.handshake(timeout=2.s)?
+
+let local_refs = @bit.refs(fs, ".git")
+let wants      = local_refs.diff(peer.refs())
+peer.fetch(wants)?
+
+println("now at: \{peer.refs().get(\"refs/heads/main\")}")
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FunctionDescription
Node::from_uri(uri)Create a peer session (lazy connect).
peer.handshake(timeout?)Open the session, exchange capabilities.
peer.refs()Snapshot of remote refs.
peer.fetch(wants)Download objects reachable from wants.
peer.send(bit)Push a single object or pack.
peer.close()Drop the session.
+
+

// see also

+

The CLI Reference documents the same surface, packaged as commands. The mapping is one-to-one: bit pr createHub::create_pr, bit fetchNode::fetch, and so on.

+
+ + + +
+ + + +
+ + + + + + + diff --git a/tools/build-docs.mjs b/tools/build-docs.mjs new file mode 100644 index 0000000..bd43902 --- /dev/null +++ b/tools/build-docs.mjs @@ -0,0 +1,412 @@ +#!/usr/bin/env node +// tools/build-docs.mjs +// Reads site/content/**/*.md, renders bodies via mizchi/markdown (via the +// MoonBit→JS module under tools/docs-build-mbt/), and writes site/
/*.html +// using inline templates that carry the bit brand layout. +// +// Frontmatter format: a leading `---` / `---` fenced block of flat key:value +// pairs (no nesting; structure encoded in named fields like prev_href / next_href). +// +// Usage: +// 1. (once) build the renderer: +// cd tools/docs-build-mbt && moon build --target js --release +// 2. node tools/build-docs.mjs + +import fs from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const root = path.resolve(here, ".."); +const contentDir = path.join(root, "site/content"); +const outDir = path.join(root, "site"); +const rendererPath = path.join( + root, + "tools/docs-build-mbt/_build/js/release/build/render/render.js", +); + +// ── load renderer ──────────────────────────────────────────────────────── +const { render } = await import(rendererPath).catch((err) => { + console.error( + `\nFailed to load MoonBit renderer at:\n ${rendererPath}\n\n` + + `Build it first:\n cd tools/docs-build-mbt && moon build --target js --release\n`, + ); + throw err; +}); + +// ── frontmatter parser ──────────────────────────────────────────────────── +function parseFrontmatter(src) { + const m = src.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/); + if (!m) return { meta: {}, body: src }; + const meta = {}; + for (const raw of m[1].split(/\r?\n/)) { + const line = raw.replace(/^\s+|\s+$/g, ""); + if (!line || line.startsWith("#")) continue; + const kv = line.match(/^([A-Za-z_][\w-]*)\s*:\s*(.*)$/); + if (!kv) continue; + let [, k, v] = kv; + v = v.replace(/^["']|["']$/g, ""); + if (/^-?\d+$/.test(v)) meta[k] = Number(v); + else if (v === "true" || v === "false") meta[k] = v === "true"; + else meta[k] = v; + } + return { meta, body: m[2] }; +} + +// ── walk content tree ──────────────────────────────────────────────────── +async function walk(dir) { + const out = []; + for (const ent of await fs.readdir(dir, { withFileTypes: true })) { + const p = path.join(dir, ent.name); + if (ent.isDirectory()) out.push(...(await walk(p))); + else if (ent.isFile() && ent.name.endsWith(".md")) out.push(p); + } + return out; +} + +// ── HTML post-processing ───────────────────────────────────────────────── +function slugify(s) { + return s + .toLowerCase() + .replace(/<[^>]+>/g, "") + .replace(/[^\p{Letter}\p{Number}]+/gu, "-") + .replace(/^-|-$/g, ""); +} + +function transformHeadings(html) { + // Number h2s "// 001" and add id + data-num. Leave h3/h4 alone. + let i = 1; + const out = html.replace( + /]*)?>([\s\S]*?)<\/h2>/g, + (_, inner) => { + const id = slugify(inner) || `s-${i}`; + const num = `// ${String(i).padStart(3, "0")}`; + i++; + return `

${inner}

`; + }, + ); + return out; +} + +function transformCodeBlocks(html) { + // mizchi/markdown emits
. + // Wrap in our .codeblock structure and hand the language off via data-lang + // so site/assets/syntax.js can pick it up. + return html.replace( + /
([\s\S]*?)<\/code><\/pre>/g,
+    (_, lang, code) => {
+      const langLabel = `// ${lang || "txt"}`;
+      const dataLang = lang || "";
+      return (
+        `
` + + `
${langLabel}` + + `
` + + `
${code}
` + + `
` + ); + }, + ); +} + +// Render a short string of markdown as inline HTML (no

wrapper). +// Useful for frontmatter fields like `lead` where we want `code` and **bold** +// to render but the result is dropped into an inline context. +function renderInline(md) { + if (!md) return ""; + const html = render(md).trim(); + return html + .replace(/^

/, "") + .replace(/<\/p>$/, "") + .replace(/<\/p>\s*

/g, "

"); +} + +function extractToc(html) { + const items = []; + const re = /([\s\S]*?)<\/h2>/g; + let m; + while ((m = re.exec(html)) !== null) { + items.push({ id: m[1], num: m[2], label: m[3].replace(/<[^>]+>/g, "").trim() }); + } + return items; +} + +// ── sidenav generation ─────────────────────────────────────────────────── +function sidenavFor(pages, section, activeSlug) { + const sectionPages = pages + .filter( + (p) => + p.meta.section === section && + p.meta.slug !== "index" && + p.meta.template !== "landing", + ) + .sort((a, b) => (a.meta.order ?? 99) - (b.meta.order ?? 99)); + + const items = sectionPages.map((p) => { + const href = `${p.meta.slug}.html`; + const num = + p.meta.order != null ? String(p.meta.order).padStart(2, "0") : ""; + const label = num ? `${num} ${p.meta.nav_label || p.meta.title}` : p.meta.title; + const active = p.meta.slug === activeSlug ? ' class="active"' : ""; + return `

  • ${label}
  • `; + }); + + const otherSection = section === "learn" ? "reference" : "learn"; + const otherLink = + section === "learn" + ? '
  • Reference
  • ' + : '
  • Learning Guide
  • '; + + return `

    // ${section === "learn" ? "Learning Guide" : "Reference"}

    +
      +${items.join("\n")} +
    +

    // More

    +
      +${otherLink} +
    • Home
    • +
    `; +} + +// ── chapter list for index pages ───────────────────────────────────────── +function chapterListFor(pages, section) { + const sectionPages = pages + .filter( + (p) => + p.meta.section === section && + p.meta.slug !== "index" && + p.meta.template !== "landing", + ) + .sort((a, b) => (a.meta.order ?? 99) - (b.meta.order ?? 99)); + + const items = sectionPages.map((p) => { + const href = `${p.meta.slug}.html`; + const num = String(p.meta.order ?? 0).padStart(2, "0"); + const tag = section === "learn" ? num : `R${p.meta.order ?? "?"}`; + return ` +
    // ${tag}
    +
    +

    ${p.meta.nav_label || p.meta.title}

    +

    ${renderInline(p.meta.summary || "")}

    +
    +
    ${p.meta.meta || ""}
    +
    `; + }); + + return ` `; +} + +// ── templates ──────────────────────────────────────────────────────────── +function pageHead({ title, description, rootPath }) { + return ` + + + + +${title}${ + description + ? `\n` + : "" + } + + + +`; +} + +function topbar({ rootPath, activeSection }) { + const cls = (s) => (activeSection === s ? ' class="active"' : ""); + return `
    +
    + bit + docs · v0.1 +
    + + +
    `; +} + +function footer({ rootPath, sectionLabel }) { + return ``; +} + +function tocAside(items) { + if (!items.length) return ""; + const lis = items + .map( + (it) => + `
  • ${it.num} ${it.label}
  • `, + ) + .join("\n"); + return ` `; +} + +function pager(meta) { + if (!meta.prev_href && !meta.next_href) return ""; + const left = meta.prev_href + ? ` + ← ${meta.prev_kicker || "back"} + ${meta.prev_label || ""} + ` + : ` `; + const right = meta.next_href + ? ` ` + : ` `; + return ``; +} + +function chapterPageTemplate({ + meta, + rootPath, + sidenav, + body, + toc, + description, +}) { + return `${pageHead({ title: meta.title, description, rootPath })} + +${topbar({ rootPath, activeSection: meta.section })} + +
    + + + +
    +
    +

    ${meta.kicker || ""}

    +

    ${meta.h1 || meta.title}

    ${ + meta.lead ? `\n

    ${renderInline(meta.lead)}

    ` : "" + } +
    + +${body} + +${pager(meta)} +
    + +${toc} + +
    + +${footer({ rootPath, sectionLabel: `${meta.section} · ${meta.slug}` })} + + + + + +`; +} + +function indexPageTemplate({ meta, rootPath, body, chapterList, description }) { + return `${pageHead({ title: meta.title, description, rootPath })} + +${topbar({ rootPath, activeSection: meta.section })} + +
    +
    + bit / ${meta.section} + ${meta.hero_tag || ""} +
    + +

    ${meta.kicker || ""}

    +

    ${meta.h1 || meta.title}

    ${ + meta.lead + ? `\n

    ${renderInline(meta.lead)}

    ` + : "" + } +
    + +
    +${chapterList} +
    + +${body ? `
    \n${body}\n
    \n` : ""} + +${footer({ rootPath, sectionLabel: meta.section })} + + + + + +`; +} + +// ── per-page renderer ──────────────────────────────────────────────────── +function renderPage(page, pages) { + const isIndex = page.meta.slug === "index" || page.meta.template === "index"; + const rootPath = "../"; // all rendered pages live under site/
    / + + const rawHtml = render(page.body || ""); + const body = transformCodeBlocks(transformHeadings(rawHtml)); + const toc = isIndex ? "" : tocAside(extractToc(body)); + const sidenav = sidenavFor(pages, page.meta.section, page.meta.slug); + + if (isIndex) { + return indexPageTemplate({ + meta: page.meta, + rootPath, + body, + chapterList: chapterListFor(pages, page.meta.section), + description: page.meta.description, + }); + } + return chapterPageTemplate({ + meta: page.meta, + rootPath, + sidenav, + body, + toc, + description: page.meta.description, + }); +} + +// ── main ──────────────────────────────────────────────────────────────── +const files = await walk(contentDir); +const pages = await Promise.all( + files.map(async (f) => { + const src = await fs.readFile(f, "utf8"); + const { meta, body } = parseFrontmatter(src); + const rel = path.relative(contentDir, f).replace(/\\/g, "/"); // posix + // Section/slug derived from path if not given in frontmatter + if (!meta.section || !meta.slug) { + const parts = rel.replace(/\.md$/, "").split("/"); + meta.section ||= parts[0]; + meta.slug ||= parts.slice(1).join("/") || "index"; + } + return { file: f, meta, body }; + }), +); + +let wrote = 0; +for (const page of pages) { + const html = renderPage(page, pages); + const slug = page.meta.slug === "index" ? "index" : page.meta.slug; + const out = path.join(outDir, page.meta.section, `${slug}.html`); + await fs.mkdir(path.dirname(out), { recursive: true }); + await fs.writeFile(out, html); + wrote++; + console.log(` ${path.relative(root, out)}`); +} + +console.log(`\nbuilt ${wrote} page${wrote === 1 ? "" : "s"}.`); diff --git a/tools/docs-build-mbt/.gitignore b/tools/docs-build-mbt/.gitignore new file mode 100644 index 0000000..338b48b --- /dev/null +++ b/tools/docs-build-mbt/.gitignore @@ -0,0 +1,3 @@ +_build/ +.mooncakes/ +target/ diff --git a/tools/docs-build-mbt/README.md b/tools/docs-build-mbt/README.md new file mode 100644 index 0000000..a403a5b --- /dev/null +++ b/tools/docs-build-mbt/README.md @@ -0,0 +1,15 @@ +# bit docs-build (MoonBit) + +A tiny MoonBit project that wraps [`mizchi/markdown`](https://github.com/mizchi/markdown.mbt) +into a single ESM-friendly `render(source) -> html` function. The `tools/build-docs.mjs` +Node script imports the compiled JS output, walks `site/content/**/*.md`, and writes +`site/learn/*.html` + `site/reference/*.html`. + +## Build + +```sh +moon update +moon build --target js --release +``` + +Output: `_build/js/release/build/render/render.js` exporting `{ render }`. diff --git a/tools/docs-build-mbt/moon.mod.json b/tools/docs-build-mbt/moon.mod.json new file mode 100644 index 0000000..4e97ca3 --- /dev/null +++ b/tools/docs-build-mbt/moon.mod.json @@ -0,0 +1,12 @@ +{ + "name": "mizchi/bit-docs-build", + "version": "0.0.1", + "deps": { + "mizchi/markdown": "0.4.9" + }, + "readme": "README.md", + "license": "Apache-2.0", + "description": "Build-time markdown renderer for the bit docs site. Wraps mizchi/markdown as a JS module consumed by tools/build-docs.mjs.", + "source": "src", + "preferred-target": "js" +} diff --git a/tools/docs-build-mbt/src/render/moon.pkg b/tools/docs-build-mbt/src/render/moon.pkg new file mode 100644 index 0000000..2131ecf --- /dev/null +++ b/tools/docs-build-mbt/src/render/moon.pkg @@ -0,0 +1,14 @@ +import { + "mizchi/markdown" @markdown, +} + +options( + link: { + "js": { + "exports": [ + "render:render", + ], + "format": "esm", + }, + }, +) diff --git a/tools/docs-build-mbt/src/render/render.mbt b/tools/docs-build-mbt/src/render/render.mbt new file mode 100644 index 0000000..641ba97 --- /dev/null +++ b/tools/docs-build-mbt/src/render/render.mbt @@ -0,0 +1,8 @@ +///| Render a markdown source string to HTML. +/// +/// Thin wrapper over `mizchi/markdown`'s convenience `md_to_html`. We keep +/// bare URLs as autolinks (the default) and leave wikilinks off — the docs +/// site uses standard markdown link syntax everywhere. +pub fn render(source : String) -> String { + @markdown.md_to_html(source) +}