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/Dockerfile b/Dockerfile index afb28a7..c9d46c2 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 @@ -25,6 +31,9 @@ RUN pacman -S --noconfirm --needed \ jq tree wget \ rsync \ imagemagick chafa jp2a \ + go \ + github-cli \ + libnewt \ && pacman -Scc --noconfirm # ── Crown Jewel #2: AUR ─────────────────────────────────────────────────────── @@ -45,22 +54,38 @@ RUN sudo -u aurbuild yay -S --noconfirm --needed \ 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 +RUN groupadd -g ${USER_GID} dev && \ + 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 @@ -69,7 +94,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 8f4f151..c235c71 100644 --- a/README.md +++ b/README.md @@ -18,59 +18,31 @@ Kanagawa Wave · rolling release · AUR-powered · git-snapshotted home --- -## What's in v2 +## What's new in v2.0 -### v2.0 — AI tooling -- Claude Code CLI (snapshot after install + login) -- Gemini CLI -- Aider -- nvm + Node.js (for AI tools that need it) - -### v2.1 — Image & ASCII tooling -- ImageMagick — image manipulation -- chafa — modern terminal image rendering (truecolor + sixel) -- jp2a — fast JPEG/PNG to ASCII art - -Workflow: AI rough sketch → `chafa --symbols ascii` → hand-tweak - -### v2.2 — Tailscale -- Tailscale baked in via AUR -- `/dev/net/tun` device pass-through for kernel-mode networking -- `NET_ADMIN` + `NET_RAW` capabilities granted -- First-run auth, then `snap tailscale` for persistence - -```bash -# Inside container, first time: -sudo tailscaled & -sudo tailscale up # follow auth URL -snap tailscale "authenticated to tailnet" -``` - -### v2.3 — Companion plugin (separate repo) -**`tailscale.nvim`** — original FOSS work, no equivalent exists yet: -- Lualine status component -- Telescope peer picker -- Quick IP copy + status window -- Repo: github.com//tailscale.nvim - ---- - -## v1.7 — What's there now - -- Stateful `/home/dev` via named Docker volume -- Git-backed snapshot/rollback system (`snap`, `snaps`, `rollback`) -- Auto-snapshot on dotfile updates -- Mobile detection (`MOBILE=1` for Termius) -- Kanagawa Wave colorscheme (desktop) / habamax (mobile) -- LSP/lint/format for Python, Bash, Lua -- Telescope, oil, lazygit, leap, treesitter +- **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 ``` @@ -85,14 +57,32 @@ Your home is a git repo (state in `~/.arch-dev-state/`). Save good states, roll back when things break. ```bash -snap node-working "NodeJS env with nvm + pnpm" # save state -snaps # list snapshots -snapd # diff vs last snapshot -rollback node-working # reset to snapshot +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.) + +--- + ## Volume Architecture | Path | Type | Purpose | @@ -115,20 +105,28 @@ The container drops ALL capabilities then re-adds only what's needed: | `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). --- -## State Tracking — Two Systems +## Multi-Window Workflow -| System | What | Where | -|---|---|---| -| **git on v2 branch** | Dockerfile, dotfiles, build recipe | Gitea repo | -| **`snap` inside container** | Runtime state, installed tools, auth | Docker volume | +`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. -Both required for full reproducibility — Dockerfile builds the OS, -snapshots restore the user state on top of it. +**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 +``` --- @@ -141,5 +139,33 @@ Set `MOBILE=1` in Termius host profile env vars to activate: - termguicolors disabled in neovim - Bufferline disabled -With Tailscale (v2.2+), you can reach arch-dev from any device on your -tailnet without exposing ports — perfect for mobile dev anywhere. +With Tailscale, you can reach arch-dev from any device on your tailnet +without exposing ports — perfect for mobile dev anywhere. + +--- + +## State Tracking — Two Systems + +| 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 4055215..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,19 +30,22 @@ 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 - - SETUID - - SETGID - - AUDIT_WRITE - - NET_ADMIN - - NET_RAW - - CHOWN # pacman temp dir ownership - - DAC_OVERRIDE # likely also needed for pacman lock files - - FOWNER # pacman + - 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 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