Compare commits

...
31 Commits
Author SHA1 Message Date
dsql 9fb5067124 docs: drop the SQLite alternative from the Redis sidecar section
Redis-only now. The persist-or-not decision stays ephemeral (--save "")
vs persistent (--appendonly yes + ${MOUNTS_DIR}), and both caveats — the
~1s appendfsync everysec window and the not-backed-up dividing line —
are unchanged.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-07-14 15:55:16 -04:00
dsql a248879451 docs: document Redis as a per-repo compose sidecar
New Deploy section 'Supporting services live in your compose, not the
fleet': Redis is declared per-repo, never shared fleet-wide — a shared
instance would re-couple services the per-project network deliberately
isolates.

- ephemeral vs persistent as a conscious choice, both compose snippets in
  content tabs (--save "" + no volume, vs --appendonly yes + ${MOUNTS_DIR})
- connect by compose service name (redis://redis:6379), never a host port
- caveats given weight: appendfsync everysec ~1s loss window, and mounts
  are not backed up — the dividing line between queue state and Postgres
- SQLite noted as the no-container alternative for a single drainer
- the don'ts, plus one line on why ACLs aren't the answer here
- cross-links the redis lib on the Libraries page

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-07-14 15:52:55 -04:00
dsql b3acd57e32 docs: use dedicated purple lambda favicon
Split favicon off from the logo — new docs/assets/favicon.svg (purple
gradient lambda), mkdocs favicon key repointed to it; logo unchanged.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-07-10 21:45:52 -04:00
dsql 6151cb2e4e docs: add paste service to Workflow; clarify app-log vs stdout in Deploy
- Workflow: paste.rethinkstudios.io pastebin section — pb() function
  (recommended) + alias alternative in content tabs, usage, expires note,
  creation rate-limit warning.
- Deploy: warning admonition distinguishing ${LOGS_DIR} (app logger, to
  disk) from container stdout/stderr where startup crashes and uncaught
  exceptions land; sys.excepthook snippet to route them into the log file.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-07-09 22:13:59 -04:00
dsql 350884c2b3 deploy: whole-flow as plain text, restore v2 diagram shape; admonition variety
- 'The whole flow' is now plain prose, not an abstract box.
- Restore the v2 diagram shape (git -> staging via develop-if-gitflow AND
  git -> deployment, converging on docker) instead of the linear main-only
  one, keeping the simplified 'libs (rethink-public, pip)' wording.
- Spread admonition types for variety across pages: index agent-callout
  tip -> example; environments uv.lock note -> info; standards logging
  note -> info. (deploy already mixes success/danger/example/warning/tip.)

Verified in-browser: whole-flow reads as text, diagram renders in the v2
shape; mkdocs build --strict clean.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-07-01 03:04:51 -04:00
dsql 5797248931 deploy: move diagram into Preparing section, vary admonitions, simplify
- Move the flow diagram up into 'Preparing for Deploy' (was a separate
  section at the bottom); drop that trailing section.
- Use more admonition types for visual variety: success (what can run),
  abstract (the whole flow), example (container-path), success (same file
  both places) — alongside the existing danger/warning/tip.
- Simplify the diagram: libs -> 'libs (rethink-public, pip)', the git edge
  -> 'branch', staging -> 'main'; abstract intro reworded to branch+main.
- Trim the Compose tab: drop the rules list that duplicated the inline
  comments + danger box; tighten the storage tiers.

Verified in-browser: diagram renders with simplified nodes near the top,
six admonition types present; mkdocs build --strict clean.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-07-01 02:58:22 -04:00
dsql 823aa7708a deploy: mermaid flow diagram, single page with tabs, section rework
Collapse the deploy sub-pages back into one deploy.md (tabs, not separate
pages), add a working mermaid flow diagram, and rework the top per feedback.

- Mermaid: enable via superfences fence_div_format (the fence_code_format
  <pre><code> wrapper broke mermaid's render — div format fixes it); vendor
  mermaid.min.js locally (no CDN dependency for a self-hosted site) + a small
  init that renders each block once and survives instant-nav.
- Content tabs Compose / Dockerfile / Secrets hold the three pieces together
  on one page (inline comments in the code, no annotation +).
- 'What can run here' tip moved above the flow; section renamed
  'Preparing for Deploy' with the 'you don't run deploy commands' point folded
  sparsely into the intro (standalone note removed); 'svc never changes'
  called out.
- Flow diagram (below the tabs): build group now includes our libraries; git
  -> staging shown as develop-if-gitflow (dotted); deployment (no MR-to-main)
  -> docker image built + run on the fleet.
- Revert inbound links to deploy.md.

Verified in-browser: diagram renders with all nodes, tabs switch, section
order correct; mkdocs build --strict clean.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-07-01 02:53:02 -04:00
dsql f9e5e4f24d split Deploy into a hub + 3 sub-pages; inline comments over annotations
The Deploy page got long and the code annotations rendered inconsistently
(some as (n) numbers, some as clickable +, and a marker on a fully-commented
line made that line vanish). Fix both:

- Deploy is now a HUB (docs/deploy/index.md): the intro + collision rule +
  central-deploy note, then card links to three focused sub-pages. Deploy
  stays the single global-nav entry; sub-pages are not_in_nav, reached from
  the hub cards.
  - deploy/compose.md — compose convention, storage tiers, how deploy fills
    it in, subprocess/browser knobs, checklist
  - deploy/dockerfile.md — services-account image, COPY, layer caching, uv
  - deploy/secrets.md — keeping secrets out of the image
- Replace code annotations with INLINE COMMENTS on the compose/Dockerfile
  examples: everything visible at once, no + to click, and the commented
  MOUNTS_DIR line no longer disappears.
- Update inbound links (index card, standards, workflow, environments) to
  deploy/ and deploy/compose.md; nav Deploy -> deploy/index.md with
  not_in_nav for the sub-pages.

Verified in-browser: hub cards link correctly, sub-pages render with visible
inline comments (0 annotation markers), left nav shows only Deploy;
mkdocs build --strict clean (validates not_in_nav + all cross-links).

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-07-01 02:32:16 -04:00
dsql c335887dd6 deploy: fix vanishing commented mounts line in compose annotations
A '# (6)!' annotation marker on a fully-commented YAML line made Material's
annotation JS swallow the whole line into the tooltip — so the commented
'# - ${MOUNTS_DIR...}' line disappeared, leaving an orphan marker with no
visible code (which is why '(6)' seemed missing).

Fix: drop the marker from the commented line, keep it as plain visible YAML
('# optional — see below'), and move its explanation to prose below the
annotation list. Renumber cache/volume markers 7->6, 8->7.

Verified in-browser: commented mounts line now renders visibly; no orphan
marker. build --strict clean.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-07-01 02:22:46 -04:00
dsql fe707d5f24 deploy: clarify container path = where your code reads/writes
Make the volume mapping explicit: the right-hand mount path is where the
app writes. WORKDIR is /app, so code writing to ./cache lands in /app/cache
(this mount); same for /app/logs and /app/config. Removes ambiguity about
which side of the volume line the code targets.

Verified: mkdocs build --strict clean.
Signed-off-by: disqualifier <dev@disqualifier.me>
2026-07-01 02:14:50 -04:00
dsql 0ebd6f86b0 deploy: scope page to the dev's job — drop operator commands
Devs never deploy — that's a central ops tool. Remove operator-facing
content and keep only what a dev is responsible for (a conforming compose).

- remove the 'Onboarding a service' section (deploy <host> <workspace>
  <git-url> command + deploy-key internals) and the 'How deploy fills it in'
  docker-ps collision table + <workspace>/<name> naming internals
- replace with 'Deployment is handled centrally': a note that devs run no
  deploy commands, plus the kept dev-facing context that the env is
  GENERATED at deploy time so ${LOGS_DIR}/${CONFIG_DIR}/${MOUNTS_DIR}
  resolve without them (less ambiguous than just 'it's handled')
- trim operator leftovers: svc-<ws>-<name> unit ref, dangling '(below)',
  and the /srv/config/<ws>/<name> path in the secrets note

Compose convention, uid 1337, restart, mounts tier, layer caching, uv,
subprocess knobs all unchanged. Verified in-browser; build --strict clean.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-07-01 02:04:17 -04:00
dsql 08ad37eddc deploy: re-add mounts as optional (commented) third storage tier
The deploy scripts already handle mounts (dir creation + MOUNTS_DIR
injection); only the docs had dropped it. Re-add it as opt-in so scripts
and docs agree:

- compose: a commented '# - ${MOUNTS_DIR:-./mounts}:/app/data' line — the
  dir is auto-created and MOUNTS_DIR injected, but the mount line stays
  dev-opted (container-side path is app-specific, can't be auto-mounted)
- reframe the storage tip as 'Three tiers of storage': logs+config (ours),
  mounts (optional, injected + auto-created, mount line opt-in), named
  volumes (yours)
- re-add MOUNTS_DIR to the injected env vars (noted opt-in) and mention it
  in the local-fallback tip, paths line, and checklist as optional

Verified in-browser; mkdocs build --strict clean.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-07-01 01:39:52 -04:00
dsql 8c56fd98a8 deploy: add COPY-into-image tabs, clarify volume ownership, drop mounts
- deploy.md: new 'Getting your files into the image' subsection — a
  multi-tab annotated block (single file / directory / deps-first) showing
  COPY <src> <dest> patterns, plus a warning not to COPY secrets.
- clarify volume ownership: 'Logs and config are ours; the rest is yours' —
  we inject/manage the logs + config host mounts; devs put everything else
  in named volumes Docker owns.
- remove all /srv/mounts: dropped the MOUNTS_DIR volume + env var and every
  mounts reference from the compose block, injected-vars, paths line, and
  checklist. environments.md + index.md mounts mentions cleaned too.

Verified in-browser: COPY tabs switch, volume tip renders, zero mounts refs
remain; mkdocs build --strict clean.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-07-01 01:36:25 -04:00
dsql 7ed388c69c new compose + deploy standard: generic compose, deploy injects identity
Supersedes the prior 'name everything after the repo' guidance, which caused
container-name collisions at fleet scale. The dev's compose now names nothing
repo-specific; the deploy layer injects identity and host paths.

deploy.md:
- replace the 'One name everywhere' danger callout with 'Your compose names
  nothing repo-specific' (no container_name, no hardcoded names/paths)
- rewrite the compose example to the standard: svc service key,
  restart: unless-stopped (required — host unit is oneshot), host paths via
  ${LOGS_DIR}/${CONFIG_DIR}/${MOUNTS_DIR} with :-./ local fallbacks, bare
  'cache' volume. Keep user: "1337:1337" + HOME=/tmp (the services account
  identity — not repo-specific)
- add 'How deploy fills it in' (injected env vars a dev can rely on + the
  resulting docker ps names) and 'Onboarding a service' (deploy <host>
  <workspace> <git-url> [name], deploy key handled via Gitea API)
- update paths to <name>, subprocess example to svc, checklist to the new
  rules; drop docker CLI from secret rotation (host-side edit, we restart)

standards.md:
- add a 'Service compose' section: short convention + Right/Wrong tabs
  (the nova before/after), linking to the Deploy guide for the full detail

Kept the services-account (1337) section and uid-agnostic Dockerfile notes.
Verified in-browser; mkdocs build --strict clean (cross-ref anchor resolves).

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-07-01 01:04:44 -04:00
dsql 17b2888c1f deploy: red callout — repo name must match container + volume names
Add a prominent danger admonition ('One name everywhere — repo = container =
volumes') right under the intro: the repo name is the name; the compose
service key, container_name, and every named volume must all use it. Spell
out the consequence — mismatched names break deploy, monitoring, and log
scraping, which key off the repo name.

Make the compose example teach it: rename yourapp -> myrepo throughout
(service key, container_name: myrepo, myrepo-data volume) with annotations
calling out each spot where the repo name must appear.

Verified in-browser; mkdocs build --strict clean.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-30 23:13:35 -04:00
dsql c53d67da2f add uv as a parallel option alongside pip
uv is presented as the recommended faster, standards-compliant drop-in;
pip stays the baseline/fallback. uv command syntax verified against
docs.astral.sh/uv before writing.

environments.md:
- new 'uv (optional, faster)' section: install from pyproject
  (uv pip install . / -e . / '.[dev]'), the uv sync managed-venv flow,
  and a note that uv.lock is local-only/gitignored (not committed)
- pinning subsection: pin direct deps in pyproject via == or git @ref,
  with a warning that this pins direct deps only — transitive deps still
  float at build time (documented tradeoff)
- uv equivalents added beside pip in the Local .venv and Docker tabs

deploy.md:
- 'Faster builds with uv' tip: uv-from-ghcr COPY, ENV UV_COMPILE_BYTECODE=1,
  uv pip install --system . (reads pyproject, no lock)
- layer-caching shows the uv variant beside the pip one
- checklist notes uv pip install + UV_COMPILE_BYTECODE; fix stray 'configs'
  plural -> 'config'

.gitignore: ignore uv.lock (local-only, never committed).

Verified in-browser; mkdocs build --strict clean (anchors resolve).

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-30 04:40:10 -04:00
dsql dafc1dcacd fix path: /srv/configs -> /srv/config (singular); fix broken anchor
- deploy.md: /srv/configs -> /srv/config (singular is canonical) across the
  compose mount, paths table, secrets path, and rotation snippet. Add the
  repo+compose row (/srv/docker/<workspace>/<project>, created by the clone,
  not pre-provisioned) and a note that all /srv paths are owned by the
  services user (1337) — rounding out the canonical layout.
- workflow.md: the first 'per-project git identity' link pointed at a
  nonexistent #per-project-git-identity anchor; point it at #handy-shell-setup
  (where the gitsetup alias lives), matching the other link to the same spot.

Did not touch pip/requirements, chmod a+rwX + user 1337, HOME=/tmp, init,
layer caching, or the git-install caveat — all intentional standards.

Verified: mkdocs build --strict clean (validates anchors); table renders.
Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-30 00:57:24 -04:00
dsql 6498c5b6d5 rename <dev> path segment to <workspace>, unify /srv paths
Host paths now follow one shape: /srv/<kind>/<workspace>/<project>/ across
configs, logs, and mounts (mounts added as its own row; configs gains the
workspace segment it was missing).

- deploy.md: compose volumes, paths table, secrets path + rotation snippet
  all use <workspace>; add a yellow (warning) admonition ABOVE the table
  explaining it — individual dev = lowercase username (ricky, xattam),
  shared/official project = workspace (bots, web, apis) — so it stands out
  from the all-blue palette and reads before the table.
- index.md: placeholder example uses <workspace>.

Verified in-browser; mkdocs build --strict clean.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 23:50:42 -04:00
dsql 5f91d73e1f agent callout: read from git direct, don't clone into the project
The 'from git' tab now tells the agent to read docs/ straight from the
repo URL instead of git-cloning it into the working project — clone to
/tmp only if it needs a local copy.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 21:30:35 -04:00
dsql 75ad61b06a add 'point your coding agent here' callout on the landing page
A tip admonition under the intro telling devs to point their coding agent
at the handbook for awareness of our libs/standards/deploy rules, with
copy-pasteable agent prompts in two tabs: from the live site, or from a
git clone of the repo.

Verified in-browser: tabs switch, prompts + clone command render, copy
buttons present; mkdocs build --strict clean.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 21:30:07 -04:00
dsql ed4e558dca tone down advertisey copy
- index.md: 'building and shipping with Rethink Studios' -> 'building and
  shipping on our network' (it's the network, not the company)
- workflow.md + its index card: 'how we actually work day to day' ->
  'get hands on with how we dev'
- deploy.md: drop the 'see the light / ready to host whatever you've built'
  pitch for a plain 'how a project gets onto rethink-net'; trim the now-
  duplicate eligibility tip

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 21:23:34 -04:00
dsql 78e3c34d8d add README
Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 20:57:45 -04:00
dsql 2e631136c6 pin build deps in requirements.txt for native host build
The handbook is served natively (no Docker) on the Gitea box: a python
venv runs mkdocs build, and the reverse proxy serves the static site/ at
docs.rethinkstudios.io. Pin the build deps so the host venv reproduces the
build exactly:

  pip install -r requirements.txt && mkdocs build

Verified the host rebuild flow from scratch: fresh venv + clean
pip install -r requirements.txt + mkdocs build --strict builds all 6 pages
with no errors. Static output uses relative URLs and site_url is the docs
subdomain, so it serves correctly behind the proxy at the subdomain root.
site/ and .venv/ stay gitignored.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 20:56:50 -04:00
dsql 613d8f28ea add Virtual environments page (project isolation + pyenv)
New docs/environments.md, nav 'Virtual environments' (before Deploy):
- the rule: never touch system Python (danger callout)
- project-based isolation as Local .venv / Makefile-driven / Docker tabs,
  each with a runnable snippet; Docker ties back to the deploy standard
- local dev with pyenv: why, official install link, shell init with
  annotated lines, everyday use, per-project .python-version
- shell quality-of-life extras (flake8 alias, .local/bin + npm-global PATH),
  cross-ref'd to Standards and Workflow

workflow.md pyenv bullet now points at the new page; index.md gains a card.

Verified in-browser: tabs switch (Local/Makefile/Docker), annotations and
admonitions render; mkdocs build --strict clean (cross-ref anchors resolve).

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 20:54:31 -04:00
dsql e684ac2853 update deploy guide per spec + add licensing standard
deploy.md:
- fix logs path to /srv/logs/<dev>/<project>
- reframe permissions as a deployer-side heads-up (bind-mount ownership is
  handled at deploy time; 'if your bot won't start or logs vanish, flag us')
  instead of a dev task / heavy footgun
- git in the image only when the container needs it (host always has git)
- NEW: layer caching (requirements before code copy)
- NEW: subprocess/browser workloads — init:true (tini + PID-1 shell-wrapper
  gotcha), shm_size 2gb, mem_limit; with code annotations and a warning
- refresh the compose-needs checklist accordingly

standards.md:
- NEW: licensing — no per-file headers; single top-level LICENSE only when a
  repo is for outside use

Verified: mkdocs build --strict clean; new deploy sections rendered.
Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 20:51:07 -04:00
dsql 4fd19bf620 rename to 'rethink development', brighten lib table, drop stale note
- Rename site 'a rethink development' -> 'rethink development' (site_name,
  landing H1); copyright -> 'rethink development (handbook)'.
- libraries.md: remove the 'if the live list is empty' admonition now that
  Gitea CORS is fixed.
- extra.css: improve Library-column readability — brighter #cfe6ff code
  chips on a cyan-tinted bg, more weight, roomier cell padding (0.7/1em),
  nowrap lib names, accent-colored links.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 20:35:16 -04:00
dsql e5ca1c0ada style the live libraries table + add Language column
- libraries.md: render the live lib list as a 3-column table (Library /
  What it does / Language), wrapped in .rt-lib-table so it picks up
  explicit styling (Material only auto-styles markdown tables, not
  innerHTML-injected ones — that's why it rendered bare before).
- extra.css: branded table (navy header, blue borders, hover rows,
  code-styled linked names) and a blue Language badge.

Verified in-browser against real Gitea API data (14 repos fetched,
handbook denylisted -> 13 libs): table renders sorted with language
badges, matches the theme. Live load on prod still needs Gitea CORS
(app.ini [cors] ALLOW_DOMAIN = the docs origin).

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 20:26:11 -04:00
dsql 8fc81fc4f4 add Gitea-style admonitions, code annotations, callouts
Make the things that matter stand out, matching the Gitea callout look:
- extra.css: brand-orange warning admonitions (#f57c00, the lambda orange),
  hotter danger (#e8590c), blue note/info and cyan tip/example, plus blue
  code-annotation markers.
- deploy.md: footgun -> danger callout, secrets -> orange warning, eligible
  -> tip; Dockerfile and compose gain numbered code annotations explaining
  each magic line; paths/mounts as a table; a restart snippet for rotation.
- workflow.md: warning on setting per-repo git identity before first commit;
  tip elevating verify-by-executing.

Verified in-browser: computed border colors match (warning #f57c00,
danger #e8590c), 4 annotation markers render in brand blue, admonition
icons + tinted headers match the Gitea style. mkdocs build --strict clean.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 20:16:02 -04:00
dsql 142a0dbff6 add lambda logo, fold sections, add python/error/log snippets
- Pull the rethink lambda logo (assets/img/logo.svg) into docs/assets and
  wire it as theme.logo + favicon — matches the Gitea brand mark.
- standards.md: fold six thin sections into three fuller ones (Files and
  style / Documentation / Quality and error handling), each illustrated
  with python snippets, a flake8 output block, Do/Don't tabbed examples,
  a traceback, and a log line. Admonitions for the run-it-locally tip and
  the lib-logging note.
- libraries.md: add a collapsible 'using a library' example (pyproject
  pin + import/usage python snippet).

Verified in-browser: logo renders in the header, snippets/tabs/traceback/
log blocks render against the dark theme, libraries example expands.
mkdocs build --strict clean.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 20:09:30 -04:00
dsql da47923088 theme handbook to match rethink Gitea + rename to 'a rethink development'
Mirror the rethink Gitea theme (data-theme=rethink) on the docs site:
- docs/stylesheets/extra.css maps the exact palette onto Material's slate
  scheme: body #0a0f1f, nav #061541, text #eef1f6, primary #569bcc,
  accent #55bbff, borders #294274; square corners and blue card/table
  borders echoing the Gitea repo panels.
- Force dark-only (drop the light/dark toggle; Gitea is dark-only).
- Rename site to lowercase 'a rethink development' (site_name, copyright,
  landing H1).

Verified: mkdocs build --strict clean; rendered landing + content pages
in-browser against the live Gitea page — colors, header bar, links, and
cards match.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 20:05:39 -04:00
dsql 5e787edbb0 add workflow page — dev-flow centerpiece
New docs/workflow.md covering how we actually work day to day:
- why git / why WSL2 (our reasons, links out for generic install)
- signing up on our Gitea (self-serve register, SSH + deploy-key model)
- our git vs public git: per-repo identity, signed commits, develop->main
- our-flavored git basics (everyday loop, commit small/often, gl graph)
- the dev workflow: plan-in-chat / build-in-Claude-Code, tmux split,
  claudedo voice control, the .claude/ project convention, AI-assist
  habits (git diff | clip.exe)
- recommended setup (WSL + VS Code + Claude Code, pyenv, flake8)
- handy .zshrc/.bashrc snippets with prose on the high-value ones

Claude-first but soft: any AI works, we recommend Claude, and the
.claude/ structure is built on that preference. Sanitized with
placeholders throughout. Nav: Home / Libraries / Standards / Workflow /
Deploy; landing card grid updated.

Verified: mkdocs build --strict clean.
Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 20:00:12 -04:00
15 changed files with 4935 additions and 130 deletions
+1
View File
@@ -9,6 +9,7 @@ __pycache__/
*.py[cod]
.venv/
venv/
uv.lock
# Playwright MCP run artifacts
.playwright-mcp/
+36
View File
@@ -0,0 +1,36 @@
# handbook
The Rethink Studios handbook — our public reference for the shared library suite,
coding standards, the dev workflow, Python environments, and how to deploy on our
network.
Live at **[docs.rethinkstudios.io](https://docs.rethinkstudios.io)**.
## What this is
A static documentation site built with [MkDocs](https://www.mkdocs.org/) +
[Material for MkDocs](https://squidfunk.github.io/mkdocs-material/). Markdown lives
under `docs/`, builds to a static `site/`, and is served by the reverse proxy at
the subdomain — no app process, just static HTML/CSS/JS.
The **Libraries** page is the one dynamic part: it fetches the `rethink-public`
repo list from the Gitea API client-side at view time, so new libraries appear on
the next page load with no rebuild. Everything else is static markdown.
## Build
```bash
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
mkdocs build # -> static site/
mkdocs serve # local preview at http://127.0.0.1:8000
```
## Contributing
- One topic per page, grouped under `docs/`, wired into the nav in `mkdocs.yml`.
- **Public — sanitize:** no real hostnames, internal IPs, secrets, or exact
topology. Use placeholders (`<dev>`, `<project>`, `/srv/...`).
- Markdown: trailing newline, no trailing whitespace, LF line endings.
- Commits signed (`git commit -s`).
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.0 KiB

+269 -74
View File
@@ -1,98 +1,293 @@
# Deployment Guide
> Ready for your project to see the light? You may be eligible for deployment on
> **rethink-net** — our fleet of Ubuntu 26.x servers, ready to host whatever
> you've built.
> How a project gets onto **rethink-net** — our Ubuntu 26.x servers. Get your
> container to follow a few consistent rules and deploying is mostly handing us a
> `compose.yaml`.
**Eligible:** APIs, websites, applets, bots, monitors.
!!! success "What can run here"
APIs, websites, applets, bots, monitors.
The whole network runs on a few simple, consistent rules. Get your container to
follow them and deploying is mostly handing us a `compose.yaml`.
## Preparing for Deploy
## Docker — the services account
You build and test locally — leaning on our libraries, AI, and this handbook — then
push to git. From there the ops tooling takes over: staging (if you run gitflow), then
it builds the Docker image and runs it on the fleet. **You don't run any deploy
commands** — your only job is the three pieces in the tabs below.
Every service runs containerized as the shared **`services`** account:
**uid/gid 1337**, fixed fleet-wide. Build your image to be **uid-agnostic** so it
runs cleanly as that account:
```mermaid
flowchart LR
subgraph build ["you build"]
direction TB
local["local testing"]
libs["libs<br/>(rethink-public, pip)"]
ai["AI-assisted<br/>(Claude Code)"]
docs["reading the<br/>handbook"]
local --- libs
libs --- ai
ai --- docs
docs --- local
end
- `user: "1337:1337"` in compose.
- `chmod -R a+rwX /app` in the Dockerfile (covers non-mounted dirs — see the
bind-mount note below).
- `HOME=/tmp`.
- **No** in-container `user`/`useradd` — don't bake a user into the image.
build -->|push| git["git<br/>(Gitea)"]
git -.->|develop, if gitflow| stage["staging"]
git --> deploy["deployment<br/>(ops tooling)"]
stage --> deploy
deploy --> docker["docker<br/>(built + run<br/>on the fleet)"]
```dockerfile
FROM python:3.12-slim
ENV HOME=/tmp
WORKDIR /app
# git in the build if you pip-install from git
RUN apt-get update && apt-get install -y --no-install-recommends git \
&& rm -rf /var/lib/apt/lists/*
COPY . .
RUN pip install --no-cache-dir . \
&& chmod -R a+rwX /app
CMD ["python", "-m", "yourapp"]
classDef ship fill:#061541,stroke:#569bcc,color:#eef1f6;
classDef work fill:#0e1530,stroke:#294274,color:#eef1f6;
class git,stage,deploy,docker ship;
class local,libs,ai,docs work;
```
```yaml
services:
yourapp:
build: .
user: "1337:1337"
environment:
HOME: /tmp
!!! danger "Your compose names nothing repo-specific"
**No `container_name`, no hardcoded names/paths.** The deploy layer injects
identity and host paths so your compose can't collide with any other service on
the fleet:
- the service key is **always `svc`** — it never changes, in any repo
- named volumes use **bare names** (`cache`, not `myapp_cache`)
- host paths come from **`${LOGS_DIR}` / `${CONFIG_DIR}`**
=== "Compose"
The `compose.yaml` your repo ships. Copy it verbatim — it names nothing
repo-specific, so the deploy layer can inject identity and host paths without
collisions.
```yaml
services:
svc: # generic service key — ALWAYS svc, no container_name
build: .
user: "1337:1337" # the shared services account (required)
restart: unless-stopped # required — host unit is oneshot; this recovers crashes
environment:
HOME: /tmp
volumes:
- ${LOGS_DIR:-./logs}:/app/logs # output — host log dir, injected at deploy
- ${CONFIG_DIR:-./config}:/app/config # input — host config dir, injected at deploy
# - ${MOUNTS_DIR:-./mounts}:/app/data # optional — arbitrary host data (opt-in)
- cache:/app/cache # your data — ephemeral named volume
volumes:
- /srv/configs/<project>:/app/config:ro # host-managed, read-only
- /srv/<dev>/<project>:/app/logs # live + rolled logs
- yourapp-data:/app/data # named volume — the rest
cache: # bare name — deploy auto-prefixes it per service
```
volumes:
yourapp-data:
```
!!! example "The container path is where your code reads and writes"
`WORKDIR` is `/app`, so the **right-hand side** of each volume line is the path
your code targets. Write to `./cache` (i.e. `/app/cache`) → the `cache` volume;
logs go to `/app/logs`, config is read from `/app/config`.
## Paths and mounts
**Storage, three tiers:**
- **Configs** → `/srv/configs/<project>/` — bind mount, host-managed.
- **Logs** → `/srv/<dev>/<project>/` — bind mount; live and rolled, scraped for
monitoring.
- **Everything else** (caches, browser profiles, scratch) → **named volumes**.
Docker manages ownership, so there are no host permissions to fiddle with.
- **`logs` + `config`** — we inject and manage these at deploy time.
- **`mounts`** (optional) — host dir for arbitrary data. We inject `MOUNTS_DIR` and
create the dir; you **uncomment** the line and pick the container path.
- **Named volumes** (`cache`, …) — yours; Docker owns them, no host paths to manage.
## Permissions — the bind-mount footgun
!!! warning "Two kinds of logs — and crashes go to the other one"
`${LOGS_DIR}` holds **only** what your app writes to disk through its logger
(e.g. `log_setup` writing a file) — your own structured logging. It does **not**
capture the process's stdout/stderr, and that's where **startup crashes and
uncaught exceptions land** — a traceback from a failed import or a missing file
never reaches your logger. So if a service dies on startup, or you don't see the
error in your log files, it's in the process output, not `${LOGS_DIR}`. Make
fatal errors visible — and to route uncaught exceptions into your log file too,
install a top-level hook:
`docker compose up` does **not** create bind-mount directories as you. If a
bind-mount source is missing, the Docker daemon (**root**) creates it **as
root** — and your container (**1337**) then can't write it. Logs fall back to
console-only, caches re-download every run.
```python
import sys, logging
sys.excepthook = lambda *exc: logging.getLogger().critical("uncaught", exc_info=exc)
```
So:
!!! success "Same file, both places"
Locally, `docker compose up` needs nothing set — the `${VAR:-./default}`
fallbacks use `./logs` / `./config`. When deployed, the ops tooling sets the
real values. One `compose.yaml` works everywhere.
- **Bind-mount sources (configs, logs) must EXIST and be 1337-owned _before_
`up`.** This is handled at provisioning, not a per-deploy chown hook.
- **Named volumes avoid this entirely** — use them for anything that doesn't need
host visibility.
- The Dockerfile `chmod -R a+rwX /app` only covers **non-mounted** dirs. A bind
mount overrides the image directory with the host directory, so for mounted
paths the **host-side ownership wins**.
**Subprocess and browser workloads** (bots that spawn Chrome, Xvfb, ffmpeg) need
three extra knobs:
## What your compose / Dockerfile needs
```yaml
services:
svc:
build: .
user: "1337:1337"
restart: unless-stopped
init: true # tini as PID 1 — reaps zombie subprocesses, forwards signals
shm_size: "2gb" # Chrome/headless browsers crash on Docker's default 64 MB /dev/shm
mem_limit: "4g" # bound memory — raise as worker/browser count grows
```
- `user: "1337:1337"`
- bind mounts for **configs + logs**
- named volumes for **the rest**
- secrets bind-mounted **`:ro`**
- `HOME=/tmp`
- `chmod -R a+rwX /app`
- `git` in the build if you `pip install` from git
!!! warning "The PID-1 gotcha with shell-wrapper CMDs"
If your `CMD` is a shell-script wrapper (e.g. `xvfb-run ...`), it must **not**
be PID 1, or the real process dies on startup. `init: true` fixes this — tini
takes PID 1, your wrapper runs as a normal child.
## Secrets
=== "Dockerfile"
We do **not** commit secrets (usually, lol). The rule:
Every service runs containerized as the shared **`services`** account: **uid/gid
1337**, fixed fleet-wide. Build the image to be **uid-agnostic** so it runs cleanly
as that account.
- Secrets stay **gitignored**.
- They're placed on the host at `/srv/configs/<project>/`.
- They're bind-mounted **read-only** at runtime.
- **Never** baked into the image — add them to `.dockerignore` so `COPY . .`
can't grab them.
```dockerfile
FROM python:3.12-slim
ENV HOME=/tmp # services account has no home dir; $HOME must be writable
Rotating a secret = edit the host file and restart. No rebuild.
WORKDIR /app
RUN apt-get update \
&& apt-get install -y --no-install-recommends git \
&& rm -rf /var/lib/apt/lists/* # include git ONLY if the container itself needs it
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt # deps before code — layer caching
COPY . .
RUN chmod -R a+rwX /app # writable by any uid — this is "uid-agnostic"
CMD ["python", "-m", "yourapp"]
```
**Layer caching:** copy the deps file and install **before** `COPY . .` — Docker
caches layers in order, so deps only reinstall when the deps file changes, not on
every code edit.
**Getting files in:** `COPY . .` grabs the whole repo; be explicit about anything
that needs its own place. In `COPY <src> <dest>`, `<src>` is relative to the build
context (your repo), `<dest>` is a path in the image.
```dockerfile
COPY ./config.toml /app/config.toml # a single file into a specific path
COPY ./assets /app/assets # a whole directory (tree mirrors ./assets/)
```
!!! tip "Faster builds with uv (optional)"
[uv](https://docs.astral.sh/uv/) is a drop-in for pip that reads the same
`pyproject.toml` — no lockfile needed in the image. Add `UV_COMPILE_BYTECODE`
so containers don't pay the first-import `.pyc` compile cost:
```dockerfile
FROM python:3.12-slim
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ # pull the uv binary from its image
ENV HOME=/tmp
ENV UV_COMPILE_BYTECODE=1 # compile bytecode at build, not cold start
WORKDIR /app
COPY pyproject.toml .
RUN uv pip install --system . # into the image's Python, no venv/lock
COPY . .
RUN chmod -R a+rwX /app
CMD ["python", "-m", "yourapp"]
```
=== "Secrets"
!!! warning "Secrets never go in the image"
We do **not** commit secrets (usually, lol). They stay **gitignored** and live
on the host in your config dir, reaching your container read-only via
`${CONFIG_DIR}`. Add them to `.dockerignore` so a `COPY . .` can't sweep them
into a layer.
- Secrets live on the **host**, in your config dir — never in git, never in the
image.
- They reach the container **read-only** via the injected `${CONFIG_DIR}` mount.
- Keep them out of the build context: list them in `.dockerignore` so a blanket
`COPY . .` can't pull them into a layer.
**Rotating a secret** is a host-side edit — update the file and the service picks
it up on restart. No rebuild, and nothing you run: flag it and we handle the
restart.
## Supporting services live in your compose, not the fleet
Need Redis? **Declare it in your own `compose.yaml`, as a sidecar.** There is no shared
Redis — nothing fleet-wide, nothing per-workspace, nothing ops provisions for you. A repo
that needs Redis brings its own; a repo that doesn't adds nothing.
That's the whole point of the deploy model. Every service already runs in its own compose
project on its own network so that one service falling over can't touch another. A shared
Redis puts that coupling straight back: one process everything depends on, whose OOM, stray
`FLUSHALL`, single-threaded stall, or restart becomes *everyone's* outage. A sidecar shares
its owning repo's fate and nobody else's — and the isolation is free, because it rides the
per-project network you already get. No ACLs, no key-prefix discipline, no shared
credentials to manage.
Your app talks to it with the [`redis` lib](libraries.md) from the suite (async,
config-free, kv/hash/ttl/pubsub), pointed at **`redis://redis:6379`** — the compose
**service name**, not a host port. The sidecar comes up auto-namespaced on your project's
network like every other container, exactly as the naming convention above describes.
### Ephemeral or persistent — pick deliberately
A sidecar Redis is **ephemeral by default**: restart it and the data is gone. That's
correct for some workloads and quietly destructive for others, so make the call on purpose.
Ask one question — *if this data vanished on a restart, would anything be lost?*
- **No → ephemeral.** A scratch cache, a dedupe set, rate-limit counters, transient data
you can just re-fetch. Nothing to back up, nothing to grow.
- **Yes → persistent.** An outbound webhook or notification queue, a job queue, anything
that could be mid-flight when the process dies. Losing it drops real work.
=== "Ephemeral (cache / throwaway)"
Fine to lose on restart — no volume, no persistence, by design.
```yaml
redis:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --save "" --maxmemory 256mb --maxmemory-policy allkeys-lru
# no volume: throwaway by design
```
=== "Persistent (durable queue / state)"
Survives restart, rebuild, and reboot — the append-only file lives on the host mounts
dir injected at deploy, the same `${MOUNTS_DIR}` mechanism described above. Redis just
uses it as its backing store.
```yaml
redis:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --appendonly yes --appendfsync everysec
volumes:
- ${MOUNTS_DIR:-./mounts}/redis:/data # AOF persists on the host mounts dir
```
The app connects the same way in both modes — `REDIS_URL: redis://redis:6379`. Only the
durability changes.
!!! warning "Two things to know before you rely on a persistent sidecar"
**`--appendfsync everysec` can lose ~1 second of the newest entries** on a hard crash.
For a webhook queue that's an acceptable trade — just know it's there. Use
`--appendfsync always` if you genuinely cannot drop a single entry (safer, slower).
**Data under the mounts dir is not backed up.** Mounts are excluded from the backup
pipeline, and that's *right* for a queue: a lost queue means some notifications didn't
fire, not that business data is gone. **That's the dividing line.** If losing this data
would actually hurt, it isn't queue or cache state — it's a system of record, and it
belongs in Postgres (which *is* backed up), not a local mount.
### The rules
!!! danger "Don't do these"
- **Don't map Redis to a host port.** No `ports: - "6379:6379"`. Two repos both
grabbing host 6379 on the same box collide. Keep it internal to the compose network —
nothing exposed, nothing to collide.
- **Don't stand up a shared or fleet-wide Redis.** Per-repo means per-need. One Redis
per *project*, shared by that project's containers if a repo runs several — never one
per fleet.
- **Don't treat persistent Redis as a database.** Queues and caches, yes. A durable
system of record, no — that's Postgres, and unlike a mount it's backed up.
**You can't reach another repo's Redis** — different project, different network. That's not
a restriction you have to work around; it's the isolation working *for* you. Nobody else's
service can touch your cache or drain your queue either, and you never have to think about
whose keys are whose.
!!! quote "What about ACLs?"
A shared Redis *can* be secured — Redis 6+ ACLs scope users by command, key pattern, and
channel. But ACLs don't solve resource contention or the noisy-neighbour problem, and
they add real management burden, so the fleet uses per-repo sidecars instead. Reserve
ACLs for the rare case of a deliberately shared, durable, backed-up Redis run as actual
infrastructure.
+198
View File
@@ -0,0 +1,198 @@
# Virtual environments
How we keep a project's Python isolated — from the system Python and from every
other project. The rule underneath all of it: **never install into, or upgrade,
the system Python.** The OS depends on it; a project must never touch it.
!!! danger "Leave the system Python alone"
No `sudo pip install`, no upgrading the system interpreter for a project. If a
project needs a different version or a package, that goes in a **virtual
environment** — never the system one. Breaking system Python can break the OS.
## Project-based isolation
Every project runs against its own isolated environment. There are three ways that
happens, depending on where the project runs:
=== "Local `.venv`"
A virtualenv living in the repo. Simplest for day-to-day local work.
```bash
python -m venv .venv # create it (once)
source .venv/bin/activate # activate for this shell
pip install -e . # install the project from pyproject.toml
```
Or with [uv](#uv-optional-faster) — same `pyproject.toml`, much faster:
```bash
uv venv # create .venv
uv pip install -e . # install from pyproject.toml
```
Keep `.venv/` **gitignored** — it's per-machine, never committed.
=== "Makefile-driven"
Wrap the venv in a `make` target so every dev (and CI) sets up identically —
no "did you activate it?" drift.
```makefile
VENV := .venv
PY := $(VENV)/bin/python
$(VENV): requirements.txt
python -m venv $(VENV)
$(PY) -m pip install -r requirements.txt
.PHONY: run
run: $(VENV)
$(PY) -m yourapp
```
`make run` creates the venv if missing, installs deps, and runs — all against
the isolated interpreter, no manual activation.
=== "Docker"
The container **is** the isolation — its own filesystem, its own interpreter,
nothing shared with the host. You don't need a `.venv` inside an image; install
straight into the container's Python.
```dockerfile
FROM python:3.12-slim
COPY pyproject.toml .
RUN pip install --no-cache-dir . # or: uv pip install .
COPY . .
```
This is how things run in production — see the [Deploy guide](deploy.md) for the
full container standard (uid 1337, the compose convention, layer caching, and the
uv image setup).
!!! tip "Which one?"
**Local `.venv`** for quick iteration, **Makefile** when you want repeatable
setup across the team, **Docker** for anything that ships. They're not
exclusive — a project often has a `.venv` for local dev *and* a Dockerfile for
deploy.
## uv (optional, faster)
!!! tip "uv is the recommended fast path; pip stays the baseline"
[uv](https://docs.astral.sh/uv/) is a faster, standards-compliant drop-in for
pip. It reads the **same `pyproject.toml`** — no workflow change required, and
`pip` keeps working exactly as before. Use it wherever you'd reach for pip; the
rest of this handbook shows the pip command with the uv equivalent beside it.
Install deps straight from `pyproject.toml` (no `requirements.txt` needed):
```bash
uv pip install . # install the project + its deps
uv pip install -e . # editable (dev) install
uv pip install '.[dev]' # with an extras group, e.g. dev
```
If you'd rather have uv manage the venv for you, use the managed-venv flow:
```bash
uv sync # create/refresh .venv from pyproject.toml + uv.lock
uv run python -m yourapp # run inside the managed env, no manual activate
```
!!! info "`uv.lock` is local-only — never committed"
`uv sync` writes a `uv.lock` for your machine's resolved environment. It is
**gitignored**, not committed — we don't ship a lockfile. Pinning happens in
`pyproject.toml` (below), not the lock.
### Pinning deps
Pin **direct** dependencies in `pyproject.toml` with `==` or a git `@ref`:
```toml
[project]
dependencies = [
"requests==2.31.0",
"mylib @ git+https://git.rethinkstudios.io/rethink-public/mylib.git@<sha>",
]
```
!!! warning "This pins direct deps only — transitive deps still float"
`==` / `@ref` pins the packages **you** list. Their dependencies still resolve
fresh at build time. That's an accepted tradeoff — documented on purpose — not
an oversight: we pin what we depend on directly and let the rest float.
## Local dev with pyenv
For local work you also need the right **Python version**, not just isolated deps.
We target **Python 3.10+**, and [pyenv](https://github.com/pyenv/pyenv) installs and
switches versions per-project without touching the system Python.
- **Per-project pinning:** a `.python-version` file in a repo makes pyenv
auto-select that interpreter when you `cd` in — everyone on the project runs the
same one.
- **Pairs with venvs:** combined with `pyenv-virtualenv`, each project gets both an
isolated version *and* isolated deps.
### Install
Follow the [official pyenv installation](https://github.com/pyenv/pyenv#installation)
for the installer and build dependencies — no point reproducing it here. Then add
the shell init below to your `~/.zshrc` (or `~/.bashrc`) and restart your shell.
### Shell init
Without these lines pyenv's shims aren't on `PATH`, so `pyenv` and
auto-version-switching won't work:
```bash
# pyenv — Python version management
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init --path)" # (1)!
eval "$(pyenv init -)" # (2)!
eval "$(pyenv virtualenv-init -)" # (3)!
```
1. Puts the **shims** dir on `PATH`, so `python` resolves to the pyenv-selected
version instead of the system one.
2. Shell integration — command rehashing and completion.
3. Auto-activates a project's virtualenv on `cd` — **only** if you use
`pyenv-virtualenv`. Drop this line if you don't.
### Everyday use
```bash
pyenv install 3.10.14 # install a version (one-time)
pyenv install --list # see available versions
cd <project>
pyenv local 3.10.14 # writes .python-version -> auto-selects here
python --version # confirms the pinned version
```
- **`pyenv local <ver>`** per project — commit the `.python-version` so the team
matches.
- **`pyenv global <ver>`** for your default outside any project.
## Shell quality-of-life extras
A couple of optional lines worth having alongside pyenv:
```bash
# flake8 with our shared config (max line 120)
alias flake8='flake8 --config ~/.config/flake8'
# local bins on PATH (pip --user installs, npm globals)
export PATH="$HOME/.local/bin:$PATH"
export PATH="$HOME/.npm-global/bin:$PATH"
```
- The **flake8 alias** keeps everyone linting with the same config (our 120
max-line, etc. — see [Standards](standards.md#files-and-style)).
- The **`.local/bin` / npm-global** PATH lines stop "command not found" after a
`pip install --user` or a global npm install.
!!! note "More shell setup"
This is the Python-env slice. The fuller dev shell setup — the WSL/Windows
clipboard bridges, per-repo git-identity aliases, and the `gl` graph log — is on
the [Workflow](workflow.md#handy-shell-setup) page.
+47 -6
View File
@@ -1,11 +1,38 @@
# Rethink Studios Handbook
# rethink development
The public reference for building and shipping with Rethink Studios: our shared
libraries, our coding standards, and how to deploy a project on our network.
The reference for building and shipping on our network: the shared libraries, the
coding standards, and how to get a project deployed.
This is a public site — it documents generic patterns and conventions. Real
infrastructure specifics (hostnames, internal IPs, exact topology, secrets) stay
out of it; examples use placeholders like `<dev>`, `<project>`, and `/srv/...`.
out of it; examples use placeholders like `<workspace>`, `<project>`, and
`/srv/...`.
!!! example "Point your coding agent here"
Want your agent aware of our libraries, standards, and deploy rules before it
writes a line? Tell it to read this handbook — so it reaches for an existing
`rethink-public` lib instead of reinventing it, follows our conventions, and
builds a deploy-ready container.
=== "From the live site"
```text
Read https://docs.rethinkstudios.io and follow it: prefer our
rethink-public libraries, match our coding standards, and make anything
deployable per the deploy guide.
```
=== "From git"
Point it straight at the repo — no need to clone into your project:
```text
Read the markdown under docs/ in
https://git.rethinkstudios.io/rethink-public/handbook and follow it:
prefer our rethink-public libraries, match our coding standards, and make
anything deployable per the deploy guide. Clone to /tmp if you need it
local.
```
## Sections
@@ -25,11 +52,25 @@ out of it; examples use placeholders like `<dev>`, `<project>`, and `/srv/...`.
House coding standards — file hygiene, docstrings, type hints, linting, and
how we handle errors.
- :material-sitemap: __[Workflow](workflow.md)__
---
Get hands on with how we dev — our Gitea, git habits, and the
plan-in-chat / build-in-Claude-Code flow, plus shell setup.
- :material-language-python: __[Virtual environments](environments.md)__
---
Project-based Python isolation — local `.venv`, Makefile, or Docker — and
local version management with pyenv.
- :material-rocket-launch: __[Deploy](deploy.md)__
---
How to get a project running on **rethink-net**containers, paths and
mounts, permissions, and secrets.
How to get a project running on **rethink-net**the compose convention,
the one-command deploy, and secrets.
</div>
+29
View File
@@ -0,0 +1,29 @@
// render mermaid diagrams (emitted as <div class="mermaid">SOURCE</div>).
// render(id, src) is used directly and each block is processed once.
(function () {
var inited = false;
var seq = 0;
function boot() {
if (typeof mermaid === "undefined") return;
if (!inited) {
mermaid.initialize({ startOnLoad: false, theme: "dark", securityLevel: "loose" });
inited = true;
}
document.querySelectorAll("div.mermaid").forEach(function (el) {
if (el.dataset.mmdDone) return;
var src = el.textContent.trim();
if (!src) return;
el.dataset.mmdDone = "1";
mermaid.render("mmd-" + seq++, src).then(function (out) {
el.innerHTML = out.svg;
}).catch(function () {
delete el.dataset.mmdDone;
});
});
}
if (window.document$ && typeof window.document$.subscribe === "function") {
window.document$.subscribe(boot);
} else {
document.addEventListener("DOMContentLoaded", boot);
}
})();
+3587
View File
File diff suppressed because one or more lines are too long
+42 -11
View File
@@ -11,10 +11,44 @@ the repo, where the README and tags live.
## Install
Pin a tag in your dependencies — never an unpinned branch:
```
<lib> @ git+https://git.rethinkstudios.io/rethink-public/<lib>.git@<tag>
```
??? example "Using a library — install, import, go"
Add it to your project's deps (e.g. `pyproject.toml`):
```toml
[project]
dependencies = [
"aioweb @ git+https://git.rethinkstudios.io/rethink-public/aioweb.git@v0.3.1",
]
```
Then use it — the README in each repo has the real surface; this is the shape:
```python
import asyncio
from aioweb import Session
async def main() -> None:
"""fetch a page through the shared async http session"""
async with Session() as web:
resp = await web.get("https://example.test")
print(resp.status, len(resp.content))
asyncio.run(main())
```
Need to bump a lib? Change the `@<tag>` and reinstall — versions live with the
lib, not in these docs.
<div id="lib-list" markdown="0">
<p class="lib-status">Loading libraries from Gitea…</p>
</div>
@@ -55,12 +89,17 @@ the repo, where the README and tags live.
var rows = libs.map(function (r) {
var url = r.html_url || (REPO_BASE + "/" + r.name);
var desc = r.description ? escapeHtml(r.description) : "<em>No description.</em>";
var lang = r.language
? '<span class="rt-lang">' + escapeHtml(r.language) + "</span>"
: '<span class="rt-lang rt-lang--none">—</span>';
return '<tr><td><a href="' + escapeHtml(url) + '"><code>' +
escapeHtml(r.name) + '</code></a></td><td>' + desc + '</td></tr>';
escapeHtml(r.name) + "</code></a></td><td>" + desc +
"</td><td>" + lang + "</td></tr>";
}).join("");
el.innerHTML =
'<table><thead><tr><th>Library</th><th>What it does</th></tr></thead>' +
'<tbody>' + rows + '</tbody></table>';
'<div class="rt-lib-table"><table>' +
"<thead><tr><th>Library</th><th>What it does</th><th>Language</th></tr></thead>" +
"<tbody>" + rows + "</tbody></table></div>";
}
function fail() {
@@ -84,11 +123,3 @@ the repo, where the README and tags live.
}
})();
</script>
!!! info "If the live list is empty or stale"
The fetch runs in your browser against the Gitea API and needs the org repos
to be readable unauthenticated (they are — it's a public org) and CORS to
allow the docs domain. If the list won't load, browse the org directly at
[git.rethinkstudios.io/rethink-public](https://git.rethinkstudios.io/rethink-public).
The fallback for a disabled live fetch is a webhook rebuild — a push to any
`rethink-public` repo triggers a site rebuild.
+151 -22
View File
@@ -3,40 +3,169 @@
The house standards every Rethink Studios project follows. They keep our code
consistent, readable, and predictable across the whole suite.
## File hygiene
## Files and style
- Files end with a **single trailing newline** (LF / Unix line endings).
- **No trailing whitespace** on any line.
- Files end with a **single trailing newline** (LF / Unix line endings), and
carry **no trailing whitespace** on any line.
- **4 spaces** for indentation by default. Respect language norms — TS/JS use
**2 spaces**, Go uses **tabs** — and when editing an existing file, follow that
file's existing indentation.
- **flake8 clean**, max line length **120**. **Type hints** on public functions.
## Indentation
A well-formed module — module docstring, type hints, lowercase-start docstring:
- **4 spaces** by default.
- Respect language norms: TS/JS use **2 spaces**, Go uses **tabs**.
- When editing an existing file, follow that file's existing indentation.
```python
"""async key/value store backed by a single json file"""
## Docstrings and comments
from pathlib import Path
- Public functions get a **docstring**. Style: **lowercase-start, no trailing
period**.
- Keep inline comments minimal — prefer docstrings. Use an inline comment only
def load_state(path: Path) -> dict[str, str]:
"""read the json state file, returning an empty dict if it's missing"""
if not path.exists():
return {}
return _read_json(path)
```
flake8 tells you the moment you drift — keep the tree clean:
```text
$ flake8
./aiokv/store.py:14:80: E501 line too long (96 > 79 characters)
./aiokv/store.py:22:1: F401 'json' imported but unused
./aiokv/store.py:31:5: E303 too many blank lines (3)
```
!!! tip "Run it locally"
Wire the shared config into an alias so every project lints the same way:
`alias flake8='flake8 --config ~/.config/flake8'` (max line 120). See the
[Workflow](workflow.md#handy-shell-setup) page.
## Documentation
- Public functions get a **docstring****lowercase-start, no trailing period**.
- Keep inline comments minimal; prefer docstrings. Use an inline comment only
where the code is genuinely complex.
- Each module/file states its scope and purpose via a **module docstring**
(header string) — **not** a license or copyright header.
- Every library and project has a **README** with the install line, what it does,
and a usage example.
## READMEs
!!! note "Licensing — no per-file headers"
Don't prepend license/copyright boilerplate to source files. If a repo needs a
license, it's a single top-level **`LICENSE`** file — never repeated per file.
Most internal repos don't carry one; add it only when a repo is meant for
outside use and the terms are decided.
Every library and project has a defined **README** covering:
=== "Do"
- the install line,
- what it does,
- a usage example.
```python
def mask_secret(value: str, keep: int = 4) -> str:
"""mask all but the last ``keep`` characters of a secret"""
if len(value) <= keep:
return "*" * len(value)
return "*" * (len(value) - keep) + value[-keep:]
```
## Linting and types
=== "Don't"
- **flake8 clean**, max line length **120**.
- **Type hints** on public functions.
```python
# Masks a secret. <- license-header-style noise, capitalized, trailing period
def mask_secret(value, keep=4): # no type hints
# loop over the chars and hide them
return "*" * (len(value) - keep) + value[-keep:] # crashes if short
```
## Error handling
## Quality and error handling
- **Fail loud** — never swallow exceptions.
- Catch the **specific** exception, and **log** it.
**Fail loud** — never swallow exceptions. Catch the **specific** exception and
**log** it; don't paper over failures with a bare `except`.
=== "Do"
```python
import logging
log = logging.getLogger(__name__)
def fetch(url: str) -> bytes:
"""fetch ``url``, logging and re-raising on failure"""
try:
return _client.get(url).content
except TimeoutError:
log.warning("fetch timed out: %s", url)
raise
```
=== "Don't"
```python
def fetch(url):
try:
return _client.get(url).content
except Exception:
pass # swallowed — the caller has no idea anything broke
```
When something does break, a loud failure gives you a real traceback to act on:
```python-traceback
Traceback (most recent call last):
File "run.py", line 42, in <module>
data = fetch("https://example.test/feed")
File "aioweb/session.py", line 88, in fetch
return self._client.get(url).content
TimeoutError: request timed out after 30s
```
…and the log line that precedes it tells you where to look:
```text
2026-06-29 14:03:11,204 WARNING aioweb.session fetch timed out: https://example.test/feed
```
!!! info "Logging belongs to the app, not the library"
Libraries **emit only** — `log = logging.getLogger(__name__)` and nothing
else. Handlers, levels, and formatting are configured once at the
application entry point, so a lib never dictates how its host logs.
## Service compose
A deployable service ships a `compose.yaml` that names **nothing repo-specific** —
the deploy layer injects identity and host paths. See the
[Deploy guide](deploy.md) for the full convention and the variables
you can rely on. The short version:
=== "Right"
```yaml
services:
svc:
user: "1337:1337"
restart: unless-stopped
volumes:
- ${LOGS_DIR:-./logs}:/app/logs
- profile:/app/profile
volumes:
profile:
```
=== "Wrong (causes collisions)"
```yaml
services:
nova:
container_name: nova # repo-specific name -> collides
volumes:
- /srv/logs/ricky/nova:/app/logs # hardcoded host path
- nova_profile:/app/profile # repo-prefixed volume
volumes:
nova_profile:
```
Generic service key `svc`, no `container_name`, host paths from `${...}` variables,
and **bare** volume names — that's what makes a service collision-proof on the
fleet.
+236
View File
@@ -0,0 +1,236 @@
/* a rethink development — mirror of the rethink Gitea theme
palette pulled from git.rethinkstudios.io (data-theme="rethink") */
:root {
--rt-body: #0a0f1f; /* deep navy page background */
--rt-nav: #061541; /* darker header/nav background */
--rt-surface: #0e1530; /* cards / code blocks, a touch above body */
--rt-text: #eef1f6; /* near-white body text */
--rt-muted: #aab4c5; /* secondary text */
--rt-primary: #569bcc; /* links / primary blue */
--rt-primary-dark: #4a8cbb;
--rt-primary-light: #6ba9d5;
--rt-accent: #55bbff; /* bright cyan-blue accent */
--rt-border: #294274; /* row dividers / borders */
}
/* Map the rethink palette onto Material's slate scheme. */
[data-md-color-scheme="slate"] {
--md-hue: 222;
--md-default-bg-color: var(--rt-body);
--md-default-fg-color: var(--rt-text);
--md-default-fg-color--light: var(--rt-muted);
--md-default-fg-color--lighter: rgba(238, 241, 246, 0.32);
--md-default-fg-color--lightest: rgba(238, 241, 246, 0.12);
--md-primary-fg-color: var(--rt-nav);
--md-primary-fg-color--light: var(--rt-primary-light);
--md-primary-fg-color--dark: var(--rt-nav);
--md-primary-bg-color: var(--rt-text);
--md-primary-bg-color--light: var(--rt-muted);
--md-accent-fg-color: var(--rt-accent);
--md-accent-fg-color--transparent: rgba(85, 187, 255, 0.1);
--md-typeset-color: var(--rt-text);
--md-typeset-a-color: var(--rt-primary);
--md-code-bg-color: var(--rt-surface);
--md-code-fg-color: #d6deeb;
--md-footer-bg-color: var(--rt-nav);
--md-footer-bg-color--dark: var(--rt-body);
}
/* Header / top nav: match Gitea's dark-blue bar. */
.md-header,
.md-tabs {
background-color: var(--rt-nav);
color: var(--rt-text);
}
/* Links hover to the bright accent, like Gitea. */
.md-typeset a:hover {
color: var(--rt-accent);
}
/* Sidebar nav active item picks up the blue. */
.md-nav__link--active,
.md-nav__item .md-nav__link--active {
color: var(--rt-primary);
}
/* Square the corners — Gitea uses radius 0 throughout. */
.md-typeset .admonition,
.md-typeset details,
.md-typeset pre > code,
.md-typeset .highlight,
.md-typeset table:not([class]),
.md-search__form,
.md-typeset .tabbed-set > input:checked + label,
.md-typeset code {
border-radius: 0;
}
/* Subtle blue row dividers in tables, echoing the repo list. */
.md-typeset table:not([class]) {
border: 1px solid var(--rt-border);
}
.md-typeset table:not([class]) th {
background-color: var(--rt-nav);
color: var(--rt-text);
}
.md-typeset table:not([class]) td {
border-top: 1px solid rgba(41, 66, 116, 0.5);
}
/* Search field on the dark bar. */
.md-search__input {
background-color: rgba(255, 255, 255, 0.06);
}
.md-search__input::placeholder {
color: rgba(238, 241, 246, 0.6);
}
/* Admonitions — Gitea-style: brand-orange warning, blue note, etc.
The lambda logo's orange (#ffae42 -> #f57c00) drives the warning/danger look. */
.md-typeset .admonition,
.md-typeset details {
border-width: 0 0 0 .2rem;
background-color: var(--rt-surface);
}
/* warning / caution: the orange Gitea callout */
.md-typeset .admonition.warning,
.md-typeset details.warning,
.md-typeset .admonition.caution,
.md-typeset details.caution {
border-color: #f57c00;
}
.md-typeset .warning > .admonition-title,
.md-typeset .warning > summary,
.md-typeset .caution > .admonition-title,
.md-typeset .caution > summary {
background-color: rgba(245, 124, 0, 0.12);
}
.md-typeset .warning > .admonition-title::before,
.md-typeset .caution > .admonition-title::before {
background-color: #f57c00;
}
/* danger: a hotter orange-red for true footguns */
.md-typeset .admonition.danger,
.md-typeset details.danger {
border-color: #e8590c;
}
.md-typeset .danger > .admonition-title,
.md-typeset .danger > summary {
background-color: rgba(232, 89, 12, 0.14);
}
/* note / info / tip: blue + cyan from the rethink palette */
.md-typeset .admonition.note,
.md-typeset details.note,
.md-typeset .admonition.info,
.md-typeset details.info {
border-color: var(--rt-primary);
}
.md-typeset .note > .admonition-title,
.md-typeset .info > .admonition-title {
background-color: rgba(86, 155, 204, 0.12);
}
.md-typeset .admonition.tip,
.md-typeset details.tip,
.md-typeset .admonition.example,
.md-typeset details.example {
border-color: var(--rt-accent);
}
.md-typeset .tip > .admonition-title,
.md-typeset .example > summary {
background-color: rgba(85, 187, 255, 0.1);
}
/* Live libraries table — JS-injected, so it needs explicit Material-style rules
(Material only auto-styles markdown tables, not innerHTML ones). */
.rt-lib-table {
overflow-x: auto;
margin: 1em 0;
}
.rt-lib-table table {
width: 100%;
border-collapse: collapse;
border: 1px solid var(--rt-border);
font-size: 0.74rem;
}
.rt-lib-table th,
.rt-lib-table td {
text-align: left;
padding: 0.7em 1em;
vertical-align: top;
}
.rt-lib-table td:first-child {
white-space: nowrap;
}
.rt-lib-table thead th {
background-color: var(--rt-nav);
color: var(--rt-text);
font-weight: 700;
border-bottom: 1px solid var(--rt-border);
}
.rt-lib-table tbody tr {
border-top: 1px solid rgba(41, 66, 116, 0.5);
transition: background-color 0.15s;
}
.rt-lib-table tbody tr:hover {
background-color: rgba(86, 155, 204, 0.08);
}
.rt-lib-table td code {
background-color: rgba(85, 187, 255, 0.12);
color: #cfe6ff;
padding: 0.2em 0.55em;
font-size: 0.9em;
font-weight: 600;
}
.rt-lib-table a {
color: var(--rt-accent);
font-weight: 600;
}
.rt-lib-table a:hover code {
background-color: rgba(85, 187, 255, 0.22);
color: #ffffff;
}
/* Language badge in the libs table. */
.rt-lang {
display: inline-block;
padding: 0.1em 0.5em;
border: 1px solid var(--rt-border);
border-radius: 0;
background-color: rgba(86, 155, 204, 0.1);
color: var(--rt-primary-light);
font-size: 0.85em;
white-space: nowrap;
}
.rt-lang--none {
border-color: transparent;
background-color: transparent;
color: var(--rt-muted);
}
/* Code annotation markers (the (1) callouts) in brand blue. */
.md-typeset .md-annotation__index {
background-color: var(--rt-primary);
color: var(--rt-body);
}
/* Landing-page cards: surface tint + blue border like Gitea panels. */
.md-typeset .grid.cards > :is(ul, ol) > li,
.md-typeset .grid > .card {
background-color: var(--rt-surface);
border: 1px solid var(--rt-border);
border-radius: 0;
}
.md-typeset .grid.cards > :is(ul, ol) > li:hover {
border-color: var(--rt-primary);
}
+288
View File
@@ -0,0 +1,288 @@
# Workflow
Get hands on with how we dev — where code lives, how we use git, the AI-assisted
flow we recommend, and the shell setup that ties it together. This is the
**our-flavored** version: why *we* do it this way and how *our* setup is wired. For
the truly generic parts (installing WSL, learning git), we link the official docs
rather than reteach them.
!!! info "Public, sanitized"
Examples use placeholders — `<you>`, `<key>`, `dev@<you>`, `/mnt/c/<your>/...`.
Swap in your own real values locally; never commit personal paths, key names,
or emails.
## Why git, why WSL
**Git** is the backbone of everything here. Version history and branching are the
obvious part, but our whole deploy model is built on git too: we `pip install`
libraries straight from git by tag, servers pull via per-repo deploy keys, and
changes flow `develop → main` before they ship. If you know git, you already
understand how our code moves from your machine to production.
**WSL2 (Ubuntu)** is where we develop — a real Linux toolchain on a Windows
desktop. You get native Linux tooling (the same environment our servers run) plus
the Windows apps you actually use day to day. Install WSL2 from Microsoft's docs;
this page documents the *our-setup* layer that goes on top.
- [Install WSL (Microsoft)](https://learn.microsoft.com/windows/wsl/install)
- [VS Code Remote — WSL](https://code.visualstudio.com/docs/remote/wsl)
## Signing up on our Gitea
Our code lives on **Gitea** at
[git.rethinkstudios.io](https://git.rethinkstudios.io). Two orgs you'll use:
- **[rethink-public](https://git.rethinkstudios.io/rethink-public)** — public
libraries, resources, and assets.
- **[rethink-software](https://git.rethinkstudios.io/rethink-software)** — our
applications.
**Get an account:**
1. Register at [git.rethinkstudios.io](https://git.rethinkstudios.io) and verify
your email.
2. Add your **SSH public key** under *Settings → SSH / GPG Keys* so you can clone
and push over SSH.
3. For servers, we use a **per-repo deploy-key** model rather than your personal
key — see the [Deploy guide](deploy.md) for how a box gets read access to just
the repos it needs.
## Our git vs. public git (GitHub / GitLab)
It's the same git — just a different host. The thing most devs trip on is
**identity and keys per host**: you may have a GitHub identity *and* a Gitea
identity on one machine, and commits need to be attributed (and signed/pushed)
with the right one per project.
We solve that with **per-repo local git config** — run a small alias inside a repo
to set its local user and the SSH key it pushes with (see
[per-project git identity](#handy-shell-setup) below). No global identity
juggling.
Our conventions, in short:
- **Signed commits** — `git commit -s`.
- **No AI co-author trailer** on your own work. (Intern/dev work you're crediting
gets that dev's `Co-Authored-By` — nothing else.)
- **`develop` is staging**, merge to **`main`** via MR when it's ready to ship.
- **Libraries install from git by tag** — pin a version in your deps, bump the
tag when the lib releases.
!!! warning "Set your per-repo identity *before* the first commit"
One machine often carries more than one Gitea identity/key. If you forget to
run `gitsetup` in a fresh clone, your commits attribute to the wrong
user — or push with the wrong key and bounce. Run it right after cloning;
see [per-project git identity](#handy-shell-setup).
## Git basics (our-flavored)
Not a git tutorial — just how the everyday loop looks against our Gitea. For the
generic command reference, keep the
[Git cheat sheet](https://training.github.com/downloads/github-git-cheat-sheet/)
or the [Pro Git book](https://git-scm.com/book) handy.
**Clone** over SSH (the alias maps to a key — see the shell setup):
```bash
git clone git@<alias>:rethink-public/<repo>.git
```
**The everyday loop:**
```bash
git switch -c <feature> # branch off
# ...make a logical change...
git commit -s -m "..." # commit, signed
git push -u origin <feature> # push
# open an MR: develop -> main
```
**Commit small and often** — one commit per logical change, not a giant
end-of-day dump. Small commits are easier to review, revert, and `git bisect`
when something breaks.
**Read history** as a graph with the `gl` alias below:
```bash
gl # git log --graph, oneline, decorated
```
## The dev workflow
This is the part that's distinctly *ours*. AI is a force multiplier, but only with
discipline around it — the two habits that matter most are **plan before you
build** and **verify by executing, not asserting**.
You can use whatever AI you like — but we **recommend Claude** (via
[Claude Code](https://docs.claude.com/en/docs/claude-code/overview), in the
terminal or VS Code). The whole project structure we recommend below — the
`.claude/` folder, `CLAUDE.md` instructions, numbered specs — is built on that
preference: it's designed around how Claude Code reads project context and takes
handoffs. Other tools can read these files too, but the convention assumes
Claude-first.
**Plan here, build in Claude Code.** Do the thinking in chat — plan, write the
spec, make the decisions. Then hand that spec to a Claude Code agent that does the
build: it implements, **verifies by running**, and pushes. The chat plans; the
agent implements.
**tmux + Claude Code split.** In practice that's a Claude Code agent running in a
tmux pane doing the build work while you plan/review in another. One spec in, a
verified change out.
**claudedo (optional, hands-free).** Voice control for Claude Code over tmux: a
wake-word plus local Whisper speech-to-text drives your tmux session without the
keyboard — handy when you're fullscreen or away from the desk. It's available
here:
[git.rethinkstudios.io/rethink-software/claudedo](https://git.rethinkstudios.io/rethink-software/claudedo).
**The `.claude/` project convention.** Every project has a top-level `.claude/`
folder (always gitignored — nothing under it is committed):
- **`CLAUDE.md`** — project-specific instructions for the agent (stack, layout,
conventions). Layered on top of your global instructions.
- **`compact.md`** — a running state log (done / decided / in-flight), updated at
checkpoints so a fresh session catches up fast.
- **`commands.log`** — an append-only record of shell commands run.
- **`spec/`** — numbered, per-change specs named `NN-<type>-<short>.md`
(e.g. `01-feature-logging.md`).
Tell the agent to **"setup project"** to scaffold all of that (and add `.claude/`
to `.gitignore`) in a new repo.
**AI-assist habits.** When you're stuck, give the AI the *exact* context instead
of describing it — the WSL clipboard bridge makes this trivial:
```bash
git diff | clip.exe # then paste the diff straight into the chat
```
Use AI for the plan and the spec, let the agent build, and always **prove it
works by running it** — don't accept "this should work."
!!! tip "Verify by executing, not asserting"
The single habit that separates good AI-assisted work from plausible-looking
nonsense: **run it**. A passing build, a real screenshot, actual output — that
is proof. "It should work" is not.
## Recommended setup
- **WSL2 (Ubuntu) + VS Code + Claude Code.** Develop in WSL, edit in VS Code over
[Remote — WSL](https://code.visualstudio.com/docs/remote/wsl), and run
[Claude Code](https://docs.claude.com/en/docs/claude-code/overview) as your
agent (terminal or the VS Code extension).
- **[pyenv](https://github.com/pyenv/pyenv)** for per-project Python versions
(we target **3.10+**) and isolated `.venv`s — full setup on the
[Virtual environments](environments.md) page.
- **[flake8](https://flake8.pycqa.org/)** with a shared config — max line length
**120** (see the alias below).
## Handy shell setup
Copy-paste these into your `.zshrc` / `.bashrc`. Replace every placeholder with
your real values. The high-value ones are explained underneath.
```bash
# --- WSL <-> Windows bridges ---
alias explorer="explorer.exe ." # open current dir in Windows Explorer
# pipe to the Windows clipboard (great for pasting context into AI):
# git diff | clip.exe -> paste the diff straight into a chat
# auto-alias Windows .exe tools on PATH (e.g. adb/platform-tools):
export PATH="$PATH:/mnt/c/<your>/platform-tools"
for exe in /mnt/c/<your>/platform-tools/*.exe; do
alias "$(basename "${exe}" .exe)"="${exe}"
done
# --- pyenv (Python version management) ---
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init --path)"
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"
# --- flake8 with a shared config ---
alias flake8='flake8 --config ~/.config/flake8'
# --- per-project git identity (switch identity per repo) ---
# set the LOCAL (per-repo) user + the key to sign/push with:
alias gitsetup='git config --local user.name "<you>"; \
git config --local user.email "dev@<you>"; \
git config --local core.sshCommand "ssh -i $HOME/.ssh/<key>"'
# a second identity for a different account, same pattern:
alias gitea='git config --local user.name "<alt>"; \
git config --local user.email "<alt>@<host>"; \
git config --local core.sshCommand "ssh -i $HOME/.ssh/<alt-key>"'
# --- git log graph ---
alias gl='git log --graph --abbrev-commit --pretty=oneline --decorate'
# --- local bins on PATH ---
export PATH="$HOME/.local/bin:$PATH"
export PATH="$HOME/.npm-global/bin:$PATH"
```
```bash
# cherry-pick a commit onto master/main quickly
gitcs() {
if [ -z "$1" ]; then echo "Usage: gitcs <commit>"; return 1; fi
git checkout master && git cherry-pick "$1"
}
```
**The ones worth understanding:**
- **`git diff | clip.exe`** — the killer WSL trick for AI-assisted dev. Pipe your
working changes straight to the Windows clipboard and paste them into the chat
so the AI sees *exactly* what changed instead of your paraphrase of it.
- **`gitsetup` / `gitea`** — per-repo identity. One machine, multiple Gitea
identities and keys; run the alias inside a repo to set its **local** user and
signing/push key, so commits attribute correctly without touching your global
config.
- **`gl`** — a readable branch graph for understanding history at a glance.
- **`pyenv`** — per-project Python versions, so each repo builds against the
version it targets.
## Paste service
A shared, self-hosted pastebin at
[paste.rethinkstudios.io](https://paste.rethinkstudios.io) — for quickly sharing
logs, snippets, or command output when you're pairing, filing an issue, or handing
output to a coding agent. Pastes are **unlisted** (random URL), **expire** by
default, and support **burn-after-read**. Anonymous — no login.
The easy way: pipe anything into it and get back a URL. Drop this into your
`.zshrc` / `.bashrc` — a **function** is preferred over a plain alias because it
reads stdin cleanly and has room to grow options (needs `jq` + `curl`):
=== "Function (recommended)"
```bash
# paste stdin to the rethink paste service, print the URL. usage: cat file | pb
pb() {
jq -Rns '{text: inputs, expires: 259200}' \
| curl -s -H 'Content-Type: application/json' --data-binary @- https://paste.rethinkstudios.io/ \
| jq -r '"https://paste.rethinkstudios.io" + .path'
}
```
=== "Alias (alternative)"
Same behaviour as a one-liner — note the extra escaping the alias form needs:
```bash
alias pb="jq -Rns '{text: inputs, expires: 259200}' | curl -s -H 'Content-Type: application/json' --data-binary @- https://paste.rethinkstudios.io/ | jq -r '\"https://paste.rethinkstudios.io\" + .path'"
```
Usage — pipe any file or command output straight in:
```bash
cat latest.log | pb # -> https://paste.rethinkstudios.io/xxxxxxx
mycommand 2>&1 | pb # pipe any command's output (stderr too)
```
`expires` is in **seconds** — `259200` = 72h (the default). Change it (e.g.
`86400` for 24h) or drop the field entirely.
!!! info "Rate-limited on creation"
The service rate-limits paste **creation** (not viewing), so it's built for
occasional shares — not bulk or automated posting.
+21 -17
View File
@@ -1,26 +1,24 @@
site_name: Rethink Studios Handbook
site_name: rethink development
site_description: Libraries, coding standards, and how to deploy on our network.
site_url: https://docs.rethinkstudios.io/
copyright: Rethink Studios
copyright: rethink development (handbook)
extra_css:
- stylesheets/extra.css
extra_javascript:
- javascripts/mermaid.min.js
- javascripts/mermaid-init.js
theme:
name: material
language: en
logo: assets/logo.svg
favicon: assets/favicon.svg
palette:
- media: "(prefers-color-scheme: light)"
scheme: default
primary: indigo
accent: indigo
toggle:
icon: material/weather-night
name: Switch to dark mode
- media: "(prefers-color-scheme: dark)"
scheme: slate
primary: indigo
accent: indigo
toggle:
icon: material/weather-sunny
name: Switch to light mode
scheme: slate
primary: custom
accent: custom
features:
- navigation.instant
- navigation.tracking
@@ -41,7 +39,11 @@ markdown_extensions:
- toc:
permalink: true
- pymdownx.details
- pymdownx.superfences
- pymdownx.superfences:
custom_fences:
- name: mermaid
class: mermaid
format: !!python/name:pymdownx.superfences.fence_div_format
- pymdownx.tabbed:
alternate_style: true
- pymdownx.highlight:
@@ -59,4 +61,6 @@ nav:
- Home: index.md
- Libraries: libraries.md
- Standards: standards.md
- Workflow: workflow.md
- Virtual environments: environments.md
- Deploy: deploy.md
+10
View File
@@ -0,0 +1,10 @@
# Build dependencies for the handbook docs site.
# Host build: python -m venv .venv && . .venv/bin/activate
# pip install -r requirements.txt && mkdocs build
# Output: static site/ served by the reverse proxy at docs.rethinkstudios.io.
mkdocs-material==9.7.6 # theme — pulls mkdocs, pymdown-extensions, pygments
mkdocs==1.6.1 # pinned explicitly for a reproducible build
mkdocs-material-extensions==1.3.1
pymdown-extensions==11.0 # admonitions, tabbed, superfences, annotations
Pygments==2.20.0 # syntax highlighting in code blocks