From d7e7f50493b3c2bbb4c1c560da499e8aa8513059 Mon Sep 17 00:00:00 2001 From: Brett Kinny Date: Sat, 9 May 2026 21:45:45 +1000 Subject: [PATCH 1/4] ci: publish install.sh/install.ps1 as release assets Adds .github/workflows/release.yml so tagged releases (v*) attach install.sh and install.ps1 to the GitHub Release. install.sh already self-bootstraps when curl-piped (clones the repo, then runs from there), so no separate bootstrap shim is needed. Switch the README quick-install URLs from raw.githubusercontent.com/.../main/install.sh (always serves HEAD) to releases/latest/download/install.sh. Pushes to main no longer break new installs until a release is cut. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/release.yml | 39 +++++++++++++++++++++++++++++++++++ README.md | 8 +++---- install.ps1 | 2 +- 3 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..fbef7e1 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,39 @@ +name: Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Create release with install scripts as assets + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + tag="${GITHUB_REF_NAME}" + + # Pre-release tags carry an `-rc`, `-beta`, etc. suffix. + prerelease_flag="" + case "$tag" in + *-*) prerelease_flag="--prerelease" ;; + esac + + # Idempotent: if the workflow re-runs on the same tag, replace assets. + if gh release view "$tag" >/dev/null 2>&1; then + gh release upload "$tag" install.sh install.ps1 --clobber + else + gh release create "$tag" \ + --title "$tag" \ + --generate-notes \ + $prerelease_flag \ + install.sh install.ps1 + fi diff --git a/README.md b/README.md index 2c814f7..3653696 100644 --- a/README.md +++ b/README.md @@ -64,13 +64,13 @@ assistant, and language SDKs. **Stable** - curl -fsSL https://raw.githubusercontent.com/SquareWaveSystems/squarebox/main/install.sh | bash + curl -fsSL https://github.com/SquareWaveSystems/squarebox/releases/latest/download/install.sh | bash **Edge** - curl -fsSL https://raw.githubusercontent.com/SquareWaveSystems/squarebox/main/install.sh | bash -s -- --edge + curl -fsSL https://github.com/SquareWaveSystems/squarebox/releases/latest/download/install.sh | bash -s -- --edge -Stable installs the latest tagged release (pre-release tags like `-rc` are skipped). Edge uses the latest commit on main. +Stable installs the latest tagged release (pre-release tags like `-rc` are skipped). Edge uses the latest commit on main. The install script itself is published as a release asset, so the URL is pinned to a tagged version of the script — pushes to `main` won't break new installs until a release is cut. If the install fails or you want to see the full docker build and git output, re-run with `--verbose`. @@ -80,7 +80,7 @@ Windows users can install directly from PowerShell - no Git Bash required. This handles clone, build, container creation, and PowerShell aliases (`sqrbx`, `squarebox`, etc.) natively: - irm https://raw.githubusercontent.com/SquareWaveSystems/squarebox/main/install.ps1 | iex + irm https://github.com/SquareWaveSystems/squarebox/releases/latest/download/install.ps1 | iex Once installed, you can re-run or pass flags from the local copy: diff --git a/install.ps1 b/install.ps1 index aaf679b..a9de49a 100644 --- a/install.ps1 +++ b/install.ps1 @@ -6,7 +6,7 @@ Run this from PowerShell 7+ to install squarebox. Handles clone, build, container creation, and PowerShell profile setup natively. - First install: irm https://raw.githubusercontent.com/SquareWaveSystems/squarebox/main/install.ps1 | iex + First install: irm https://github.com/SquareWaveSystems/squarebox/releases/latest/download/install.ps1 | iex Re-install: .\install.ps1 Edge: .\install.ps1 -Edge Verbose: .\install.ps1 -Verbose From 451ab54c8d3e24e82ab6b24f5789b431c7b72cad Mon Sep 17 00:00:00 2001 From: Brett Kinny Date: Sat, 9 May 2026 21:49:44 +1000 Subject: [PATCH 2/4] home: back /home/dev with a named volume + bind-mounted bashrc Switch /home/dev from in-image-only to the squarebox-home named Docker volume. Shell history, GitHub CLI auth, claude-code data, and (in a follow-up) mise toolchains now survive container recreates without the /workspace/.squarebox kludge. Image-managed config that previously lived inside the container - .bashrc - is moved to dotfiles/bashrc in the repo and bind-mounted read-only into /home/dev/.bashrc. The bind mount overrides the named volume's copy at that sub-path (same pattern starship.toml and lazygit already use), so repo updates propagate without touching user state. gh auth: stop copying to /workspace/.squarebox/gh on every change. The default ~/.config/gh location is now persistent via the named volume. The legacy path is migrated once on first run for upgraders, then ignored. The skip marker moves to ~/.squarebox-gh-skip. Verified: docker build clean; container with squarebox-home volume + bashrc bind mount loads aliases (ls/cat/g), starship, MOTD, and writes to $HOME successfully. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 6 +++- Dockerfile | 59 ++++----------------------------------- README.md | 27 ++++++++++-------- dotfiles/bashrc | 73 +++++++++++++++++++++++++++++++++++++++++++++++++ install.ps1 | 10 +++++++ install.sh | 8 ++++++ setup.sh | 32 ++++++++++------------ 7 files changed, 131 insertions(+), 84 deletions(-) create mode 100644 dotfiles/bashrc diff --git a/CLAUDE.md b/CLAUDE.md index 768ad1b..f6593c0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -squarebox is a containerized development environment (Docker or Podman) combining modern CLI/TUI tools with Claude Code. It uses a persistent container model — the container suspends on exit and resumes on restart, preserving state. Workspace code lives on the host at `~/squarebox/workspace` via volume mount. +squarebox is a containerized development environment (Docker or Podman) combining modern CLI/TUI tools with Claude Code. It uses a persistent container model — the container suspends on exit and resumes on restart, preserving state. Workspace code lives on the host at `~/squarebox/workspace` via bind mount; per-user state (shell history, gh auth, claude-code data, mise toolchains) lives in the `squarebox-home` named Docker volume; image-managed config (`.bashrc`, `starship.toml`, `lazygit/`) is bind-mounted from `~/squarebox/dotfiles` so repo updates flow through. ## Build & Run @@ -21,6 +21,8 @@ docker run -it --name squarebox \ -v ~/.ssh/config:/home/dev/.ssh/config:ro \ -v ~/.ssh/known_hosts:/home/dev/.ssh/known_hosts:ro \ -v ~/squarebox/workspace:/workspace \ + -v squarebox-home:/home/dev \ + -v ~/squarebox/dotfiles/bashrc:/home/dev/.bashrc:ro \ -v ~/.config/git:/home/dev/.config/git \ -v ~/squarebox/.config/starship.toml:/home/dev/.config/starship.toml \ -v ~/squarebox/.config/lazygit:/home/dev/.config/lazygit \ @@ -31,6 +33,8 @@ docker run -it --name squarebox \ docker start -ai squarebox ``` +The `squarebox-home` named volume holds per-user state. Bind mounts at sub-paths inside `/home/dev` (`.bashrc`, `.config/starship.toml`, `.config/lazygit`, `.config/git`) override the volume so repo-managed files stay in lockstep with the image. + Replace `docker` with `podman` above if using Podman. The `install.sh` script auto-detects the runtime; override with `SQUAREBOX_RUNTIME=docker|podman`. The `install.sh` script automates initial setup (clone, build, create container, add `sqrbx` shell alias). A `.devcontainer/devcontainer.json` is also provided for VS Code Dev Containers / Codespaces. diff --git a/Dockerfile b/Dockerfile index 76560f1..b394ad6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -132,61 +132,12 @@ ENV HOME=/home/dev ENV SQUAREBOX=1 # 7. Shell Config +# The .bashrc lives in dotfiles/ on the host so install.sh can bind-mount it +# into the container — keeping it in sync with the repo while shell history +# and per-user state stay in the squarebox-home named volume. The COPY here +# is what seeds a fresh volume; subsequent runs see the bind-mounted version. -RUN cat <<'EOFRC' >> ~/.bashrc -eval "$(starship init bash)" -eval "$(zoxide init bash --cmd cd)" -alias ls='eza --icons' -alias ll='eza -la --icons' -alias lsa='ls -a' -alias lt='eza --tree --level=2 --long --icons --git' -alias lta='lt -a' -alias cat='bat --paging=never' -alias ff="fzf --preview 'bat --style=numbers --color=always {}'" -alias eff='$EDITOR "$(ff)"' -alias ..='cd ..' -alias ...='cd ../..' -alias ....='cd ../../..' -export EDITOR='nano' -[ -f ~/.squarebox-ai-aliases ] && source ~/.squarebox-ai-aliases -[ -f ~/.squarebox-editor-aliases ] && source ~/.squarebox-editor-aliases -[ -f ~/.squarebox-tui-aliases ] && source ~/.squarebox-tui-aliases -[ -f ~/.squarebox-sdk-paths ] && source ~/.squarebox-sdk-paths -alias g='git' -alias gcm='git commit -m' -alias gcam='git commit -a -m' -alias gcad='git commit -a --amend' -export PATH="$HOME/.local/bin:$PATH" -# First-run setup -if [ ! -f ~/.squarebox-setup-done ]; then - if [ -n "${DEVCONTAINER:-}" ]; then - touch ~/.squarebox-setup-done - else - ~/setup.sh && touch ~/.squarebox-setup-done - [ -f ~/.squarebox-ai-aliases ] && source ~/.squarebox-ai-aliases - [ -f ~/.squarebox-editor-aliases ] && source ~/.squarebox-editor-aliases - [ -f ~/.squarebox-tui-aliases ] && source ~/.squarebox-tui-aliases - [ -f ~/.squarebox-sdk-paths ] && source ~/.squarebox-sdk-paths - fi -fi -# Hand off to zsh if the user opted in via setup.sh (experimental). -# SQUAREBOX_IN_ZSH guards against re-exec loops; SQUAREBOX_NO_ZSH lets -# users force bash for one shell without removing the marker. -if [ -f ~/.squarebox-use-zsh ] && [ -z "${SQUAREBOX_IN_ZSH:-}" ] && [ -z "${SQUAREBOX_NO_ZSH:-}" ] && command -v zsh >/dev/null 2>&1; then - export SQUAREBOX_IN_ZSH=1 - exec zsh -l -fi -# Hand off to fish if the user opted in via setup.sh (experimental). -# SQUAREBOX_IN_FISH guards against re-exec loops; SQUAREBOX_NO_FISH lets -# users force bash for one shell without removing the marker. -if [ -f ~/.squarebox-use-fish ] && [ -z "${SQUAREBOX_IN_FISH:-}" ] && [ -z "${SQUAREBOX_NO_FISH:-}" ] && command -v fish >/dev/null 2>&1; then - export SQUAREBOX_IN_FISH=1 - exec fish -l -fi -EOFRC - -# Display MOTD on interactive shell login -RUN echo '~/motd.sh' >> ~/.bashrc +COPY --chown=dev:dev dotfiles/bashrc /home/dev/.bashrc WORKDIR /workspace CMD ["/bin/bash"] diff --git a/README.md b/README.md index 3653696..ba30d5c 100644 --- a/README.md +++ b/README.md @@ -103,9 +103,11 @@ PowerShell 7+. The container is persistent: it suspends on exit and resumes on start, keeping installed packages, config, and shell history intact between sessions. Your -code and tool config live on the host under `~/squarebox` (`workspace/` for -code, `.config/` for tool config) via volume mounts, so they survive even if -the container is deleted. +code lives on the host at `~/squarebox/workspace` (bind-mounted), and per-user +state — shell history, GitHub CLI auth, claude-code data, mise toolchains — +lives in a named Docker volume (`squarebox-home`) that survives container +recreation. Image-managed config like `.bashrc` is bind-mounted from the repo +so updates flow through to the running container. What's included --------------- @@ -290,22 +292,23 @@ preserved. sqrbx-rebuild Pulls the latest changes, rebuilds the image, and replaces the container. -Your code in ~/squarebox/workspace is safe since it lives on the host. -Setup selections (AI tool, editors, SDKs, GitHub auth) are persisted in the -workspace volume and restored automatically. However, shell history, manually -installed packages, and custom config files inside the container are lost. +Your code in ~/squarebox/workspace is safe since it lives on the host. Most +in-container state (shell history, GitHub auth, SDK toolchains) survives +because /home/dev is backed by the `squarebox-home` named Docker volume. +Manually installed apt packages are still lost, since the image is rebuilt. #### What survives a rebuild | Survives | Lost | |----------|------| -| Code in ~/squarebox/workspace (host volume) | Shell history (~/.bash_history) | -| Starship and lazygit config (host volume) | Manually installed apt packages | -| AI tool / editor / SDK selections | Custom dotfiles in /home/dev/ | -| GitHub CLI auth | Caches and temp files | +| Code in ~/squarebox/workspace (host bind mount) | Manually installed apt packages | +| /home/dev (squarebox-home named volume): shell history, GitHub CLI auth, claude-code data, mise toolchains | | +| Starship, lazygit, .bashrc (bind-mounted from repo, picks up updates) | | +| AI tool / editor / SDK selections (in /workspace/.squarebox) | | | SSH keys (on host, forwarded via agent) | | -To preserve extra files across rebuilds, store them in `/workspace/.squarebox/`. +To wipe per-user state and start fresh, remove the named volume: +`docker volume rm squarebox-home`. > **Tip:** Use `sqrbx-update` from inside the container to update tools without > rebuilding. Only use `sqrbx-rebuild` when the base image itself needs to diff --git a/dotfiles/bashrc b/dotfiles/bashrc new file mode 100644 index 0000000..cd11735 --- /dev/null +++ b/dotfiles/bashrc @@ -0,0 +1,73 @@ +# squarebox /home/dev/.bashrc — managed by the repo, bind-mounted read-only +# from $INSTALL_DIR/dotfiles/bashrc by install.sh / install.ps1. Edits to this +# file inside the container will not persist across rebuilds; edit the copy in +# ~/squarebox/dotfiles/bashrc on the host instead. + +# Bail out if not running interactively. +case $- in + *i*) ;; + *) return ;; +esac + +# History +HISTCONTROL=ignoreboth +HISTSIZE=10000 +HISTFILESIZE=20000 +shopt -s histappend +shopt -s checkwinsize + +# squarebox additions +eval "$(starship init bash)" +eval "$(zoxide init bash --cmd cd)" +alias ls='eza --icons' +alias ll='eza -la --icons' +alias lsa='ls -a' +alias lt='eza --tree --level=2 --long --icons --git' +alias lta='lt -a' +alias cat='bat --paging=never' +alias ff="fzf --preview 'bat --style=numbers --color=always {}'" +alias eff='$EDITOR "$(ff)"' +alias ..='cd ..' +alias ...='cd ../..' +alias ....='cd ../../..' +export EDITOR='nano' +[ -f ~/.squarebox-ai-aliases ] && source ~/.squarebox-ai-aliases +[ -f ~/.squarebox-editor-aliases ] && source ~/.squarebox-editor-aliases +[ -f ~/.squarebox-tui-aliases ] && source ~/.squarebox-tui-aliases +[ -f ~/.squarebox-sdk-paths ] && source ~/.squarebox-sdk-paths +alias g='git' +alias gcm='git commit -m' +alias gcam='git commit -a -m' +alias gcad='git commit -a --amend' +export PATH="$HOME/.local/bin:$PATH" + +# First-run setup +if [ ! -f ~/.squarebox-setup-done ]; then + if [ -n "${DEVCONTAINER:-}" ]; then + touch ~/.squarebox-setup-done + else + ~/setup.sh && touch ~/.squarebox-setup-done + [ -f ~/.squarebox-ai-aliases ] && source ~/.squarebox-ai-aliases + [ -f ~/.squarebox-editor-aliases ] && source ~/.squarebox-editor-aliases + [ -f ~/.squarebox-tui-aliases ] && source ~/.squarebox-tui-aliases + [ -f ~/.squarebox-sdk-paths ] && source ~/.squarebox-sdk-paths + fi +fi + +# Hand off to zsh if the user opted in via setup.sh (experimental). +# SQUAREBOX_IN_ZSH guards against re-exec loops; SQUAREBOX_NO_ZSH lets +# users force bash for one shell without removing the marker. +if [ -f ~/.squarebox-use-zsh ] && [ -z "${SQUAREBOX_IN_ZSH:-}" ] && [ -z "${SQUAREBOX_NO_ZSH:-}" ] && command -v zsh >/dev/null 2>&1; then + export SQUAREBOX_IN_ZSH=1 + exec zsh -l +fi + +# Hand off to fish if the user opted in via setup.sh (experimental). +# SQUAREBOX_IN_FISH guards against re-exec loops; SQUAREBOX_NO_FISH lets +# users force bash for one shell without removing the marker. +if [ -f ~/.squarebox-use-fish ] && [ -z "${SQUAREBOX_IN_FISH:-}" ] && [ -z "${SQUAREBOX_NO_FISH:-}" ] && command -v fish >/dev/null 2>&1; then + export SQUAREBOX_IN_FISH=1 + exec fish -l +fi + +[ -x ~/motd.sh ] && ~/motd.sh diff --git a/install.ps1 b/install.ps1 index a9de49a..1b91297 100644 --- a/install.ps1 +++ b/install.ps1 @@ -173,8 +173,18 @@ git: # --- Create container --- Write-Host "Creating container..." +# Volume strategy: a single named volume backs /home/dev so shell history, +# claude-code state, mise toolchains, and gh auth survive container recreates. +# Bind mounts at sub-paths inside /home/dev override the volume - that's how +# we keep image-managed config (bashrc, starship.toml, lazygit) in lockstep +# with the repo while user state stays in the volume. +$homeVolume = if ($env:SQUAREBOX_HOME_VOLUME) { $env:SQUAREBOX_HOME_VOLUME } else { 'squarebox-home' } +$bashrcPath = Join-Path $InstallDir 'dotfiles\bashrc' + $rtVolumes = @( '-v', "$workspaceDir`:/workspace" + '-v', "$homeVolume`:/home/dev" + '-v', "${bashrcPath}:/home/dev/.bashrc:ro" '-v', "$gitCfgDir`:/home/dev/.config/git" '-v', "${starshipDest}:/home/dev/.config/starship.toml" '-v', "${lazygitDir}:/home/dev/.config/lazygit" diff --git a/install.sh b/install.sh index 61cc365..475ee32 100755 --- a/install.sh +++ b/install.sh @@ -397,8 +397,16 @@ fi echo "Creating container..." RT_OPTS=() +# Volume strategy: a single named volume backs /home/dev so shell history, +# claude-code state, mise toolchains, and gh auth survive container recreates. +# Bind mounts at sub-paths inside /home/dev override the volume — that's how +# we keep image-managed config (bashrc, starship.toml, lazygit) in lockstep +# with the repo while user state stays in the volume. +HOME_VOLUME="${SQUAREBOX_HOME_VOLUME:-squarebox-home}" RT_VOLUMES=( -v "${INSTALL_DIR}/workspace:/workspace" + -v "${HOME_VOLUME}:/home/dev" + -v "${INSTALL_DIR}/dotfiles/bashrc:/home/dev/.bashrc:ro" -v "${USER_HOME}/.config/git:/home/dev/.config/git" -v "${INSTALL_DIR}/.config/starship.toml:/home/dev/.config/starship.toml" -v "${INSTALL_DIR}/.config/lazygit:/home/dev/.config/lazygit" diff --git a/setup.sh b/setup.sh index fa80b9b..3e9da66 100755 --- a/setup.sh +++ b/setup.sh @@ -173,12 +173,20 @@ if should_run git; then fi fi -# Restore GitHub CLI config from persistent storage if available -GH_PERSIST="/workspace/.squarebox/gh" -GH_SKIP_MARKER="/workspace/.squarebox/gh-skip" -if [ -d "$GH_PERSIST" ] && [ ! -d ~/.config/gh ]; then +# GitHub CLI config lives at ~/.config/gh (the gh default) and is preserved +# by the squarebox-home named volume. The legacy persistence path +# (/workspace/.squarebox/gh) is migrated once for users upgrading from a +# pre-named-volume install; new installs never touch it. +GH_PERSIST_LEGACY="/workspace/.squarebox/gh" +GH_SKIP_MARKER="$HOME/.squarebox-gh-skip" +GH_SKIP_MARKER_LEGACY="/workspace/.squarebox/gh-skip" +if [ -d "$GH_PERSIST_LEGACY" ] && [ ! -d ~/.config/gh ]; then mkdir -p ~/.config - cp -r "$GH_PERSIST" ~/.config/gh + cp -r "$GH_PERSIST_LEGACY" ~/.config/gh +fi +# Migrate legacy skip marker into $HOME so it persists with the rest of state. +if [ -f "$GH_SKIP_MARKER_LEGACY" ] && [ ! -f "$GH_SKIP_MARKER" ]; then + touch "$GH_SKIP_MARKER" fi # GitHub CLI (optional — users who don't use GitHub can skip this) @@ -199,10 +207,6 @@ if should_run github; then fi if $_do_reauth; then BROWSER=echo gh auth login - if gh auth status &>/dev/null; then - mkdir -p "$GH_PERSIST" - cp -r ~/.config/gh/. "$GH_PERSIST"/ - fi fi else echo "GitHub CLI: already authenticated" @@ -223,11 +227,9 @@ if should_run github; then echo "Logging into GitHub..." BROWSER=echo gh auth login if gh auth status &>/dev/null; then - mkdir -p "$GH_PERSIST" - cp -r ~/.config/gh/. "$GH_PERSIST"/ rm -f "$GH_SKIP_MARKER" else - echo "GitHub CLI auth was not completed — skipping config persistence" + echo "GitHub CLI auth was not completed — skipping" fi fi else @@ -244,18 +246,14 @@ if should_run github; then *) do_gh_login=true ;; esac fi - mkdir -p /workspace/.squarebox if $do_gh_login; then echo "Logging into GitHub..." # BROWSER=echo makes gh print the auth URL instead of trying to open a browser BROWSER=echo gh auth login - # Persist gh config for future rebuilds (only if auth succeeded) if gh auth status &>/dev/null; then - mkdir -p "$GH_PERSIST" - cp -r ~/.config/gh/. "$GH_PERSIST"/ rm -f "$GH_SKIP_MARKER" else - echo "GitHub CLI auth was not completed — skipping config persistence" + echo "GitHub CLI auth was not completed — skipping" fi else touch "$GH_SKIP_MARKER" From ccfa222412baa4d2f3c2614b74e4e2e4efb3456b Mon Sep 17 00:00:00 2001 From: Brett Kinny Date: Sat, 9 May 2026 22:00:48 +1000 Subject: [PATCH 3/4] sdk: replace nvm/uv/Go/.NET/rustup with mise Drops five separate per-language installers (nvm, astral.sh/uv, go.dev tarball, dotnet-install.sh, rustup) and the ~/.squarebox-sdk-paths shell-init kludge. mise now manages all SDKs through ~/.config/mise/config.toml; `mise activate {bash,zsh,fish}` in the relevant rc file wires shims onto PATH. mise itself joins the Dockerfile-pinned tier (tools.yaml dockerfile group, ARG MISE_VERSION, checksums.txt entry, sqrbx-update aware). update-versions.sh now bumps MISE_VERSION alongside the others. setup.sh: install_node/python/go/dotnet/rust collapse to thin wrappers around _install_mise_sdk. ensure_node_for_npm uses mise too. Selection flow (gum + /workspace/.squarebox/sdks) is unchanged so re-runs and the AI-tools auto-Node path keep working. Apt deps: gpg (for mise's signature verification of Node releases) and libatomic1 (required by official Node Linux binaries) are now installed unconditionally; the post-install gnupg purge is dropped since mise needs it at runtime. Verified end-to-end: setup.sh non-interactive run with sdks=node,go installs both via mise; `mise ls --current` shows them; `node` and `go` are on PATH after `mise activate bash --shims`. Build CI's tools list and e2e-test's SDK assertions updated accordingly. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/build.yml | 2 +- Dockerfile | 14 ++- README.md | 37 ++++--- checksums.txt | 3 + dotfiles/bashrc | 4 +- scripts/e2e-test.sh | 16 +-- scripts/lib/tools.yaml | 9 ++ scripts/squarebox-update.sh | 7 +- scripts/update-versions.sh | 1 + setup.sh | 199 +++++++++++------------------------- 10 files changed, 122 insertions(+), 170 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b03da62..9d822e5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,7 +49,7 @@ jobs: - name: All tools exist run: | docker run --rm squarebox:test bash -c ' - for cmd in bat curl delta difft eza fd fzf gh glow gum jq just nano rg starship xh yq zoxide; do + for cmd in bat curl delta difft eza fd fzf gh glow gum jq just mise nano rg starship xh yq zoxide; do which "$cmd" || { echo "MISSING: $cmd"; exit 1; } done ' diff --git a/Dockerfile b/Dockerfile index b394ad6..f6c0820 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,6 +21,11 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ toilet-fonts \ libicu-dev \ locales \ + # Runtime deps for mise-managed SDKs: + # gpg — mise verifies upstream signatures (Node, etc.) + # libatomic1 — required by official Node Linux builds + gpg \ + libatomic1 \ && sed -i '/en_US.UTF-8/s/^# //' /etc/locale.gen \ && locale-gen \ && rm -rf /var/lib/apt/lists/* \ @@ -41,6 +46,7 @@ ARG GLOW_VERSION=2.1.2 ARG GUM_VERSION=0.17.0 ARG JUST_VERSION=1.49.0 ARG DIFFTASTIC_VERSION=0.68.0 +ARG MISE_VERSION=2026.5.4 # Validate version ARGs are non-empty RUN test -n "$DELTA_VERSION" || { echo "Error: DELTA_VERSION is empty" >&2; exit 1; } \ @@ -50,7 +56,8 @@ RUN test -n "$DELTA_VERSION" || { echo "Error: DELTA_VERSION is empty" >&2 && test -n "$GLOW_VERSION" || { echo "Error: GLOW_VERSION is empty" >&2; exit 1; } \ && test -n "$GUM_VERSION" || { echo "Error: GUM_VERSION is empty" >&2; exit 1; } \ && test -n "$JUST_VERSION" || { echo "Error: JUST_VERSION is empty" >&2; exit 1; } \ - && test -n "$DIFFTASTIC_VERSION" || { echo "Error: DIFFTASTIC_VERSION is empty" >&2; exit 1; } + && test -n "$DIFFTASTIC_VERSION" || { echo "Error: DIFFTASTIC_VERSION is empty" >&2; exit 1; } \ + && test -n "$MISE_VERSION" || { echo "Error: MISE_VERSION is empty" >&2; exit 1; } # Checksum verification infrastructure COPY checksums.txt /tmp/checksums.txt @@ -77,8 +84,8 @@ RUN mkdir -p -m 755 /etc/apt/keyrings \ # Install from repos && apt-get update \ && apt-get install -y --no-install-recommends gh eza \ - # Purge build-only dependency - && apt-get purge -y --auto-remove gnupg \ + # Note: gnupg is kept (also installed in layer 1 as `gpg`) — mise needs it + # at runtime to verify Node release signatures. && rm -rf /var/lib/apt/lists/* # Build-time tool install helper: sources library + wires up checksum verification @@ -93,6 +100,7 @@ RUN . /tmp/sb-init.sh && sb_install gum "$GUM_VERSION" RUN . /tmp/sb-init.sh && sb_install starship "$STARSHIP_VERSION" RUN . /tmp/sb-init.sh && sb_install just "$JUST_VERSION" RUN . /tmp/sb-init.sh && sb_install difftastic "$DIFFTASTIC_VERSION" +RUN . /tmp/sb-init.sh && sb_install mise "$MISE_VERSION" # Clean up build-time files RUN rm -f /tmp/checksums.txt /tmp/tools.yaml /tmp/tool-lib.sh /tmp/sb-init.sh diff --git a/README.md b/README.md index ba30d5c..aa01531 100644 --- a/README.md +++ b/README.md @@ -214,19 +214,23 @@ AI/editor/TUI/SDK selections are translated from their bash files into > Set `SQUAREBOX_NO_ZSH=1` or `SQUAREBOX_NO_FISH=1` to force bash for a single > session, or re-run `sqrbx-setup shell` to switch back permanently. Tooling > is primarily tested against bash, so a few edge cases may need polish — -> please file an issue if you hit one. Known fish limitation: nvm (Node.js) -> is not wired into fish; install [nvm.fish](https://github.com/jorgebucaran/nvm.fish) -> separately if you need it. +> please file an issue if you hit one. SDK shims are wired into all three +> shells via `mise activate {bash,zsh,fish}`. ### SDKs -| SDK | Installed via | -|-----|---------------| -| Node.js | [nvm](https://github.com/nvm-sh/nvm) | -| Python | [uv](https://github.com/astral-sh/uv) | -| Go | [go.dev](https://go.dev) | -| .NET | [dotnet-install](https://dot.net) | -| Rust | [rustup](https://rustup.rs) | +All SDKs are managed by [mise](https://github.com/jdx/mise) — a single +polyglot version manager. Selections are written to `~/.config/mise/config.toml` +and `mise activate` wires up shims and PATH automatically across bash, zsh, +and fish. + +| SDK | mise tool | +|-------|-----------| +| Node.js | `node` | +| Python | `python` | +| Go | `go` | +| .NET | `dotnet` | +| Rust | `rust` | Aliases ------- @@ -332,7 +336,7 @@ First-run selections add to that: | micro / edit | ~12 / ~7 MB | | fresh / nvim | ~10 / ~45 MB | | Node.js | ~90 MB | -| Python (uv) | ~35 MB | +| Python | ~50 MB | | Go | ~500 MB | | .NET | ~800 MB | @@ -345,16 +349,17 @@ Security Base image tools are pinned to specific versions and verified against SHA256 checksums when the Docker image is built, so `docker build` is reproducible. -Optional tools selected during first-run setup (editors, TUIs, OpenCode, nvm, -Go, zellij) install the latest upstream release at the time you run setup. The +Optional tools selected during first-run setup (editors, TUIs, OpenCode, +zellij) install the latest upstream release at the time you run setup. The trust model is the same as running each tool's installer yourself: HTTPS downloads from the project's official GitHub release (or upstream server). You get new features without waiting for a squarebox release, at the cost of build-time pinning for that tier. -Third-party install scripts (Claude Code, uv, .NET) delegate to the vendor -installer. npm-based AI tools (Copilot CLI, Gemini CLI, Codex CLI) use npm's -built-in integrity verification. +SDKs (Node, Python, Go, .NET, Rust) are installed by [mise](https://github.com/jdx/mise), +which is itself a Dockerfile-tier pinned binary. mise downloads each SDK +toolchain from its upstream over HTTPS using its own integrity checks. npm-based +AI tools (Copilot CLI, Gemini CLI, Codex CLI) use npm's built-in integrity verification. For the full trust model (what `install.sh` does on your machine, how each layer is verified, and how to inspect the script before running it) see diff --git a/checksums.txt b/checksums.txt index d1b791b..6acbff9 100644 --- a/checksums.txt +++ b/checksums.txt @@ -26,3 +26,6 @@ b0b9ed95cbf7c8b7073f17b9591811f5c001e33c7cfd066ca83ce8a07c576f9c gum_0.17.0_Lin # Difftastic 0.68.0 f50c2d77f44a551fe24a7abfa955fbb893e6d0ab2a3767f39ca3823f0995dabd difft-x86_64-unknown-linux-gnu.tar.gz faadfb3a88c194033449092fad3a86f1179738a0b3bfc44580c83473bdb17451 difft-aarch64-unknown-linux-gnu.tar.gz +# Mise 2026.5.4 +96a0eefa1ad8c92461c808e6e07644f95cda830d7719895b98c819c94b0e0b1c mise-v2026.5.4-linux-x64 +49e2d71d72d68dcb5d554724602d69f33358e1060a0352e5b02b1afcbeecc9b6 mise-v2026.5.4-linux-arm64 diff --git a/dotfiles/bashrc b/dotfiles/bashrc index cd11735..ea58bf2 100644 --- a/dotfiles/bashrc +++ b/dotfiles/bashrc @@ -34,7 +34,7 @@ export EDITOR='nano' [ -f ~/.squarebox-ai-aliases ] && source ~/.squarebox-ai-aliases [ -f ~/.squarebox-editor-aliases ] && source ~/.squarebox-editor-aliases [ -f ~/.squarebox-tui-aliases ] && source ~/.squarebox-tui-aliases -[ -f ~/.squarebox-sdk-paths ] && source ~/.squarebox-sdk-paths +command -v mise >/dev/null 2>&1 && eval "$(mise activate bash)" alias g='git' alias gcm='git commit -m' alias gcam='git commit -a -m' @@ -50,7 +50,7 @@ if [ ! -f ~/.squarebox-setup-done ]; then [ -f ~/.squarebox-ai-aliases ] && source ~/.squarebox-ai-aliases [ -f ~/.squarebox-editor-aliases ] && source ~/.squarebox-editor-aliases [ -f ~/.squarebox-tui-aliases ] && source ~/.squarebox-tui-aliases - [ -f ~/.squarebox-sdk-paths ] && source ~/.squarebox-sdk-paths + command -v mise >/dev/null 2>&1 && eval "$(mise activate bash)" fi fi diff --git a/scripts/e2e-test.sh b/scripts/e2e-test.sh index 69cb820..36acd42 100755 --- a/scripts/e2e-test.sh +++ b/scripts/e2e-test.sh @@ -187,10 +187,13 @@ suite_setup_editors() { # Run setup.sh non-interactively (uses saved selections) echo "" | ~/setup.sh 2>&1 || true - # Source SDK paths and add ~/.local/bin to PATH for this session + # Activate mise so SDK shims are visible in this session, and add + # ~/.local/bin to PATH (where opencode/editors/TUIs install). export PATH="$HOME/.local/bin:$PATH" - # shellcheck source=/dev/null - [ -f ~/.squarebox-sdk-paths ] && source ~/.squarebox-sdk-paths + if command -v mise >/dev/null 2>&1; then + eval "$(mise activate bash --shims)" + export PATH="$HOME/.local/share/mise/shims:$PATH" + fi # 3.7 editors installed run_test "3.7a opencode installed" command -v opencode @@ -211,9 +214,10 @@ suite_setup_editors() { run_test "3.8a tmux installed" command -v tmux run_test "3.8b zellij installed" command -v zellij - # 3.9 SDKs installed - run_test "3.9a node installed" command -v node - run_test "3.9b go installed" test -x "$HOME/.local/go/bin/go" + # 3.9 SDKs installed (via mise) + run_test "3.9a node installed (via mise)" command -v node + run_test "3.9b go installed (via mise)" command -v go + run_test "3.9c mise tracks node + go" sh -c 'mise ls --current 2>/dev/null | grep -E "^(node|go)\\b"' # 3.11 selections saved run_test "3.11a ai-tool config saved" test -f /workspace/.squarebox/ai-tool diff --git a/scripts/lib/tools.yaml b/scripts/lib/tools.yaml index 5ff3793..0c5d991 100644 --- a/scripts/lib/tools.yaml +++ b/scripts/lib/tools.yaml @@ -93,6 +93,15 @@ tools: dest: system group: dockerfile + mise: + repo: jdx/mise + version_prefix: v + artifact: mise-v{version}-linux-{ocarch} + method: binary + binaries: mise + dest: system + group: dockerfile + # ── setup.sh tools (installed as user to ~/.local/bin) ────────────── lazygit: diff --git a/scripts/squarebox-update.sh b/scripts/squarebox-update.sh index ffa1ba6..d73c985 100755 --- a/scripts/squarebox-update.sh +++ b/scripts/squarebox-update.sh @@ -141,6 +141,7 @@ opencode_current() { opencode --version 2>/dev/null | grep -oP '[\d.]+' | head - zellij_current() { zellij --version 2>/dev/null | head -1 | awk '{print $2}' || echo "not installed"; } just_current() { just --version 2>/dev/null | awk '{print $2}' || echo "not installed"; } difftastic_current() { difft --version 2>/dev/null | head -1 | awk '{print $2}' || echo "not installed"; } +mise_current() { mise --version 2>/dev/null | awk '{print $1}' || echo "not installed"; } # ── Latest version fetching ──────────────────────────────────────────── # Uses repo from tools.yaml via sb_get; strips v prefix where needed. @@ -160,8 +161,8 @@ tool_latest() { # ── Tool registry ────────────────────────────────────────────────────── -TOOLS=(delta yq lazygit xh yazi starship ghdash glow gum just difftastic micro fresh edit helix nvim opencode zellij) -TOOL_DISPLAY_NAMES=(delta yq lazygit xh yazi starship gh-dash glow gum just difftastic micro fresh edit helix nvim opencode zellij) +TOOLS=(delta yq lazygit xh yazi starship ghdash glow gum just difftastic mise micro fresh edit helix nvim opencode zellij) +TOOL_DISPLAY_NAMES=(delta yq lazygit xh yazi starship gh-dash glow gum just difftastic mise micro fresh edit helix nvim opencode zellij) # Map display names to tools.yaml names (ghdash → gh-dash) yaml_name() { @@ -185,7 +186,7 @@ usage() { sqrbx-update --help Show this help ${BOLD}Tools:${RESET} - delta, yq, lazygit, xh, yazi, starship, gh-dash, glow, gum, just, difftastic, micro, fresh, edit, helix, nvim, opencode, zellij + delta, yq, lazygit, xh, yazi, starship, gh-dash, glow, gum, just, difftastic, mise, micro, fresh, edit, helix, nvim, opencode, zellij EOF } diff --git a/scripts/update-versions.sh b/scripts/update-versions.sh index 87d7853..0ab35a4 100755 --- a/scripts/update-versions.sh +++ b/scripts/update-versions.sh @@ -108,6 +108,7 @@ update_arg GLOW_VERSION "${VERSIONS[glow]}" update_arg GUM_VERSION "${VERSIONS[gum]}" update_arg JUST_VERSION "${VERSIONS[just]}" update_arg DIFFTASTIC_VERSION "${VERSIONS[difftastic]}" +update_arg MISE_VERSION "${VERSIONS[mise]}" echo echo "Done. Review changes with: git diff" diff --git a/setup.sh b/setup.sh index 3e9da66..18ced5c 100755 --- a/setup.sh +++ b/setup.sh @@ -266,51 +266,60 @@ fi # Shared infrastructure (needed by multiple sections) mkdir -p /workspace/.squarebox ~/.local/bin -touch ~/.squarebox-sdk-paths # Optional tools install the latest upstream release at setup time. # Pinned versions live only in the Dockerfile tier (checksums.txt). +# +# SDKs are managed by mise (jdx/mise) — a single polyglot version manager +# replaces the previous per-language grab-bag (nvm, uv, Go tarball, .NET +# script, rustup). mise itself is installed in the Dockerfile tier and +# activated by ~/.bashrc. We write tool selections to ~/.config/mise/config.toml +# via `mise use -g`, which both registers the tool and triggers install. + +# Make mise-installed binaries visible in *this* shell. dotfiles/bashrc +# already runs `mise activate bash`, but setup.sh on first launch may execute +# in an environment where mise wasn't yet on PATH (or the shims dir is empty +# and gets repopulated mid-script). Re-eval is cheap and idempotent. +_squarebox_mise_activate() { + command -v mise >/dev/null 2>&1 || return 0 + eval "$(mise activate bash --shims)" + export PATH="$HOME/.local/share/mise/shims:$PATH" +} -_install_node_inner() { - rm -rf "$HOME/.nvm" - local nvm_tag - nvm_tag=$(sb_gh_latest_tag nvm-sh/nvm) || return 1 - curl -fsSo "${SB_TMPDIR}/nvm-install.sh" "https://raw.githubusercontent.com/nvm-sh/nvm/${nvm_tag}/install.sh" - bash "${SB_TMPDIR}/nvm-install.sh" >/dev/null 2>&1 - export NVM_DIR="$HOME/.nvm" - # shellcheck source=/dev/null - [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" - # Node.js binary verification is handled by nvm - nvm install --lts >/dev/null 2>&1 - if ! grep -q 'NVM_DIR' ~/.squarebox-sdk-paths 2>/dev/null; then - cat <<'PATHS' >> ~/.squarebox-sdk-paths -export NVM_DIR="$HOME/.nvm" -[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" -PATHS - fi +_install_mise_sdk_inner() { + mise use -g "$1@latest" >/dev/null 2>&1 } -install_node() { - if command -v node &>/dev/null; then echo "Node.js already installed, skipping."; return 0; fi - run_with_spinner "Installing Node.js (via nvm)..." _install_node_inner - # Source nvm in current shell (spinner runs in subshell) - export NVM_DIR="$HOME/.nvm" - # shellcheck source=/dev/null - [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" - if ! command -v node &>/dev/null; then - echo "Error: Node.js binary not found after installation" >&2 +_install_mise_sdk() { + local tool="$1" label="$2" + if ! command -v mise >/dev/null 2>&1; then + echo "Error: mise is not installed (expected at /usr/local/bin/mise)" >&2 + return 1 + fi + if mise which "$tool" >/dev/null 2>&1; then + echo "${label} already installed, skipping." + return 0 + fi + run_with_spinner "Installing ${label} (via mise)..." _install_mise_sdk_inner "$tool" + _squarebox_mise_activate + if ! mise which "$tool" >/dev/null 2>&1; then + echo "Error: ${label} not available after mise install" >&2 return 1 fi } +install_node() { _install_mise_sdk node "Node.js"; } +install_python() { _install_mise_sdk python "Python"; } +install_go() { _install_mise_sdk go "Go"; } +install_dotnet() { _install_mise_sdk dotnet ".NET"; } +install_rust() { _install_mise_sdk rust "Rust"; } + # Ensure Node.js is available for npm-based AI tools ensure_node_for_npm() { + _squarebox_mise_activate if command -v node &>/dev/null; then return 0; fi install_node - # Ensure node/npm are available in this session - export NVM_DIR="$HOME/.nvm" - # shellcheck source=/dev/null - [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" + _squarebox_mise_activate # Persist Node.js in SDK config so it survives rebuilds local sdk_cfg="/workspace/.squarebox/sdks" if [ -f "$sdk_cfg" ]; then @@ -1211,108 +1220,16 @@ else echo "$sdk_list" > "$SDK_CONFIG" fi -# install_node is defined earlier (shared by AI tools and SDKs sections) - -_install_python_inner() { - # Trust boundary: the uv install script manages its own binary fetching - # and verification. We rely on HTTPS for script integrity. - curl -fsSL https://astral.sh/uv/install.sh | bash &>/dev/null - if ! grep -q '\.local/bin' ~/.squarebox-sdk-paths 2>/dev/null; then - cat <<'PATHS' >> ~/.squarebox-sdk-paths -export PATH="$HOME/.local/bin:$PATH" -PATHS - fi -} - -install_python() { - if command -v uv &>/dev/null; then echo "uv already installed, skipping."; return 0; fi - run_with_spinner "Installing Python (via uv)..." _install_python_inner - export PATH="$HOME/.local/bin:$PATH" - if ! command -v uv &>/dev/null; then - echo "Error: uv binary not found after installation" >&2 - return 1 - fi -} - -_install_go_inner() { - rm -rf "$HOME/.local/go" - local go_version - go_version=$(curl -fsSL "https://go.dev/VERSION?m=text" | head -1) - if ! echo "$go_version" | grep -qE '^go[0-9]+\.[0-9]+'; then - echo "Error: unexpected Go version format: '$go_version'" >&2 - return 1 - fi - curl -fsSLo "${SB_TMPDIR}/go.tar.gz" "https://go.dev/dl/${go_version}.linux-${SB_GOARCH}.tar.gz" - tar xzf "${SB_TMPDIR}/go.tar.gz" -C ~/.local - if ! grep -q 'GOROOT' ~/.squarebox-sdk-paths 2>/dev/null; then - cat <<'PATHS' >> ~/.squarebox-sdk-paths -export GOROOT="$HOME/.local/go" -export GOPATH="$HOME/go" -export PATH="$GOROOT/bin:$GOPATH/bin:$PATH" -PATHS - fi -} - -install_go() { - if [ -x "${HOME}/.local/go/bin/go" ]; then echo "Go already installed, skipping."; return 0; fi - run_with_spinner "Installing Go..." _install_go_inner - if [ ! -x "${HOME}/.local/go/bin/go" ]; then - echo "Error: Go binary not found after installation" >&2 - return 1 - fi -} - -_install_dotnet_inner() { - rm -rf "$HOME/.dotnet" - # Trust boundary: the .NET install script manages its own binary fetching - # and verification. We rely on HTTPS for script integrity. - curl -fsSL https://dot.net/v1/dotnet-install.sh | bash -s -- --channel LTS >/dev/null 2>&1 - if ! grep -q 'DOTNET_ROOT' ~/.squarebox-sdk-paths 2>/dev/null; then - cat <<'PATHS' >> ~/.squarebox-sdk-paths -export DOTNET_ROOT="$HOME/.dotnet" -export PATH="$DOTNET_ROOT:$DOTNET_ROOT/tools:$PATH" -PATHS - fi -} - -install_dotnet() { - if [ -x "${HOME}/.dotnet/dotnet" ]; then echo ".NET already installed, skipping."; return 0; fi - run_with_spinner "Installing .NET..." _install_dotnet_inner - if [ ! -x "${HOME}/.dotnet/dotnet" ]; then - echo "Error: .NET binary not found after installation" >&2 - return 1 - fi -} - -_install_rust_inner() { - # Trust boundary: the rustup install script manages its own binary - # fetching and verification. We rely on HTTPS for script integrity. - # --no-modify-path: squarebox manages PATH via ~/.squarebox-sdk-paths. - curl --proto '=https' --tlsv1.2 -fsSL https://sh.rustup.rs \ - | sh -s -- -y --default-toolchain stable --no-modify-path &>/dev/null - if ! grep -q '\.cargo/bin' ~/.squarebox-sdk-paths 2>/dev/null; then - cat <<'PATHS' >> ~/.squarebox-sdk-paths -export PATH="$HOME/.cargo/bin:$PATH" -PATHS - fi -} - -install_rust() { - if [ -x "${HOME}/.cargo/bin/rustc" ]; then echo "Rust already installed, skipping."; return 0; fi - run_with_spinner "Installing Rust (via rustup)..." _install_rust_inner - if [ ! -x "${HOME}/.cargo/bin/rustc" ]; then - echo "Error: rustc binary not found after installation" >&2 - return 1 - fi -} +# All SDK installers (install_node/python/go/dotnet/rust) are defined earlier +# in setup.sh and delegate to mise via _install_mise_sdk. for sdk in $(echo "$sdk_list" | tr ',' ' '); do case "$sdk" in - node) install_node || echo "Warning: Node.js installation failed." ;; - python) install_python || echo "Warning: Python (uv) installation failed." ;; - go) install_go || echo "Warning: Go installation failed." ;; + node) install_node || echo "Warning: Node.js installation failed." ;; + python) install_python || echo "Warning: Python installation failed." ;; + go) install_go || echo "Warning: Go installation failed." ;; dotnet) install_dotnet || echo "Warning: .NET installation failed." ;; - rust) install_rust || echo "Warning: Rust installation failed." ;; + rust) install_rust || echo "Warning: Rust installation failed." ;; esac done fi # should_run sdks @@ -1438,7 +1355,7 @@ _install_zsh_inner() { [ -f ~/.squarebox-ai-aliases ] && source ~/.squarebox-ai-aliases [ -f ~/.squarebox-editor-aliases ] && source ~/.squarebox-editor-aliases [ -f ~/.squarebox-tui-aliases ] && source ~/.squarebox-tui-aliases - [ -f ~/.squarebox-sdk-paths ] && source ~/.squarebox-sdk-paths + command -v mise >/dev/null 2>&1 && eval "$(mise activate zsh)" alias g='git' alias gcm='git commit -m' alias gcam='git commit -a -m' @@ -1456,13 +1373,13 @@ install_zsh() { run_with_spinner "Installing Zsh + Oh My Zsh..." _install_zsh_inner } -# Translate one bash-syntax line from a ~/.squarebox-* alias/path file into -# its fish equivalent on stdout. Handles: +# Translate one bash-syntax line from a ~/.squarebox-* alias file into its +# fish equivalent on stdout. Handles: # export PATH="A:B:$PATH" → set -x PATH A B $PATH # export NAME='value' → set -x NAME value # alias name='cmd' → passed through (fish accepts this form) -# Bash-only constructs (nvm sourcing, [ -s ... ] && . ..., etc.) are dropped; -# users who need nvm in fish should install a fish plugin (e.g. nvm.fish). +# Other constructs are dropped. SDK PATHs are now handled by `mise activate +# fish` directly — this translator is only used for AI/editor/TUI aliases. _squarebox_bash_line_to_fish() { local line="$1" case "$line" in @@ -1521,7 +1438,11 @@ _install_fish_inner() { set -x EDITOR nano fish_add_path -g $HOME/.local/bin - # User selections translated from bash files at install time. + # mise (SDK manager) wires PATH and shims for whatever the user + # selected during setup. Safe to run unconditionally — fish-native. + command -v mise >/dev/null 2>&1; and mise activate fish | source + + # User AI/editor/TUI selections translated from bash files at install time. test -f $HOME/.config/fish/conf.d/squarebox-selections.fish and source $HOME/.config/fish/conf.d/squarebox-selections.fish @@ -1529,16 +1450,16 @@ _install_fish_inner() { FISHRC [ -f "$HOME/.config/fish/config.fish" ] || return 1 - # Translate AI/editor/TUI/SDK bash-syntax files into a single fish - # conf.d snippet. Regenerated each install to reflect current selections. + # Translate AI/editor/TUI bash-syntax files into a single fish conf.d + # snippet. Regenerated each install to reflect current selections. + # (SDK paths are no longer translated — mise handles that natively.) local sel_out="$HOME/.config/fish/conf.d/squarebox-selections.fish" { echo "# Generated by setup.sh from ~/.squarebox-* bash files." for src in \ "$HOME/.squarebox-ai-aliases" \ "$HOME/.squarebox-editor-aliases" \ - "$HOME/.squarebox-tui-aliases" \ - "$HOME/.squarebox-sdk-paths"; do + "$HOME/.squarebox-tui-aliases"; do [ -f "$src" ] || continue echo "# --- from $(basename "$src") ---" while IFS= read -r _sq_line; do From 942af860541b2eb2e00bf9e9d95a0ac492916800 Mon Sep 17 00:00:00 2001 From: Brett Kinny Date: Sat, 9 May 2026 22:09:18 +1000 Subject: [PATCH 4/4] docs/uninstall/test: align with mise + named-volume changes After auditing the rest of the repo for stale references: - e2e-test.sh: replace the ~/.squarebox-sdk-paths assertion (4.6d) with a check for `mise activate bash` in ~/.bashrc. - CONTRIBUTING.md: drop nvm/Go from the optional-tools list and note that SDKs now ride on mise (Dockerfile-tier pinned). - CLAUDE.md: SDK row in the first-run setup list now says "via mise"; removed nvm/Go from the optional-tools paragraph. - SECURITY.md: install URL points at the release asset; trust-model table swaps the per-SDK installer rows for a single mise row and bumps the binary-tools count to 9. - uninstall.sh / uninstall.ps1: detect the squarebox-home named volume and remove it on --purge / -Purge (kept by default, parallel to ~/squarebox/workspace). Summary lines and "kept" hints updated. - README "Uninstall" section: mention the named volume in --purge. Verified: e2e suites shell (18/18), tools (11/11), setup (3/3), update (4/4) all pass against squarebox:test. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 4 ++-- CONTRIBUTING.md | 8 +++++-- README.md | 5 ++-- SECURITY.md | 24 ++++++++++++------- scripts/e2e-test.sh | 2 +- uninstall.ps1 | 47 +++++++++++++++++++++++++++++++----- uninstall.sh | 58 ++++++++++++++++++++++++++++++++++++++------- 7 files changed, 117 insertions(+), 31 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index f6593c0..08fd770 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -51,7 +51,7 @@ The `install.sh` script automates initial setup (clone, build, create container, 4. **Text editors** — micro, edit (Microsoft), fresh, nvim (nano is always available) 5. **TUI tools** — lazygit, gh-dash, yazi (any combination) 6. **Terminal multiplexers** — tmux, zellij -7. **SDKs** — Node.js (via nvm), Python (via uv), Go, .NET, Rust (via rustup) +7. **SDKs** — Node.js, Python, Go, .NET, Rust (all installed and managed by [mise](https://github.com/jdx/mise) via `~/.config/mise/config.toml`) 8. **Shell** — bash (default) or zsh + Oh My Zsh + autosuggestions + syntax highlighting (experimental) Selections are saved to `/workspace/.squarebox/` and reused on subsequent rebuilds. @@ -83,7 +83,7 @@ sqrbx-update `scripts/update-versions.sh` only touches the Dockerfile tier (delta, yq, xh, glow, gum, starship, just, difftastic). It fetches latest GitHub releases, downloads artifacts for both architectures, computes SHA256 checksums, and updates `checksums.txt` and the Dockerfile ARGs. -Optional tools installed by `setup.sh` (opencode, editors, TUIs, zellij, Go, nvm) are not pinned. They install the latest upstream release at setup time, so there is no checksum file or version variable to update in the repo. +Optional tools installed by `setup.sh` (opencode, editors, TUIs, zellij) are not pinned. They install the latest upstream release at setup time, so there is no checksum file or version variable to update in the repo. SDKs are installed by mise (a Dockerfile-tier pinned binary), so SDK trust is anchored to mise's own pinning rather than per-language installers. ## CI diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 06b2f1e..9ee1040 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -75,11 +75,15 @@ Dockerfile-tier tools (delta, yq, xh, glow, gum, starship, just, difftastic) are 2. Review the diff. Verify version bumps and checksums look correct. 3. Rebuild and test. -Optional tools (editors, TUIs, opencode, zellij, Go, nvm) track upstream -latest at install time. To add a new optional tool, add an entry to +Optional tools (editors, TUIs, opencode, zellij) track upstream latest at +install time. To add a new optional tool, add an entry to `scripts/lib/tools.yaml` and call `sb_install latest` from `setup.sh`. No checksum file update is required. +SDKs (Node, Python, Go, .NET, Rust) are installed by mise itself, which is +part of the Dockerfile-pinned tier — so SDK availability is gated by mise's +own pinning, not the SDK installers individually. + Never skip checksum verification for the Dockerfile tier. ### Style diff --git a/README.md b/README.md index aa01531..4b4cc8c 100644 --- a/README.md +++ b/README.md @@ -386,8 +386,9 @@ Uninstall sqrbx-uninstall Removes the container, image, and shell integration but **keeps** -`~/squarebox` (including `workspace/`) so your code is safe by default. -Pass `--purge` to also remove `~/squarebox`: +`~/squarebox` (including `workspace/`) and the `squarebox-home` named volume +(shell history, gh auth, mise toolchains) so your code and per-user state are +safe by default. Pass `--purge` to also remove both: sqrbx-uninstall --purge diff --git a/SECURITY.md b/SECURITY.md index 59e8c48..a02ca20 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -31,7 +31,7 @@ your home directory (the install script runs entirely as your user). If you prefer to inspect the script before running it: ```bash -curl -fsSL https://raw.githubusercontent.com/SquareWaveSystems/squarebox/main/install.sh -o install.sh +curl -fsSL https://github.com/SquareWaveSystems/squarebox/releases/latest/download/install.sh -o install.sh less install.sh # read it bash install.sh # run it ``` @@ -44,13 +44,15 @@ at each layer: | Layer | What's fetched | Transport | Integrity check | Version pinned? | |-------|---------------|-----------|-----------------|-----------------| -| **install.sh** | Git repo from GitHub | HTTPS | Git transport verification | Tracks `main` branch | +| **install.sh** | Git repo from GitHub | HTTPS | Git transport verification | Tracks the latest tag (or `--edge` for `main`) | +| **bootstrap script** | `install.sh` itself, served as a release asset | HTTPS | Pinned to a tagged release; pushes to `main` don't affect new installs | Yes (tagged release) | | **Dockerfile APT packages** | Ubuntu 24.04 packages, GitHub CLI, Eza | HTTPS | APT GPG signatures | Distro versions (not pinned) | -| **Dockerfile binary tools** | 8 tools from GitHub Releases (delta, yq, xh, glow, gum, starship, just, difftastic) | HTTPS | SHA256 checksum, build fails on mismatch | Yes, all pinned | -| **setup.sh optional tools** | OpenCode, nvm, Go, editors (micro, edit, fresh, nvim), TUIs (lazygit, gh-dash, yazi), zellij | HTTPS | None beyond transport | No, latest upstream at install time | -| **sqrbx-update (Dockerfile tier)** | delta, yq, xh, glow, gum, starship, just, difftastic | HTTPS | SHA256 checksum fetched from repo, update refused on mismatch or missing checksum | Only vetted versions | +| **Dockerfile binary tools** | 9 tools from GitHub Releases (delta, yq, xh, glow, gum, starship, just, difftastic, mise) | HTTPS | SHA256 checksum, build fails on mismatch | Yes, all pinned | +| **setup.sh optional tools** | OpenCode, editors (micro, edit, fresh, nvim), TUIs (lazygit, gh-dash, yazi), zellij | HTTPS | None beyond transport | No, latest upstream at install time | +| **sqrbx-update (Dockerfile tier)** | delta, yq, xh, glow, gum, starship, just, difftastic, mise | HTTPS | SHA256 checksum fetched from repo, update refused on mismatch or missing checksum | Only vetted versions | | **sqrbx-update (optional tier)** | Optional tools listed above | HTTPS | None beyond transport | Latest upstream | -| **setup.sh third-party installers** | Claude Code, uv, .NET, rustup | HTTPS | Delegates to vendor installer | No (latest/LTS) | +| **setup.sh SDKs (Node, Python, Go, .NET, Rust)** | Installed by mise from upstream | HTTPS | mise's own integrity checks (signatures / checksums per language) | mise itself is pinned; SDKs install latest | +| **setup.sh third-party installers** | Claude Code (npm) | HTTPS | npm's built-in package integrity verification | No (latest) | **What this means in practice:** @@ -62,9 +64,13 @@ at each layer: release from GitHub over HTTPS. Trust model is the same as running each tool's installer yourself. You get new features without waiting for a squarebox release, at the cost of the build-time pinning guarantee. -- Third-party install scripts (Claude Code from Anthropic, uv from Astral, .NET - from Microsoft, rustup from the Rust project) delegate to the vendor installer - and inherit whatever verification that installer performs. +- SDKs (Node, Python, Go, .NET, Rust) are installed by [mise](https://github.com/jdx/mise), + which is itself a Dockerfile-tier pinned binary. mise downloads each SDK + toolchain from its upstream over HTTPS and applies its own integrity checks + (e.g. signature verification for Node, checksums for Python). The trust + anchor for SDKs is therefore mise itself, not each language's installer. +- npm-based AI tools (Claude Code) inherit npm's built-in package integrity + verification. - APT packages are verified by Ubuntu's and each repo's GPG signatures. ## Container isolation diff --git a/scripts/e2e-test.sh b/scripts/e2e-test.sh index 36acd42..a95a71a 100755 --- a/scripts/e2e-test.sh +++ b/scripts/e2e-test.sh @@ -119,7 +119,7 @@ suite_shell() { run_test_grep "4.6a ai-aliases sourced" "squarebox-ai-aliases" cat ~/.bashrc run_test_grep "4.6b editor-aliases sourced" "squarebox-editor-aliases" cat ~/.bashrc run_test_grep "4.6c tui-aliases sourced" "squarebox-tui-aliases" cat ~/.bashrc - run_test_grep "4.6d sdk-paths sourced" "squarebox-sdk-paths" cat ~/.bashrc + run_test_grep "4.6d mise activated" 'mise activate bash' cat ~/.bashrc # Shell config loads without errors (also in build.yml) run_test "4.0 shell config loads" bash -lc 'echo ok' diff --git a/uninstall.ps1 b/uninstall.ps1 index 19a1338..863b2eb 100644 --- a/uninstall.ps1 +++ b/uninstall.ps1 @@ -31,6 +31,7 @@ $ErrorActionPreference = 'Stop' $ImageName = 'squarebox' $ContainerName = 'squarebox' $InstallDir = Join-Path $env:USERPROFILE 'squarebox' +$HomeVolume = if ($env:SQUAREBOX_HOME_VOLUME) { $env:SQUAREBOX_HOME_VOLUME } else { 'squarebox-home' } function Abort([string]$msg) { Write-Host "Error: $msg" -ForegroundColor Red @@ -114,6 +115,13 @@ if (Test-Path $PROFILE) { $hasInstallDir = Test-Path $InstallDir +# Detect the named volume (only meaningful if a runtime is detected). +$hasHomeVolume = $false +if ($SelectedRuntime) { + & $SelectedRuntime volume inspect $HomeVolume 2>$null | Out-Null + if ($LASTEXITCODE -eq 0) { $hasHomeVolume = $true } +} + # Summary. Write-Host "squarebox uninstall" Write-Host "===================" @@ -143,14 +151,19 @@ if ($Purge -and $hasInstallDir) { Write-Host " - Install dir: $InstallDir" $anythingToDo = $true } +if ($Purge -and $hasHomeVolume) { + Write-Host " - Named volume: $HomeVolume ($SelectedRuntime)" + $anythingToDo = $true +} if (-not $anythingToDo) { Write-Host " (nothing)" } -if (-not $Purge -and $hasInstallDir) { +if (-not $Purge -and ($hasInstallDir -or $hasHomeVolume)) { Write-Host "" Write-Host "Will KEEP:" - Write-Host " - $InstallDir (re-run with -Purge to remove, including workspace)" + if ($hasInstallDir) { Write-Host " - $InstallDir (re-run with -Purge to remove, including workspace)" } + if ($hasHomeVolume) { Write-Host " - $HomeVolume named volume (shell history, gh auth, mise toolchains)" } } Write-Host "" @@ -199,6 +212,7 @@ $removedContainer = $false $removedImage = $false $removedProfileBlock = $false $removedInstallDir = $false +$removedHomeVolume = $false if ($hasContainer) { Write-Host "Stopping container..." @@ -237,17 +251,38 @@ if ($Purge -and $hasInstallDir) { $removedInstallDir = $true } +# Named volume removal is gated on -Purge: the volume holds shell history, gh +# auth, and mise toolchains. The container has to be gone first (volume rm +# refuses if the volume is in use) - the rm above ensures that. +if ($Purge -and $hasHomeVolume) { + Write-Host "Removing named volume..." + & $SelectedRuntime volume rm $HomeVolume 2>$null | Out-Null + if ($LASTEXITCODE -eq 0) { + $removedHomeVolume = $true + } else { + Write-Host "Warning: failed to remove $HomeVolume (may still be in use)." -ForegroundColor Yellow + } +} + Write-Host "" Write-Host "Done." if ($removedContainer) { Write-Host " Removed container $ContainerName from $SelectedRuntime." } if ($removedImage) { Write-Host " Removed image $ImageName from $SelectedRuntime." } if ($removedProfileBlock) { Write-Host " Scrubbed squarebox block from $PROFILE." } if ($removedInstallDir) { Write-Host " Removed $InstallDir." } +if ($removedHomeVolume) { Write-Host " Removed named volume $HomeVolume." } -if (-not $Purge -and $hasInstallDir) { - Write-Host "" - Write-Host "Kept $InstallDir (including workspace). Remove manually with:" - Write-Host " Remove-Item -Recurse -Force $InstallDir" +if (-not $Purge) { + if ($hasInstallDir) { + Write-Host "" + Write-Host "Kept $InstallDir (including workspace). Remove manually with:" + Write-Host " Remove-Item -Recurse -Force $InstallDir" + } + if ($hasHomeVolume) { + Write-Host "" + Write-Host "Kept $HomeVolume named volume. Remove manually with:" + Write-Host " $SelectedRuntime volume rm $HomeVolume" + } } Write-Host "" diff --git a/uninstall.sh b/uninstall.sh index 8516586..46e0555 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -3,6 +3,7 @@ set -euo pipefail IMAGE_NAME="squarebox" CONTAINER_NAME="squarebox" +HOME_VOLUME="${SQUAREBOX_HOME_VOLUME:-squarebox-home}" # On MSYS2/Git Bash, HOME points to the MSYS home (/home/user) while the # install dir was placed under $USERPROFILE (C:/Users/user/squarebox). Mirror @@ -30,7 +31,9 @@ Remove the squarebox container, image, and shell integration. Options: --purge Also remove the install directory (~/squarebox), - including workspace/ and any host-side config under it. + including workspace/ and any host-side config under it, + plus the squarebox-home named volume (shell history, + gh auth, mise toolchains). -y, --yes Skip all confirmation prompts (for scripting). --runtime RUNTIME Use docker or podman explicitly (default: auto-detect by looking for the squarebox container/image). @@ -39,8 +42,10 @@ Options: Environment: SQUAREBOX_RUNTIME Same as --runtime (the flag takes priority). -By default, ~/squarebox is preserved so your workspace is not lost. Use --purge -to remove it; a second confirmation is required if workspace/ is non-empty. +By default, ~/squarebox and the squarebox-home named volume are preserved so +your workspace and per-user state (shell history, gh auth, mise toolchains) +survive a reinstall. Use --purge to remove both; a second confirmation is +required if workspace/ is non-empty. EOF } @@ -175,6 +180,14 @@ fi has_install_dir=0 [ -d "$INSTALL_DIR" ] && has_install_dir=1 +# Detect the named volume (only meaningful if a runtime is detected). +has_home_volume=0 +if [ -n "$RUNTIME" ]; then + if rt_cmd volume inspect "$HOME_VOLUME" >/dev/null 2>&1; then + has_home_volume=1 + fi +fi + # Summary. echo "squarebox uninstall" echo "===================" @@ -221,14 +234,19 @@ if [ "$PURGE" = 1 ] && [ "$has_install_dir" = 1 ]; then echo " - Install dir: $INSTALL_DIR" anything_to_do=1 fi +if [ "$PURGE" = 1 ] && [ "$has_home_volume" = 1 ]; then + echo " - Named volume: $HOME_VOLUME ($RUNTIME)" + anything_to_do=1 +fi if [ "$anything_to_do" = 0 ]; then echo " (nothing)" fi -if [ "$PURGE" = 0 ] && [ "$has_install_dir" = 1 ]; then +if [ "$PURGE" = 0 ] && { [ "$has_install_dir" = 1 ] || [ "$has_home_volume" = 1 ]; }; then echo "" echo "Will KEEP:" - echo " - $INSTALL_DIR (re-run with --purge to remove, including workspace)" + [ "$has_install_dir" = 1 ] && echo " - $INSTALL_DIR (re-run with --purge to remove, including workspace)" + [ "$has_home_volume" = 1 ] && echo " - $HOME_VOLUME named volume (shell history, gh auth, mise toolchains)" fi echo "" @@ -282,6 +300,7 @@ removed_shell_init=0 removed_rc_entries=() removed_bash_profile_source=0 removed_install_dir=0 +removed_home_volume=0 if [ "$has_container" = 1 ]; then echo "Stopping container..." @@ -369,6 +388,19 @@ if [ "$PURGE" = 1 ] && [ "$has_install_dir" = 1 ]; then removed_install_dir=1 fi +# Named volume removal is gated on --purge: it holds shell history, gh auth, +# and mise toolchains. The container has to be gone first (volumes can't be +# removed while in use) — `docker volume rm` returns non-zero if so, which is +# the safety we want. +if [ "$PURGE" = 1 ] && [ "$has_home_volume" = 1 ]; then + echo "Removing named volume..." + if rt_cmd volume rm "$HOME_VOLUME" >/dev/null 2>&1; then + removed_home_volume=1 + else + echo "Warning: failed to remove $HOME_VOLUME (may still be in use)." >&2 + fi +fi + echo "" echo "Done." [ "$removed_container" = 1 ] && echo " Removed container $CONTAINER_NAME from $RUNTIME." @@ -379,11 +411,19 @@ for f in "${removed_rc_entries[@]}"; do done [ "$removed_bash_profile_source" = 1 ] && echo " Scrubbed .bashrc source snippet from $_bash_profile." [ "$removed_install_dir" = 1 ] && echo " Removed $INSTALL_DIR." +[ "$removed_home_volume" = 1 ] && echo " Removed named volume $HOME_VOLUME." -if [ "$PURGE" = 0 ] && [ "$has_install_dir" = 1 ]; then - echo "" - echo "Kept $INSTALL_DIR (including workspace). Remove manually with:" - echo " rm -rf $INSTALL_DIR" +if [ "$PURGE" = 0 ]; then + if [ "$has_install_dir" = 1 ]; then + echo "" + echo "Kept $INSTALL_DIR (including workspace). Remove manually with:" + echo " rm -rf $INSTALL_DIR" + fi + if [ "$has_home_volume" = 1 ]; then + echo "" + echo "Kept $HOME_VOLUME named volume. Remove manually with:" + echo " $RUNTIME volume rm $HOME_VOLUME" + fi fi echo ""