diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a76bc9c --- /dev/null +++ b/.env.example @@ -0,0 +1,14 @@ +# Copy to .env (gitignored). Source recommended: +# echo "UID=$(id -u)" > .env +# echo "GID=$(id -g)" >> .env + +# Match host UID/GID for /workspace permissions +UID=1000 +GID=1000 + +# Set MOBILE=1 to activate mobile-optimized prompt + screen autoattach +MOBILE=0 + +# Git identity used inside the container +GIT_NAME=Your Name +GIT_EMAIL=you@example.com diff --git a/.gitignore b/.gitignore index 2fea26c..6be0b0d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,8 @@ # Environment / secrets .env -.env.* +.env.local +.env.*.local !.env.example # Editor diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..11f4621 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,153 @@ +# arch-dev — Project Context for Claude Code + +## What this is +A portable, stateless Arch Linux development environment running in Docker. +Headless, riced, mobile-aware. Built for serious dev work from any box — +including from a phone via Termius. + +The tagline that stuck: **"Riced Neovim IDE"** + +--- + +## The Human + +Wayne. Works primarily from mobile (Android + Termius). +Prefers vim keybindings everywhere. Wants things clean and professional — +"Apple good" was the benchmark set early on. Doesn't want walls of text. +Will call you out if you guess instead of reading docs. +Has a Gitea instance at https://code.waynehayesdevelopment.com + +--- + +## Architecture + +``` +arch-dev/ +├── Dockerfile # Arch rolling release, pacman + AUR (yay) +├── docker-compose.yml # Named volume for stateful /home/dev +├── entrypoint.sh # First-run seeding + dotfile update detection +├── .gitignore +├── workspace/ # Bind mount — host-visible project files +└── dotfiles/ # Baked into /etc/skel-arch-dev/ in image + ├── .zshrc # 256-color only, mobile detection, snapshot fns + ├── .aliases + ├── .screenrc # Mobile multiplexer + ├── .config/ + │ ├── starship.toml # Desktop: 256-color Kanagawa Wave approx + │ ├── starship-mobile.toml # Mobile: single line, no icons + │ ├── tmux/tmux.conf # Desktop mux, Ctrl+Space prefix + │ └── nvim/ # Full LSP/lint/format setup +``` + +--- + +## The Stateful Home System (v1.7) + +`/home/dev` is a **named Docker volume** (`arch-dev-home`). Persists across +`--rm` container exits. NOT wiped on exit — only on `docker volume rm`. + +On first run, entrypoint seeds the volume from `/etc/skel-arch-dev/` +(baked into the image). Subsequent runs detect if image dotfiles are newer +and auto-update (with auto-snapshot first). + +### Git-backed snapshots + +`~/.arch-dev-state/` is a separate-git-dir tracking `/home/dev`. +No `.git` folder in `~` — avoids nested git conflicts with project repos. + +Shell functions (defined in .zshrc): +- `snapshot [msg]` / `snap` — tag current state +- `snapshots` / `snaps` — list tags + recent commits +- `rollback ` / `snapr` — reset to tag (confirms first, auto-snaps) +- `diff-state` / `snapd` — what's changed since last commit +- `show-snapshot ` — inspect a tag +- `unsnapshot ` — delete a tag +- `_archdev_git` — raw git wrapper (separate git dir) + +--- + +## The Color Problem (solved in v1.4+) + +Termius on Android cannot render truecolor RGB escape codes. +They show as **purple/magenta backgrounds** on everything. + +**Rules:** +- No `COLORTERM=truecolor` in compose or shell +- No hex colors (`#7E9CD8`) in starship, zsh-syntax-highlighting, tmux +- All shell-visible configs use 256-color ANSI codes (`fg=110` etc.) +- `termguicolors` in neovim is conditional: `vim.env.MOBILE ~= "1"` +- Mobile colorscheme: `habamax` (built-in, 256-color clean) +- Desktop colorscheme: `kanagawa-wave` (needs termguicolors) +- Bufferline disabled on mobile (emits RGB for tab backgrounds) +- noice.nvim removed entirely (bled colors into terminal) + +**Kanagawa Wave 256-color approximations:** +``` +110 = crystalBlue 106 = springGreen 139 = oniViolet +173 = boatYellow 167 = peachRed 66 = waveAqua +242 = fujiGray 250 = fujiWhite 236 = waveBlue +``` + +--- + +## Mobile Detection + +`MOBILE=1` env var (set in Termius host profile) activates: +- Minimal single-line starship prompt (no icons) +- screen auto-attach instead of tmux +- habamax colorscheme in neovim +- termguicolors disabled +- Bufferline disabled +- Compact aliases (`g` for git, etc.) + +--- + +## Plugin API Changes (hard-won knowledge) + +Every one of these bit us. Read docs before touching: + +| Plugin | Issue | Fix | +|--------|-------|-----| +| `leap.nvim` | Moved GitHub→Codeberg | `url = "https://codeberg.org/andyg/leap.nvim"` | +| `leap.nvim` | `add_default_mappings()` removed | Use `(leap)` keymaps directly | +| `nvim-treesitter` | `.configs` module gone | `require("nvim-treesitter").setup{}`, `branch="main"`, `lazy=false` | +| `nvim-surround` | v4 removed `keymaps` table | `require("nvim-surround").setup()` defaults only | +| `nvim-lspconfig` | Framework deprecated in 0.11 | `vim.lsp.config()` + `vim.lsp.enable()` | +| `vim.lsp.with()` | Deprecated | Handlers in `vim.lsp.config("*", { handlers = {} })` | +| `on_attach` | Old pattern | `LspAttach` autocmd | + +--- + +## What's NOT in v1 (coming in v2) + +- nvm + nodejs +- Claude CLI +- Gemini CLI +- Aider +- nvim AI plugin (copilot/avante/codecompanion) + +v2 plan: install AI tools in the stateful container, snapshot as `ai-tools`, +then layer nodejs on top. + +--- + +## Git / Repo + +- Gitea: https://code.waynehayesdevelopment.com/wayne/neovim-ide +- `main` branch = current stable (v1.7) +- `v2` branch = active development +- `v1.7` tag = frozen reference + +Wayne is new to multi-branch git workflow — be explicit about which branch +to be on before any git operations. + +--- + +## Tone / Style Notes + +- Short responses. Wayne reads on mobile. +- No bullet-point walls. Prose or tight tables. +- Don't guess at APIs — fetch the docs first. +- When something breaks, ask for the actual error before debugging. +- Wayne will test each version and report back with screenshots. +- "Apple good" is the visual quality bar. diff --git a/Dockerfile b/Dockerfile index a7ce3b1..ddc5a32 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,11 @@ FROM archlinux:latest +# ── Build args: UID/GID matching for clean /workspace permissions ───────────── +# Override at build: UID=$(id -u) GID=$(id -g) docker compose build +# Or set in .env file +ARG USER_UID=1000 +ARG USER_GID=1000 + # ── Rolling release: full system update first, always ───────────────────────── RUN pacman -Syu --noconfirm @@ -24,10 +30,14 @@ RUN pacman -S --noconfirm --needed \ man-db man-pages \ jq tree wget \ rsync \ + imagemagick chafa jp2a \ + go \ + github-cli \ + libnewt \ && pacman -Scc --noconfirm # ── Crown Jewel #2: AUR ─────────────────────────────────────────────────────── -RUN useradd -m -s /bin/zsh -G wheel aurbuild && \ +RUN useradd -m -s /bin/zsh -u 9001 -G wheel aurbuild && \ echo 'aurbuild ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers.d/aurbuild RUN cd /tmp && \ @@ -41,24 +51,48 @@ RUN sudo -u aurbuild yay -S --noconfirm --needed \ eza \ wl-clipboard \ trash-cli \ + tailscale \ && sudo -u aurbuild yay -Scc --noconfirm -# ── Dev user ────────────────────────────────────────────────────────────────── -RUN useradd -m -s /bin/zsh -G wheel dev && \ +# ── Dev user with host-matching UID/GID ─────────────────────────────────────── +# UID/GID match host so /workspace bind mount has clean permissions both sides. +# aurbuild is parked at UID 9001 so there's no collision with host UID. +RUN set -e; \ + # Group: handle pre-existing GID gracefully (Arch base has users:1000) + if getent group ${USER_GID} >/dev/null; then \ + groupmod -n dev "$(getent group ${USER_GID} | cut -d: -f1)"; \ + else \ + groupadd -g ${USER_GID} dev; \ + fi; \ + useradd -m -s /bin/zsh -u ${USER_UID} -g ${USER_GID} -G wheel dev; \ echo 'dev ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers.d/dev # ── Skeleton: bake dotfiles into /etc/skel-arch-dev/ ────────────────────────── -# This is the SOURCE OF TRUTH. The volume gets seeded from here on first run. COPY --chown=dev:dev dotfiles/ /etc/skel-arch-dev/ # ── Initial seed of /home/dev so plugin bake works at build time ────────────── RUN cp -an /etc/skel-arch-dev/. /home/dev/ && \ chown -R dev:dev /home/dev +# ── nvm + LTS Node (as dev user) ────────────────────────────────────────────── +# nvm install script writes to ~/.zshrc — we sandbox it then merge cleanly +RUN sudo -u dev bash -c '\ + export PROFILE=/dev/null && \ + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash && \ + export NVM_DIR="/home/dev/.nvm" && \ + [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" && \ + nvm install --lts && \ + nvm alias default node \ +' + +# Persist nvm install into the skel template so volume seeding includes it +RUN cp -an /home/dev/.nvm /etc/skel-arch-dev/ && \ + chown -R dev:dev /etc/skel-arch-dev/.nvm + # ── Python tools ────────────────────────────────────────────────────────────── RUN pip install --break-system-packages pynvim httpx requests -# ── Bake neovim plugins into /etc/skel-arch-dev so they seed too ───────────── +# ── Bake neovim plugins ─────────────────────────────────────────────────────── RUN sudo -u dev HOME=/home/dev XDG_DATA_HOME=/home/dev/.local/share \ nvim --headless +"Lazy! sync" +qa 2>/dev/null; exit 0 @@ -67,7 +101,7 @@ RUN sudo -u dev HOME=/home/dev XDG_DATA_HOME=/home/dev/.local/share \ +"TSUpdateSync python bash lua json yaml toml markdown vim vimdoc regex" \ +qa 2>/dev/null; exit 0 -# Copy the fully-baked /home/dev back into the skel template +# Copy fully-baked /home/dev back into the skel template RUN cp -an /home/dev/.local /etc/skel-arch-dev/ && \ cp -an /home/dev/.cache /etc/skel-arch-dev/ 2>/dev/null || true && \ chown -R dev:dev /etc/skel-arch-dev diff --git a/README.md b/README.md index b7a7d2b..c235c71 100644 --- a/README.md +++ b/README.md @@ -1,62 +1,85 @@ -# arch-dev v1.7 +# arch-dev ### Riced Neovim IDE · Arch Linux · Stateful · Mobile-Aware > *"Like Gentoo without the compiling."* -> Kanagawa Wave · rolling release · AUR-powered · git-snapshotted home +> *I use Arch BTW* + +Kanagawa Wave · rolling release · AUR-powered · git-snapshotted home --- -## What's new in v1.7 +## Branches -- **Stateful `/home/dev`** — your shell history, plugin state, and config tweaks survive container exit -- **Git-backed snapshot system** — commit good states, roll back when things break -- **Auto-snapshot on dotfile updates** — image upgrades preserve your history -- **Mobile improvements** — bufferline disabled on mobile, dashboard uses thin-line ASCII +| Branch | Purpose | +|---|---| +| `main` | Latest stable | +| `v2` | Active development | +| `v1.7` (tag) | Frozen v1.7 reference | --- -## The Snapshot System +## What's new in v2.0 -Your `/home/dev` is a git repo (state in `~/.arch-dev-state/`). When something works, snapshot it. When something breaks, roll back. - -```bash -# Working state — save it -snap node-working "NodeJS env with nvm + pnpm" - -# List your snapshots -snaps - -# See what's changed since last snapshot -snapd - -# Try something risky, breaks things... no problem -snapr node-working - -# Show what's in a snapshot -show-snapshot node-working - -# Remove a snapshot you don't need -unsnapshot old-thing -``` - -What's tracked: dotfiles, configs, neovim plugins, anything in `~`. -What's ignored: `.cache`, `.zsh_history`, build artifacts, log files. - -Snapshots use real git so you can: -- Branch (`_archdev_git checkout -b experimenting`) -- Push to remote (`_archdev_git remote add origin git@...`) -- Diff between snapshots (`_archdev_git diff snap-a snap-b`) +- **Host UID/GID matching** — `/workspace` permissions just work, no more chowning +- **Tailscale** baked in (AUR) with `tun` device + `NET_ADMIN`/`NET_RAW` +- **Image tooling** — ImageMagick, chafa, jp2a +- **Go** — for go-based tooling +- **github-cli** (`gh`) — GitHub from terminal +- **libnewt** — provides `whiptail` for shell TUI scripts (sysmenu) +- **nvm + LTS Node** — lazy-loaded, baked into skel template +- **Capabilities settled** — pacman/sudo/tailscale all working post-`cap_drop ALL` --- ## Quick Start ```bash +# First time: create .env with your UID/GID +cp .env.example .env +echo "UID=$(id -u)" >> .env +echo "GID=$(id -g)" >> .env + +# Build (UID/GID picked up from .env) docker compose build + +# Run docker compose run --rm arch-dev ``` -First run seeds the home volume from a baked-in skeleton and creates a `skeleton` tag you can always roll back to. +First run seeds `/home/dev` from the baked-in skeleton and creates a +`skeleton` snapshot you can always roll back to. + +--- + +## Snapshot System + +Your home is a git repo (state in `~/.arch-dev-state/`). Save good states, +roll back when things break. + +```bash +snap claude-code "Claude Code installed and authed" +snaps # list snapshots +snapd # diff vs last snapshot +rollback claude-code # reset to snapshot +``` + +--- + +## Tailscale + +```bash +# Inside container, first time: +sudo tailscaled & +sudo tailscale up # follow auth URL +snap tailscale "authenticated to tailnet" +``` + +After that, tailscale state persists in the named volume. + +**Reminder learned the hard way**: Tailscale default-denies all tailnet +traffic. ACLs grant exceptions, not restrictions. Check ACLs FIRST when +peer connections fail silently. (Also: `tailscale ping ` rules out +ACL issues before you start blaming nftables/routes.) --- @@ -65,54 +88,84 @@ First run seeds the home volume from a baked-in skeleton and creates a `skeleton | Path | Type | Purpose | |---|---|---| | `/workspace` | bind mount → `./workspace` | Project files, host-visible | -| `/home/dev` | named volume `arch-dev-home` | Stateful user home, survives `--rm` | -| `/etc/skel-arch-dev/` | image layer | Read-only template, used to seed and update | +| `/home/dev` | named volume | Stateful user home | +| `/etc/skel-arch-dev/` | image layer | Read-only template | -**Reset home to factory:** -```bash -docker volume rm arch-dev_arch-dev-home -docker compose run --rm arch-dev -``` - -**Or roll back inside the container:** -```bash -rollback skeleton -``` +Reset home to factory: `docker volume rm _arch-dev-home` --- -## Image Updates +## Container Capabilities -When you rebuild the image with new dotfiles: +The container drops ALL capabilities then re-adds only what's needed: -1. Container starts, entrypoint compares image dotfiles to home -2. If image is newer, takes an auto-snapshot of current home -3. Updates dotfiles, leaving user data alone (history, project state) -4. You can `rollback` to the auto-snapshot if you don't like the new dotfiles +| Cap | Why | +|---|---| +| `NET_BIND_SERVICE` | Bind to ports < 1024 (mosh) | +| `SETUID` / `SETGID` | sudo | +| `AUDIT_WRITE` | sudoers_audit plugin | +| `NET_ADMIN` / `NET_RAW` | Tailscale | +| `CHOWN` / `DAC_OVERRIDE` / `FOWNER` | pacman | + +Plus device pass-through for `/dev/net/tun` (Tailscale kernel mode). --- -## Known Caveats +## Multi-Window Workflow -- **AUR/pacman packages** are NOT in snapshots (they live in `/usr/`, not `~`) -- **Docker volume size** grows with snapshots — `_archdev_git gc` periodically -- First `snap` after major changes can take a few seconds (lots to hash) +`docker exec` does NOT inherit cap_add from compose — it gets default +capabilities. That means pacman/sudo work in the original `docker compose +run` window but not in `docker exec` windows. + +**Best practice:** Use `tmux` inside the container for multiple panes. +All panes inherit the original session's full caps. + +```bash +tmux new -s work +# Ctrl+Space " split horizontal +# Ctrl+Space % split vertical +# Ctrl+Space d detach (container keeps running) +# tmux attach -t work +``` --- ## Mobile (Termius) -`MOBILE=1` activates: -- Minimal starship prompt +Set `MOBILE=1` in Termius host profile env vars to activate: +- Single-line minimal starship prompt - Auto-attach screen on connect -- Bufferline disabled (was showing as purple bar) -- Habamax colorscheme (kanagawa needs truecolor which Termius mangles) +- habamax colorscheme (kanagawa needs truecolor) +- termguicolors disabled in neovim +- Bufferline disabled + +With Tailscale, you can reach arch-dev from any device on your tailnet +without exposing ports — perfect for mobile dev anywhere. --- -## v2 Roadmap +## State Tracking — Two Systems -- nvm + nodejs (snapshot it after install!) -- gemini-cli for AI in the terminal -- copilot.nvim or avante.nvim -- nvim-dap (debugger) +| System | What | Where | +|---|---|---| +| **git on branch** | Dockerfile, dotfiles, build recipe | Gitea repo | +| **`snap` inside container** | Runtime state, installed tools, auth | Docker volume | + +Both required for full reproducibility — Dockerfile builds the OS, +snapshots restore the user state on top of it. + +--- + +## Roadmap + +### v2.1 (next) +- ASCIInator integration with chafa workflow refinement +- Custom neovim dashboard ASCII (using arch-dev's own logo) + +### v2.2 (in design) +- Snapshot auto-push to remote (`snap` does `git push`, with override flag) +- Audit-clean .gitignore for credential paths + +### v2.3+ (separate repos) +- **tailscale.nvim** — original FOSS neovim plugin for Tailscale interaction +- **ASCIInator** — image-to-ASCII tool diff --git a/docker-compose.yml b/docker-compose.yml index 659a4d9..a76c31d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,13 @@ services: arch-dev: - build: . + build: + context: . + args: + # Match host UID/GID for clean /workspace permissions + # Set via: UID=$(id -u) GID=$(id -g) docker compose build + # Or .env file in repo root + USER_UID: ${UID:-1000} + USER_GID: ${GID:-1000} image: arch-dev:latest container_name: arch-dev hostname: arch-dev @@ -8,11 +15,11 @@ services: tty: true volumes: - # Project files — bind mount, host-visible + # Project files — bind mount, host-visible, host-UID-owned - ./workspace:/workspace # Stateful home — named volume, survives --rm - # Reset with: docker volume rm arch-dev_arch-dev-home + # Reset with: docker volume rm _arch-dev-home - arch-dev-home:/home/dev environment: @@ -23,10 +30,24 @@ services: - GIT_COMMITTER_NAME=${GIT_NAME:-dev} - GIT_COMMITTER_EMAIL=${GIT_EMAIL:-dev@localhost} + # Capability set built up through testing — + # cap_drop ALL then re-add only what's needed. cap_drop: - ALL cap_add: - - NET_BIND_SERVICE + - NET_BIND_SERVICE # mosh, bind <1024 + - SETUID # sudo + - SETGID # sudo + - AUDIT_WRITE # sudoers_audit plugin + - NET_ADMIN # tailscale + - NET_RAW # tailscale + - CHOWN # pacman temp dir ownership + - DAC_OVERRIDE # pacman lock files + - FOWNER # pacman package ownership + + # Tailscale needs tun device for kernel-mode networking + devices: + - /dev/net/tun:/dev/net/tun volumes: arch-dev-home: diff --git a/nvm.zsh.snippet b/nvm.zsh.snippet new file mode 100644 index 0000000..e5b8949 --- /dev/null +++ b/nvm.zsh.snippet @@ -0,0 +1,33 @@ +# ── nvm (added in v2.0) ─────────────────────────────────────────────────────── +# Lazy-load nvm to keep shell startup fast +export NVM_DIR="$HOME/.nvm" +if [[ -s "$NVM_DIR/nvm.sh" ]]; then + # Stub functions that load nvm on first use + nvm() { + unset -f nvm node npm npx + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" + nvm "$@" + } + node() { + unset -f nvm node npm npx + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + node "$@" + } + npm() { + unset -f nvm node npm npx + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + npm "$@" + } + npx() { + unset -f nvm node npm npx + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + npx "$@" + } + # Add default node bin to PATH so node-installed CLIs work without lazy trigger + if [[ -d "$NVM_DIR/alias" ]] && [[ -f "$NVM_DIR/alias/default" ]]; then + DEFAULT_NODE="$(cat "$NVM_DIR/alias/default" 2>/dev/null)" + [[ -d "$NVM_DIR/versions/node/v$DEFAULT_NODE/bin" ]] && \ + PATH="$NVM_DIR/versions/node/v$DEFAULT_NODE/bin:$PATH" + fi +fi