From 9ee4cba6bc69f84eb0b0ca467cf16e7f1fee55e8 Mon Sep 17 00:00:00 2001 From: arbiter-hd <288758862+arbiter-hd@users.noreply.github.com> Date: Sun, 30 Aug 2026 02:14:30 -0400 Subject: [PATCH 1/5] feat: add staged Mac Studio profile --- AGENTS.md | 6 + README.md | 10 ++ chezmoi/README.md | 6 +- chezmoi/apply.sh | 6 +- chezmoi/bootstrap.sh | 6 +- chezmoi/lib.sh | 6 +- chezmoi/preview.sh | 10 ++ chezmoi/profiles/mac-studio.ancestors | 3 + chezmoi/profiles/mac-studio.paths | 14 +++ chezmoi/profiles/mac-studio.toml | 2 + chezmoi/source/.chezmoiignore.tmpl | 8 ++ chezmoi/source/symlink_dot_zshrc.tmpl | 2 + chezmoi/tests/production-test.sh | 23 +++- hosts/mac-studio/.zshrc | 26 +++++ hosts/mac-studio/Brewfile | 25 ++++ hosts/mac-studio/README.md | 104 +++++++++++++++++ hosts/mac-studio/ssh/ubuntu-vagrant.conf | 22 ++++ hosts/mac-studio/tests/vm-lifecycle-test.sh | 76 ++++++++++++ hosts/mac-studio/vm.zsh | 58 ++++++++++ hosts/shared/macos/README.md | 8 +- hosts/shared/macos/bootstrap.sh | 49 +++++++- hosts/shared/macos/doctor.sh | 98 +++++++++++++++- hosts/shared/macos/lib.sh | 5 +- hosts/shared/macos/tests/bootstrap-test.sh | 122 ++++++++++++++++++++ hosts/tests/run.sh | 1 + hosts/ubuntu-dev/GUIDE.md | 4 +- hosts/ubuntu-dev/README.md | 8 +- 27 files changed, 688 insertions(+), 20 deletions(-) create mode 100644 chezmoi/profiles/mac-studio.ancestors create mode 100644 chezmoi/profiles/mac-studio.paths create mode 100644 chezmoi/profiles/mac-studio.toml create mode 100644 hosts/mac-studio/.zshrc create mode 100644 hosts/mac-studio/Brewfile create mode 100644 hosts/mac-studio/README.md create mode 100644 hosts/mac-studio/ssh/ubuntu-vagrant.conf create mode 100644 hosts/mac-studio/tests/vm-lifecycle-test.sh create mode 100644 hosts/mac-studio/vm.zsh diff --git a/AGENTS.md b/AGENTS.md index aa7c96d1..23639348 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,6 +19,7 @@ constraints, and agent workflow policy here. | Target | Source of truth | | ------------------------------------------ | ------------------------------------ | | Thin personal MacBook (`mac-thin`) | `hosts/mac-thin/README.md` | +| Staged primary Mac Studio (`mac-studio`) | `hosts/mac-studio/README.md` | | Standalone development MacBook (`mac-pro`) | `hosts/mac-pro/README.md` | | Personal Mac mini (`mac-mini`) | `hosts/mac-mini/README.md` | | Resilience work Mac (`mac-work`) | `hosts/mac-work/README.md` | @@ -61,6 +62,11 @@ host-specific provisioning, lifecycle, maintenance, doctors, and runbooks. If Hamel explicitly asks for one link, create it safely. Do not expand that request into a full-machine migration. +The `mac-studio` profile is pre-arrival staging. Do not apply it, move or +destroy the Ubuntu VM, or change live host routing until the Studio arrives +and Hamel approves the cutover gates in its runbook. Studio apply requires +`DOTFILES_MAC_STUDIO_ARRIVED=1`; never set it on another host. + ## Package Ownership - Mac package policy lives only in `hosts/shared/macos/Brewfile` and the diff --git a/README.md b/README.md index 6342ea84..a1d10d97 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Hamel's profile-aware dotfiles, provisioning, and machine operations. | --- | --- | --- | | Thin Mac | Control plane for Codex, SSH, Vagrant, and VMware Fusion | [`hosts/mac-thin/README.md`](hosts/mac-thin/README.md) | | Ubuntu dev | Primary VM development workstation | [`hosts/ubuntu-dev/README.md`](hosts/ubuntu-dev/README.md) | +| Mac Studio | Staged primary controller, VM host, and local-AI machine | [`hosts/mac-studio/README.md`](hosts/mac-studio/README.md) | | Mac Pro | Standalone full-development MacBook; local Brew stack, no VM | [`hosts/mac-pro/README.md`](hosts/mac-pro/README.md) | | Mac mini | Production runtime host | [`hosts/mac-mini/README.md`](hosts/mac-mini/README.md) | | Work Mac | Company-scoped terminal and editor setup | [`hosts/mac-work/README.md`](hosts/mac-work/README.md) | @@ -33,6 +34,7 @@ dotfiles-hd/ │ ├── tests/ one host-validation runner │ ├── shared/macos/ shared full-Mac provisioning and doctor │ ├── mac-thin/ control plane and VM lifecycle +│ ├── mac-studio/ staged primary controller, VM host, and local AI │ ├── ubuntu-dev/ Vagrant guest provisioning and maintenance │ ├── mac-pro/ standalone development MacBook policy │ ├── mac-mini/ production runtime Mac policy @@ -59,6 +61,13 @@ hosts/mac-thin/bootstrap.sh --apply uvm-up ``` +Staged Mac Studio profile (do not apply before arrival): + +```bash +hosts/shared/macos/bootstrap.sh --profile mac-studio --dry-run +hosts/shared/macos/bootstrap.sh --profile mac-studio --check +``` + Standalone full-development MacBook: ```bash @@ -86,6 +95,7 @@ gates pass. - `chezmoi/` owns approved user configuration links and timestamped rollback. - `hosts/mac-thin/` owns the Vagrant and VMware lifecycle. +- `hosts/mac-studio/` becomes the VM lifecycle owner only after approved cutover. - `hosts/ubuntu-dev/` owns guest provisioning and workstation maintenance. - `hosts/shared/macos/` owns common full-Mac packages and operational setup. - `hosts/mac-pro/` and `hosts/mac-mini/` own profile package overlays and shell entry points. diff --git a/chezmoi/README.md b/chezmoi/README.md index 662d985d..27b18029 100644 --- a/chezmoi/README.md +++ b/chezmoi/README.md @@ -14,6 +14,7 @@ services, secrets, identity, enrollment, macOS preferences, or mutable state. | `ubuntu` | Ubuntu development VM | Installs the declared mise toolchain | | `mac-thin` | Thin Mac control plane | Installs the thin Brewfile | | `mac-pro` | Standalone full-development MacBook | Installs shared and profile Brewfiles through the Mac bootstrap | +| `mac-studio` | Staged primary Mac, VM host, and local AI | Installs shared and profile Brewfiles through the Mac bootstrap | | `mac-mini` | Production Mac mini | Configuration-only inside Chezmoi; packages stay in the guarded Mac bootstrap | | `mac-work` | Resilience work Mac | Reserved for a later opt-in rollout | @@ -27,7 +28,8 @@ original type must be restored exactly. ./chezmoi/bootstrap.sh ubuntu --preview ``` -Substitute `mac-thin`, `mac-pro`, `mac-mini`, or `mac-work` as needed. +Substitute `mac-thin`, `mac-pro`, `mac-studio`, `mac-mini`, or `mac-work` as +needed. After review and host approval: @@ -79,5 +81,5 @@ CHEZMOI_BIN="$HOME/.local/bin/chezmoi" \ bash chezmoi/tests/production-test.sh ``` -The test renders and exercises all five profiles in temporary homes. It covers +The test renders and exercises all six profiles in temporary homes. It covers backup, apply, second-apply idempotence, doctor, rollback, and recovery/reapply. diff --git a/chezmoi/apply.sh b/chezmoi/apply.sh index 2e474520..896d3266 100755 --- a/chezmoi/apply.sh +++ b/chezmoi/apply.sh @@ -9,6 +9,8 @@ validate_profile_os require_canonical_checkout [[ "${DOTFILES_CHEZMOI_APPROVED:-0}" == 1 ]] \ || die "set DOTFILES_CHEZMOI_APPROVED=1 after approving this host and preview" +[[ "$PROFILE" != mac-studio || "${DOTFILES_MAC_STUDIO_ARRIVED:-0}" == 1 ]] \ + || die "mac-studio apply requires DOTFILES_MAC_STUDIO_ARRIVED=1 after the hardware arrives" [[ "$PROFILE" != mac-mini || "${DOTFILES_MAC_MINI_CONFIG_ONLY:-0}" == 1 ]] \ || die "Mac mini requires DOTFILES_MAC_MINI_CONFIG_ONLY=1" [[ "$PROFILE" != mac-work || "${DOTFILES_WORK_MAC_OPT_IN:-0}" == 1 ]] \ @@ -34,9 +36,9 @@ if [[ -n "$PROFILE_ANCESTORS" ]]; then prepared="$backup_dir/prepared-ancestors/$relative" mkdir -p "$(dirname "$prepared")" mv "$target" "$prepared" - mkdir -m "$managed_mode" "$target" + mkdir -p -m "$managed_mode" "$target" elif [[ ! -e "$target" ]]; then - mkdir -m "$managed_mode" "$target" + mkdir -p -m "$managed_mode" "$target" fi done < "$PROFILE_ANCESTORS" while IFS='|' read -r relative _source; do diff --git a/chezmoi/bootstrap.sh b/chezmoi/bootstrap.sh index e4a58017..a5be51b8 100755 --- a/chezmoi/bootstrap.sh +++ b/chezmoi/bootstrap.sh @@ -4,11 +4,11 @@ profile="${1:-}"; mode="${2:---preview}"; version="v2.72.0" script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)" bin="${CHEZMOI_BIN:-$HOME/.local/bin/chezmoi}" case "$profile:$mode" in - ubuntu:--preview|ubuntu:--apply|mac-thin:--preview|mac-thin:--apply|mac-pro:--preview|mac-pro:--apply|mac-mini:--preview|mac-mini:--apply|mac-work:--preview|mac-work:--apply) ;; - *) echo "usage: $0 ubuntu|mac-thin|mac-pro|mac-mini|mac-work --preview|--apply" >&2; exit 2 ;; + ubuntu:--preview|ubuntu:--apply|mac-thin:--preview|mac-thin:--apply|mac-pro:--preview|mac-pro:--apply|mac-studio:--preview|mac-studio:--apply|mac-mini:--preview|mac-mini:--apply|mac-work:--preview|mac-work:--apply) ;; + *) echo "usage: $0 ubuntu|mac-thin|mac-pro|mac-studio|mac-mini|mac-work --preview|--apply" >&2; exit 2 ;; esac case "$profile:$(uname -s)" in - ubuntu:Linux|mac-thin:Darwin|mac-pro:Darwin|mac-mini:Darwin|mac-work:Darwin) ;; + ubuntu:Linux|mac-thin:Darwin|mac-pro:Darwin|mac-studio:Darwin|mac-mini:Darwin|mac-work:Darwin) ;; *) [ "${DOTFILES_CHEZMOI_TEST:-0}" = 1 ] || { echo "profile/OS mismatch" >&2; exit 1; } ;; esac if [ "$mode" = --apply ] || [ "${DOTFILES_CHEZMOI_REQUIRE_REVIEWED:-0}" = 1 ]; then diff --git a/chezmoi/lib.sh b/chezmoi/lib.sh index 37e2f77f..41717147 100755 --- a/chezmoi/lib.sh +++ b/chezmoi/lib.sh @@ -21,8 +21,8 @@ die() { load_profile() { PROFILE="${1:-}" case "$PROFILE" in - ubuntu|mac-thin|mac-pro|mac-mini|mac-work) ;; - *) die "profile must be ubuntu, mac-thin, mac-pro, mac-mini, or mac-work" ;; + ubuntu|mac-thin|mac-pro|mac-studio|mac-mini|mac-work) ;; + *) die "profile must be ubuntu, mac-thin, mac-pro, mac-studio, mac-mini, or mac-work" ;; esac PROFILE_CONFIG="$PROFILES_DIR/$PROFILE.toml" PROFILE_MANIFEST="$PROFILES_DIR/$PROFILE.paths" @@ -46,7 +46,7 @@ activate_profile() { validate_profile_os() { [[ "${DOTFILES_CHEZMOI_TEST:-0}" == 1 ]] && return case "$PROFILE:$(uname -s)" in - ubuntu:Linux|mac-thin:Darwin|mac-pro:Darwin|mac-mini:Darwin|mac-work:Darwin) ;; + ubuntu:Linux|mac-thin:Darwin|mac-pro:Darwin|mac-studio:Darwin|mac-mini:Darwin|mac-work:Darwin) ;; *) die "$PROFILE does not match $(uname -s)" ;; esac } diff --git a/chezmoi/preview.sh b/chezmoi/preview.sh index a2eb7708..d499e2bd 100755 --- a/chezmoi/preview.sh +++ b/chezmoi/preview.sh @@ -96,6 +96,16 @@ if [[ "${DOTFILES_CHEZMOI_CONFIG_ONLY_PREVIEW:-0}" != 1 ]]; then printf ' Homebrew is required before the approved apply\n' fi ;; + mac-studio) + if command -v brew >/dev/null 2>&1; then + HOMEBREW_NO_AUTO_UPDATE=1 brew bundle check --verbose \ + --file "$REPO_DIR/hosts/shared/macos/Brewfile" || true + HOMEBREW_NO_AUTO_UPDATE=1 brew bundle check --verbose \ + --file "$REPO_DIR/hosts/mac-studio/Brewfile" || true + else + printf ' Homebrew is required before the approved apply\n' + fi + ;; mac-work) if command -v brew >/dev/null 2>&1; then HOMEBREW_NO_AUTO_UPDATE=1 brew bundle check --verbose \ diff --git a/chezmoi/profiles/mac-studio.ancestors b/chezmoi/profiles/mac-studio.ancestors new file mode 100644 index 00000000..54f08a05 --- /dev/null +++ b/chezmoi/profiles/mac-studio.ancestors @@ -0,0 +1,3 @@ +.config/btop|config/btop|700 +.config/fastfetch|config/fastfetch|700 +.config/mise|config/mise|700 diff --git a/chezmoi/profiles/mac-studio.paths b/chezmoi/profiles/mac-studio.paths new file mode 100644 index 00000000..7beafe4e --- /dev/null +++ b/chezmoi/profiles/mac-studio.paths @@ -0,0 +1,14 @@ +.zshrc|hosts/mac-studio/.zshrc +.config/zsh|config/zsh +.config/bookokrat|config/bookokrat +.config/btop/btop.conf|config/btop/btop.conf +.config/btop/themes|config/btop/themes +.config/fastfetch/config.jsonc|config/fastfetch/config.jsonc +.config/fastfetch/logo-anon-glitch.txt|config/fastfetch/logo-anon-glitch.txt +.config/fastfetch/logo-anon.txt|config/fastfetch/logo-anon.txt +Library/Application Support/com.mitchellh.ghostty/config|config/ghostty/config +.config/herdr/config.toml|config/herdr/config.toml +.config/hunk/config.toml|config/hunk/config.toml +.config/mise/config.toml|config/mise/config.toml +.config/nvim|config/nvim +.config/starship.toml|config/starship/starship.toml diff --git a/chezmoi/profiles/mac-studio.toml b/chezmoi/profiles/mac-studio.toml new file mode 100644 index 00000000..2f1794cf --- /dev/null +++ b/chezmoi/profiles/mac-studio.toml @@ -0,0 +1,2 @@ +[data] +profile = "mac-studio" diff --git a/chezmoi/source/.chezmoiignore.tmpl b/chezmoi/source/.chezmoiignore.tmpl index 077e317f..56b26e33 100644 --- a/chezmoi/source/.chezmoiignore.tmpl +++ b/chezmoi/source/.chezmoiignore.tmpl @@ -54,6 +54,14 @@ Library 30-install-thin-tools.sh 40-install-work-tools.sh {{- end }} +{{- if eq .profile "mac-studio" }} +.config/homebrew +.hermes +.terminfo +20-install-ubuntu-tools.sh +30-install-thin-tools.sh +40-install-work-tools.sh +{{- end }} {{- if eq .profile "mac-work" }} .hermes .terminfo diff --git a/chezmoi/source/symlink_dot_zshrc.tmpl b/chezmoi/source/symlink_dot_zshrc.tmpl index 631a0fa9..fb23971b 100644 --- a/chezmoi/source/symlink_dot_zshrc.tmpl +++ b/chezmoi/source/symlink_dot_zshrc.tmpl @@ -5,6 +5,8 @@ {{ joinPath $repo "hosts" "mac-thin" ".zshrc" }} {{- else if eq .profile "mac-pro" -}} {{ joinPath $repo "hosts" "mac-pro" ".zshrc" }} +{{- else if eq .profile "mac-studio" -}} +{{ joinPath $repo "hosts" "mac-studio" ".zshrc" }} {{- else if eq .profile "mac-mini" -}} {{ joinPath $repo "hosts" "mac-mini" ".zshrc" }} {{- end -}} diff --git a/chezmoi/tests/production-test.sh b/chezmoi/tests/production-test.sh index a8ffef08..641486f8 100755 --- a/chezmoi/tests/production-test.sh +++ b/chezmoi/tests/production-test.sh @@ -178,13 +178,13 @@ set -e ((layout_status != 0)) [[ "$layout_output" == *"unapproved mac-thin symlink parent"* ]] -for profile in ubuntu mac-thin mac-pro mac-mini mac-work; do +for profile in ubuntu mac-thin mac-pro mac-studio mac-mini mac-work; do home_dir="$case_dir/$profile/home" state_dir="$case_dir/$profile/state" mkdir -p "$home_dir" "$state_dir" case "$profile" in ubuntu) mkdir -p "$home_dir/.config/btop" "$home_dir/.config/fastfetch" ;; - mac-pro|mac-mini) prepare_mac_mini_home "$home_dir" ;; + mac-pro|mac-studio|mac-mini) prepare_mac_mini_home "$home_dir" ;; esac prepare_profile_parents "$profile" "$home_dir" if [[ "$profile" == mac-thin ]]; then @@ -208,6 +208,7 @@ for profile in ubuntu mac-thin mac-pro mac-mini mac-work; do ubuntu) printf '%s\n' 10-configure-git.sh 20-install-ubuntu-tools.sh ;; mac-thin) printf '%s\n' 10-configure-git.sh 30-install-thin-tools.sh ;; mac-pro) printf '%s\n' 10-configure-git.sh ;; + mac-studio) printf '%s\n' 10-configure-git.sh ;; mac-mini) printf '%s\n' 10-configure-git.sh ;; mac-work) printf '%s\n' 10-configure-git.sh 40-install-work-tools.sh ;; esac > "$case_dir/$profile.expected-scripts" @@ -254,6 +255,24 @@ for profile in ubuntu mac-thin mac-pro mac-mini mac-work; do bash "$CHEZMOI_DIR/doctor.sh" "$profile" >/dev/null done +studio_guard_home="$case_dir/mac-studio-guard/home" +studio_guard_state="$case_dir/mac-studio-guard/state" +mkdir -p "$studio_guard_home" "$studio_guard_state" +set +e +studio_guard_output="$({ + DOTFILES_CHEZMOI_TEST=1 \ + DOTFILES_CHEZMOI_APPROVED=1 \ + CHEZMOI_BIN="$CHEZMOI_BIN" \ + CHEZMOI_DESTINATION="$studio_guard_home" \ + CHEZMOI_STATE_DIR="$studio_guard_state" \ + bash "$CHEZMOI_DIR/apply.sh" mac-studio +} 2>&1)" +studio_guard_status=$? +set -e +((studio_guard_status != 0)) +[[ "$studio_guard_output" == \ + *"mac-studio apply requires DOTFILES_MAC_STUDIO_ARRIVED=1"* ]] + rollback_home="$case_dir/rollback/home" rollback_state="$case_dir/rollback/state" rollback_backups="$case_dir/rollback/backups" diff --git a/hosts/mac-studio/.zshrc b/hosts/mac-studio/.zshrc new file mode 100644 index 00000000..f5f31957 --- /dev/null +++ b/hosts/mac-studio/.zshrc @@ -0,0 +1,26 @@ +# Mac Studio primary control plane and compute host. + +export DOTFILES_MAC_PROFILE="mac-studio" + +source "$HOME/Developer/dotfiles-hd/config/zsh/mac/init.zsh" +source "$HOME/Developer/dotfiles-hd/config/zsh/mac/personal/init.zsh" + +_load_homebrew_plugin "/opt/homebrew/share/zsh-autosuggestions/zsh-autosuggestions.zsh" +_load_homebrew_plugin "/opt/homebrew/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh" + +unset NO_COLOR +unset CI +export FORCE_COLOR=1 + +_zsh_add_completion_dirs \ + "$HOME/.docker/completions" \ + /opt/homebrew/share/zsh/site-functions \ + /usr/local/share/zsh/site-functions +_zsh_init_completions 43200 +typeset -gaU path +path=("${XDG_BIN_HOME:-$HOME/.local/bin}" $path) +export PATH + +_activate_mise + +source "$HOME/Developer/dotfiles-hd/hosts/mac-studio/vm.zsh" diff --git a/hosts/mac-studio/Brewfile b/hosts/mac-studio/Brewfile new file mode 100644 index 00000000..a5e04ed1 --- /dev/null +++ b/hosts/mac-studio/Brewfile @@ -0,0 +1,25 @@ +# Primary personal-Mac applications. Shared development tools live in +# ../shared/macos/Brewfile. + +cask "1password" +cask "brave-browser" +cask "codex" +cask "daisydisk" +cask "deskflow" +cask "google-chrome" +cask "istat-menus" +cask "little-snitch" +cask "micro-snitch" +cask "mullvad-vpn" +cask "notunes" +cask "obsidian" +cask "ollama-app" +cask "pearcleaner" +cask "raycast" +cask "tableplus" +cask "tailscale-app" +cask "telegram" +cask "vagrant" +cask "vagrant-vmware-utility" +cask "vlc" +cask "zoom" diff --git a/hosts/mac-studio/README.md b/hosts/mac-studio/README.md new file mode 100644 index 00000000..ab1f87eb --- /dev/null +++ b/hosts/mac-studio/README.md @@ -0,0 +1,104 @@ +# Mac Studio + +The Mac Studio is the planned primary machine: full macOS development, local +AI, VM hosting, and control of the Ubuntu VM and Mac mini. + +This profile is staged before the hardware arrives. It does not change the +current thin-Mac, Ubuntu, or Mac-mini routes. Do not apply it until the Studio +is present and Hamel approves the reviewed preview and check. + +## Topology + +```text +Mac Studio (primary controller and compute) +├── Ubuntu VM (lives inside the Studio; primary development workstation) +├── Ollama (native macOS app; models and cache stay on the Studio) +└── SSH / Screen Sharing -> Mac mini (Hermes runtime) + +MacBook Air (mobile controller) +├── SSH / Screen Sharing -> Mac Studio +├── SSH -> Ubuntu VM +└── SSH -> Mac mini +``` + +The Studio and Air use Hamel's personal Apple Account. The Mac mini keeps its +existing dedicated Apple Account. Apple Account setup, Remote Login, Screen +Sharing, Tailscale enrollment, SSH keys, and Ollama models are machine-owned +and never copied by dotfiles. + +The future Air reuses the existing `mac-thin` profile. No new Air-specific +profile is needed. + +## On Arrival + +Manually install and finish first-run setup for Xcode Command Line Tools, +Homebrew, and VMware Fusion. Then clone this repository to +`~/Developer/dotfiles-hd` and run: + +```bash +hosts/shared/macos/bootstrap.sh --profile mac-studio --dry-run +hosts/shared/macos/bootstrap.sh --profile mac-studio --check +``` + +Review both results before approving: + +```bash +DOTFILES_MAC_STUDIO_ARRIVED=1 \ + hosts/shared/macos/bootstrap.sh --profile mac-studio --apply +hosts/shared/macos/doctor.sh --profile mac-studio +``` + +The arrival flag is a hard safety gate. Never set it on another Mac. + +The SSH config is machine-owned. After the Studio exists, manually add this +line before any older `Host ubuntu-vm*` blocks in `~/.ssh/config`: + +```sshconfig +Include ~/Developer/dotfiles-hd/hosts/mac-studio/ssh/ubuntu-vagrant.conf +``` + +The apply installs the Ollama app, Vagrant, the VMware utility, Rosetta 2 when +needed, and `vagrant-vmware-desktop` 3.0.5. It does not launch Ollama, download +models, start services, create the VM, or alter remote-access settings. + +## Ubuntu VM + +After the Studio profile is green, move by rebuilding the Ubuntu VM from the +tracked Vagrant definition. Do not manually move or delete the current Fusion +VM. The cutover is a separate approval gate. + +Cut over in this order: + +1. Back up and verify uncommitted repositories, databases, and other VM-local + data. Record the old VM's SSH host-key and Git-key fingerprints. + +1. Halt the old VM and keep it recoverable. Never run both `ubuntu-dev` + machines on the LAN or tailnet together. + +1. Rebuild on the Studio without a Tailscale auth key. Verify localhost SSH, + provisioning, repositories, tools, and restored data first. + +1. Retire the old Tailscale node, enroll the new VM as `ubuntu-dev`, then + replace known-host entries only after verifying its new fingerprint. + +1. Run the full Ubuntu doctor and the Studio cutover check: + + ```bash + DOTFILES_MAC_STUDIO_CUTOVER=1 \ + hosts/shared/macos/doctor.sh --profile mac-studio + ``` + +1. Destroy the old VM and remove its registered Git keys only after Hamel + approves the verified replacement. + +The Studio shell exposes `uvm-up`, `uvm-stop`, `uvm-suspend`, `uvm-resume`, +`uvm-status`, `uvm-ip`, and interactive `uvm-destroy`. The VM remains the +primary home for project repositories, Docker, databases, and PR work. + +## Ownership + +- Dotfiles own the reviewed profile, shell, packages, and VM commands. +- Vagrant owns the Ubuntu VM definition and lifecycle. +- VMware Fusion owns the local VM runtime. +- Ollama models and `~/.ollama` stay machine-owned. +- The Mac mini remains the Hermes production runtime. diff --git a/hosts/mac-studio/ssh/ubuntu-vagrant.conf b/hosts/mac-studio/ssh/ubuntu-vagrant.conf new file mode 100644 index 00000000..4f9d8ba7 --- /dev/null +++ b/hosts/mac-studio/ssh/ubuntu-vagrant.conf @@ -0,0 +1,22 @@ +# Include this file before older Ubuntu blocks so these routes take precedence. + +Host ubuntu-vm + HostName 127.0.0.1 + Port 2222 + User hamel + AddressFamily inet + IdentityFile ~/.ssh/id_ed25519_ubuntu_vm + IdentitiesOnly yes + ForwardAgent no + HostKeyAlias ubuntu-dev + StrictHostKeyChecking yes + +Host ubuntu-vm-ts + HostName ubuntu-dev + User hamel + AddressFamily inet + IdentityFile ~/.ssh/id_ed25519_ubuntu_vm + IdentitiesOnly yes + ForwardAgent no + HostKeyAlias ubuntu-dev + StrictHostKeyChecking yes diff --git a/hosts/mac-studio/tests/vm-lifecycle-test.sh b/hosts/mac-studio/tests/vm-lifecycle-test.sh new file mode 100644 index 00000000..8efeda04 --- /dev/null +++ b/hosts/mac-studio/tests/vm-lifecycle-test.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +set -euo pipefail + +TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +REPO_DIR="$(cd "$TEST_DIR/../../.." && pwd -P)" +TEST_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/dotfiles-studio-vm-test.XXXXXX")" +trap 'rm -rf "$TEST_ROOT"' EXIT + +FAKE_BIN="$TEST_ROOT/bin" +TEST_HOME="$TEST_ROOT/home" +VAGRANT_LOG="$TEST_ROOT/vagrant.log" +mkdir -p "$FAKE_BIN" "$TEST_HOME" + +fail() { + printf 'FAIL: %s\n' "$*" >&2 + exit 1 +} + +assert_contains() { + grep -Fq -- "$2" "$1" || fail "expected '$2' in $1" +} + +assert_not_contains() { + ! grep -Fq -- "$2" "$1" || fail "did not expect '$2' in $1" +} + +cat > "$FAKE_BIN/vagrant" <<'FAKE_VAGRANT' +#!/usr/bin/env bash +printf 'cwd=%s vagrant_cwd=%s gui=%s provider=%s clone=%s args=%s\n' \ + "$PWD" \ + "${VAGRANT_CWD:-unset}" \ + "${UBUNTU_VM_GUI:-unset}" \ + "${VAGRANT_DEFAULT_PROVIDER:-unset}" \ + "${VAGRANT_VMWARE_CLONE_DIRECTORY:-unset}" \ + "$*" >> "${VAGRANT_TEST_LOG:?}" +if [[ "${1:-}" == ssh ]]; then + printf '192.0.2.10\n' +fi +FAKE_VAGRANT +chmod +x "$FAKE_BIN/vagrant" + +output="$({ + HOME="$TEST_HOME" \ + PATH="$FAKE_BIN:/usr/bin:/bin" \ + DOTFILES_UBUNTU_VAGRANT_DIR="$REPO_DIR/hosts/ubuntu-dev" \ + VAGRANT_TEST_LOG="$VAGRANT_LOG" \ + /bin/zsh -dfc ' + source "$1" + uvm-up + uvm-stop + uvm-suspend + uvm-resume + uvm-status + uvm-ip + uvm-destroy + ' zsh "$REPO_DIR/hosts/mac-studio/vm.zsh" +} 2>&1)" || fail "$output" + +assert_contains "$VAGRANT_LOG" \ + "cwd=$REPO_DIR/hosts/ubuntu-dev vagrant_cwd=$REPO_DIR/hosts/ubuntu-dev gui=1 provider=vmware_desktop" +for args in up halt suspend resume status 'ssh -c hostname -I' destroy; do + assert_contains "$VAGRANT_LOG" "args=$args" +done +assert_contains "$VAGRANT_LOG" 'sudo -n -u hamel' +assert_not_contains "$VAGRANT_LOG" 'destroy -f' +[[ "$output" == *"Remove this VM's three registered Git public keys"* ]] \ + || fail 'destroy should print the Git-key removal reminder' + +assert_contains "$REPO_DIR/hosts/mac-studio/.zshrc" \ + 'source "$HOME/Developer/dotfiles-hd/hosts/mac-studio/vm.zsh"' +for expected in 'Host ubuntu-vm' 'HostName 127.0.0.1' 'ForwardAgent no' \ + 'StrictHostKeyChecking yes'; do + assert_contains "$REPO_DIR/hosts/mac-studio/ssh/ubuntu-vagrant.conf" "$expected" +done + +printf 'Mac Studio VM lifecycle tests passed.\n' diff --git a/hosts/mac-studio/vm.zsh b/hosts/mac-studio/vm.zsh new file mode 100644 index 00000000..f30b5dd4 --- /dev/null +++ b/hosts/mac-studio/vm.zsh @@ -0,0 +1,58 @@ +# Mac Studio Vagrant shortcuts. Development runs inside the Ubuntu guest. + +_ubuntu_vagrant() { + emulate -L zsh + + local project_dir="${DOTFILES_UBUNTU_VAGRANT_DIR:-$HOME/Developer/dotfiles-hd/hosts/ubuntu-dev}" + local clone_dir="${VAGRANT_VMWARE_CLONE_DIRECTORY:-$HOME/Virtual Machines.localized/VMWIsoImages}" + + if [[ ! -f "$project_dir/Vagrantfile" ]]; then + echo "Ubuntu Vagrant project missing: $project_dir" + return 1 + fi + + ( + cd "$project_dir" || return + export VAGRANT_CWD="$project_dir" + export VAGRANT_DEFAULT_PROVIDER=vmware_desktop + export VAGRANT_VMWARE_CLONE_DIRECTORY="$clone_dir" + command vagrant "$@" + ) +} + +uvm-up() { + UBUNTU_VM_GUI=1 _ubuntu_vagrant up +} + +uvm-stop() { + _ubuntu_vagrant halt +} + +uvm-suspend() { + _ubuntu_vagrant suspend +} + +uvm-resume() { + _ubuntu_vagrant resume +} + +uvm-status() { + _ubuntu_vagrant status +} + +uvm-ip() { + _ubuntu_vagrant ssh -c 'hostname -I' +} + +uvm-destroy() { + emulate -L zsh + + # Best effort only: the VM may already be stopped. Never start it to inspect keys. + _ubuntu_vagrant ssh -c \ + 'sudo -n -u hamel sh -c '\''for key in /home/hamel/.ssh/id_ed25519_hd719 /home/hamel/.ssh/id_ed25519_arbiter_hd /home/hamel/.ssh/id_ed25519_forgejo_truenas; do test -f "$key.pub" && ssh-keygen -lf "$key.pub"; done'\''' \ + 2>/dev/null || true + + echo "Remove this VM's three registered Git public keys after replacement." + echo "Vagrant will ask for confirmation; this command never forces destroy." + _ubuntu_vagrant destroy +} diff --git a/hosts/shared/macos/README.md b/hosts/shared/macos/README.md index 7f6560c8..b3354d1f 100644 --- a/hosts/shared/macos/README.md +++ b/hosts/shared/macos/README.md @@ -1,10 +1,11 @@ # Shared Personal Mac Bootstrap -Shared provisioning for two full local macOS profiles: +Shared provisioning for three full local macOS profiles: | Profile | Purpose | | --- | --- | | `mac-pro` | Standalone full-development MacBook; all development tools local, no VM | +| `mac-studio` | Staged primary controller with Ubuntu VM hosting and Ollama | | `mac-mini` | Production runtime Mac with extra apply gates | Do not run this bootstrap on the thin Mac. Use @@ -52,6 +53,11 @@ ancestor manifest. Secrets, auth state, Herdr sessions, Hunk state, tmux plugins, services, Docker state, databases, and application data remain machine-owned. +For `mac-studio`, bootstrap also installs Rosetta 2 when needed and pins the +Vagrant VMware provider. It installs the Ollama app but never launches it or +downloads models. VMware Fusion first-run setup and remote access remain +manual. + ## Rollback Use the exact Chezmoi rollback command printed by apply. The timestamped backup diff --git a/hosts/shared/macos/bootstrap.sh b/hosts/shared/macos/bootstrap.sh index 254b1262..3cf7fe37 100755 --- a/hosts/shared/macos/bootstrap.sh +++ b/hosts/shared/macos/bootstrap.sh @@ -8,6 +8,10 @@ STAMP="${DOTFILES_STAMP:-$(date +%Y%m%d-%H%M%S)}" CHEZMOI_BOOTSTRAP="${DOTFILES_CHEZMOI_BOOTSTRAP:-$DOTFILES_DIR/chezmoi/bootstrap.sh}" CHEZMOI_PREVIEW="${DOTFILES_CHEZMOI_PREVIEW:-$DOTFILES_DIR/chezmoi/preview.sh}" HOST_DOCTOR="${DOTFILES_MAC_DOCTOR:-$SCRIPT_DIR/doctor.sh}" +PKGUTIL="${DOTFILES_PKGUTIL:-/usr/sbin/pkgutil}" +SOFTWAREUPDATE="${DOTFILES_SOFTWAREUPDATE:-/usr/sbin/softwareupdate}" +SUDO="${DOTFILES_SUDO:-/usr/bin/sudo}" +VAGRANT_VMWARE_PLUGIN_VERSION="3.0.5" PROFILE="" MODE="dry-run" @@ -16,7 +20,7 @@ source "$SCRIPT_DIR/lib.sh" usage() { cat <<'EOF' -Usage: bootstrap.sh --profile mac-pro|mac-mini [--dry-run|--check|--apply] +Usage: bootstrap.sh --profile mac-pro|mac-studio|mac-mini [--dry-run|--check|--apply] Modes: --dry-run Show planned commands and filesystem changes without invoking @@ -67,6 +71,17 @@ done PROFILE="$(canonical_profile "$PROFILE")" || exit 2 load_profile "$PROFILE" "$DOTFILES_DIR" "$HOME" +rosetta_installed() { + "$PKGUTIL" --pkg-info com.apple.pkg.RosettaUpdateAuto >/dev/null 2>&1 +} + +vagrant_vmware_plugin_current() { + command -v vagrant >/dev/null 2>&1 || return 1 + vagrant plugin list 2>/dev/null \ + | /usr/bin/grep -Eq \ + "^vagrant-vmware-desktop \($VAGRANT_VMWARE_PLUGIN_VERSION([,)])" +} + [[ "$(uname -s)" == "Darwin" ]] || die "personal-Mac bootstrap requires macOS" [[ "$(uname -m)" == "arm64" ]] || die "personal-Mac bootstrap currently supports Apple Silicon only" xcode-select -p >/dev/null 2>&1 || die "install Xcode Command Line Tools first: xcode-select --install" @@ -75,6 +90,9 @@ git -C "$DOTFILES_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1 \ || die "not a Git checkout: $DOTFILES_DIR" if [[ "$MODE" == "apply" ]]; then + [[ "$PROFILE" != mac-studio \ + || "${DOTFILES_MAC_STUDIO_ARRIVED:-0}" == "1" ]] \ + || die "mac-studio apply requires DOTFILES_MAC_STUDIO_ARRIVED=1 after the hardware arrives" [[ "$DOTFILES_DIR" == "$HOME/Developer/dotfiles-hd" \ || "${DOTFILES_ALLOW_NONCANONICAL:-0}" == "1" ]] \ || die "--apply requires the canonical clone at $HOME/Developer/dotfiles-hd" @@ -112,6 +130,10 @@ if [[ "$MODE" == "dry-run" ]]; then say "would apply the $PROFILE Chezmoi profile and portable Git aliases" say "would restore locked Neovim plugins and required Tree-sitter parsers without changing lazy-lock.json" say "would run the verification doctor" + if [[ "$PROFILE" == mac-studio ]]; then + say "would install Rosetta 2 and vagrant-vmware-desktop 3.0.5 when missing" + say "VMware Fusion remains a manual install; Ollama models remain machine-owned" + fi write_zprofile_block "$HOME/.zprofile" "$MISE_FRAGMENT" "$STAMP" 1 if [[ -x "${CHEZMOI_BIN:-$HOME/.local/bin/chezmoi}" ]]; then @@ -129,6 +151,12 @@ if [[ "$MODE" == "check" ]]; then say "dotfiles: $DOTFILES_DIR" say "mode: check (no installs or managed-config writes)" + if [[ "$PROFILE" == mac-studio ]]; then + rosetta_installed || { say "Rosetta 2 missing"; status=1; } + vagrant_vmware_plugin_current \ + || { say "Vagrant VMware provider 3.0.5 missing"; status=1; } + fi + for brewfile in "$COMMON_BREWFILE" "$PROFILE_BREWFILE"; do if HOMEBREW_NO_AUTO_UPDATE=1 brew bundle check --no-upgrade --file "$brewfile"; then say "Brewfile satisfied: $brewfile" @@ -156,12 +184,31 @@ DOTFILES_CHEZMOI_CONFIG_ONLY_PREVIEW=1 \ DOTFILES_CHEZMOI_REQUIRE_REVIEWED=1 \ "$CHEZMOI_BOOTSTRAP" "$PROFILE" --preview >/dev/null +if [[ "$PROFILE" == mac-studio ]] && ! rosetta_installed; then + say "Installing Rosetta 2 for the Vagrant VMware utility..." + "$SUDO" "$SOFTWAREUPDATE" --install-rosetta --agree-to-license +fi + say "Installing shared Homebrew dependencies without broad upgrades..." HOMEBREW_NO_AUTO_UPDATE=1 brew bundle install --no-upgrade --file "$COMMON_BREWFILE" say "Installing $PROFILE Homebrew overlay without broad upgrades..." HOMEBREW_NO_AUTO_UPDATE=1 brew bundle install --no-upgrade --file "$PROFILE_BREWFILE" hash -r +if [[ "$PROFILE" == mac-studio ]]; then + if ! command -v vagrant >/dev/null 2>&1; then + say "Repairing the incomplete Vagrant package install..." + HOMEBREW_NO_AUTO_UPDATE=1 brew reinstall --cask vagrant + hash -r + fi + + if ! vagrant_vmware_plugin_current; then + say "Installing pinned Vagrant VMware provider..." + vagrant plugin install vagrant-vmware-desktop \ + --plugin-version "$VAGRANT_VMWARE_PLUGIN_VERSION" + fi +fi + say "Installing pinned mise runtimes..." MISE_NO_CONFIG=1 mise install "${MISE_SPECS[@]}" diff --git a/hosts/shared/macos/doctor.sh b/hosts/shared/macos/doctor.sh index 3eba0c48..0caaf4ba 100755 --- a/hosts/shared/macos/doctor.sh +++ b/hosts/shared/macos/doctor.sh @@ -9,12 +9,19 @@ CHEZMOI_DOCTOR="${DOTFILES_CHEZMOI_DOCTOR:-$DOTFILES_DIR/chezmoi/doctor.sh}" PROFILE="" FAILURES=0 MISE_RUNTIME_FAILURES=0 +APPLICATIONS_DIR="${DOTFILES_APPLICATIONS_DIR:-/Applications}" +PKGUTIL="${DOTFILES_PKGUTIL:-/usr/sbin/pkgutil}" +VAGRANT_VMWARE_PLUGIN_VERSION="3.0.5" +VAGRANT_VMWARE_UTILITY="${DOTFILES_VAGRANT_VMWARE_UTILITY:-/opt/vagrant-vmware-desktop/bin/vagrant-vmware-utility}" +VAGRANT_VMWARE_SERVICE_LABEL="com.vagrant.vagrant-vmware-utility" +SSH_CONFIG="$HOME/.ssh/config" +UBUNTU_LOGIN_KEY="$HOME/.ssh/id_ed25519_ubuntu_vm" # shellcheck source=lib.sh source "$SCRIPT_DIR/lib.sh" usage() { - printf 'Usage: doctor.sh --profile mac-pro|mac-mini\n' + printf 'Usage: doctor.sh --profile mac-pro|mac-studio|mac-mini\n' } pass() { @@ -26,6 +33,37 @@ fail() { FAILURES=$((FAILURES + 1)) } +vagrant_vmware_plugin_current() { + command -v vagrant >/dev/null 2>&1 \ + && vagrant plugin list 2>/dev/null \ + | /usr/bin/grep -Eq \ + "^vagrant-vmware-desktop \($VAGRANT_VMWARE_PLUGIN_VERSION([,)])" +} + +check_studio_ubuntu_ssh_alias() { + local ssh_alias="$1" + local expected_hostname="$2" + local expected_port="$3" + local effective + + effective="$(ssh -G -F "$SSH_CONFIG" "$ssh_alias" 2>/dev/null || true)" + if printf '%s\n' "$effective" \ + | /usr/bin/grep -Fxq "hostname $expected_hostname" \ + && printf '%s\n' "$effective" | /usr/bin/grep -Fxq "port $expected_port" \ + && printf '%s\n' "$effective" | /usr/bin/grep -Fxq "user hamel" \ + && printf '%s\n' "$effective" | /usr/bin/grep -Fxq "addressfamily inet" \ + && printf '%s\n' "$effective" | /usr/bin/grep -Fxq "identitiesonly yes" \ + && printf '%s\n' "$effective" | /usr/bin/grep -Fxq "forwardagent no" \ + && printf '%s\n' "$effective" | /usr/bin/grep -Fxq "hostkeyalias ubuntu-dev" \ + && printf '%s\n' "$effective" | /usr/bin/grep -Fxq "stricthostkeychecking true" \ + && printf '%s\n' "$effective" \ + | /usr/bin/grep -Fq "$(basename "$UBUNTU_LOGIN_KEY")"; then + pass "$ssh_alias Ubuntu route" + else + fail "$ssh_alias Ubuntu route is missing or unsafe" + fi +} + while [[ $# -gt 0 ]]; do case "$1" in --profile) @@ -83,6 +121,64 @@ for brewfile in "$COMMON_BREWFILE" "$PROFILE_BREWFILE"; do fi done +if [[ "$PROFILE" == mac-studio ]]; then + if "$PKGUTIL" --pkg-info com.apple.pkg.RosettaUpdateAuto >/dev/null 2>&1; then + pass "Rosetta 2 installed" + else + fail "Rosetta 2 missing" + fi + + if command -v vagrant >/dev/null 2>&1; then + pass "Vagrant available" + else + fail "Vagrant missing" + fi + + if [[ -x "$VAGRANT_VMWARE_UTILITY" ]]; then + pass "Vagrant VMware utility available" + else + fail "Vagrant VMware utility missing" + fi + + if launchctl print "system/$VAGRANT_VMWARE_SERVICE_LABEL" >/dev/null 2>&1; then + pass "Vagrant VMware utility service active" + else + fail "Vagrant VMware utility service is not active" + fi + + if vagrant_vmware_plugin_current; then + pass "Vagrant VMware provider $VAGRANT_VMWARE_PLUGIN_VERSION" + else + fail "Vagrant VMware provider must be $VAGRANT_VMWARE_PLUGIN_VERSION" + fi + + for app_name in "VMware Fusion.app" "Ollama.app"; do + if [[ -d "$APPLICATIONS_DIR/$app_name" ]]; then + pass "$app_name installed" + else + fail "$app_name missing" + fi + done + + if [[ "${DOTFILES_MAC_STUDIO_CUTOVER:-0}" == "1" ]]; then + if [[ -r "$SSH_CONFIG" ]]; then + pass "SSH config readable" + check_studio_ubuntu_ssh_alias ubuntu-vm 127.0.0.1 2222 + check_studio_ubuntu_ssh_alias ubuntu-vm-ts ubuntu-dev 22 + else + fail "SSH config missing or unreadable" + fi + if [[ -f "$UBUNTU_LOGIN_KEY" \ + && "$(stat -f '%Lp' "$UBUNTU_LOGIN_KEY" 2>/dev/null)" == "600" ]]; then + pass "Ubuntu login key present with mode 600" + else + fail "Ubuntu login key missing or not mode 600" + fi + else + printf 'SKIP Studio Ubuntu SSH routes until DOTFILES_MAC_STUDIO_CUTOVER=1\n' + fi +fi + if "$GIT_ALIASES_SCRIPT" --check >/dev/null 2>&1; then pass "portable Git aliases" else diff --git a/hosts/shared/macos/lib.sh b/hosts/shared/macos/lib.sh index a58e4394..bb1c11cf 100755 --- a/hosts/shared/macos/lib.sh +++ b/hosts/shared/macos/lib.sh @@ -47,11 +47,11 @@ next_backup_path() { canonical_profile() { case "$1" in - mac-pro|mac-mini) + mac-pro|mac-studio|mac-mini) printf '%s\n' "$1" ;; *) - die "unknown profile '$1' (expected mac-pro or mac-mini)" + die "unknown profile '$1' (expected mac-pro, mac-studio, or mac-mini)" return 1 ;; esac @@ -233,6 +233,7 @@ load_profile() { MISE_FRAGMENT="$dotfiles_dir/hosts/shared/macos/mise-shims.zsh" case "$profile" in mac-pro) PROFILE_BREWFILE="$dotfiles_dir/hosts/mac-pro/Brewfile" ;; + mac-studio) PROFILE_BREWFILE="$dotfiles_dir/hosts/mac-studio/Brewfile" ;; mac-mini) PROFILE_BREWFILE="$dotfiles_dir/hosts/mac-mini/Brewfile" ;; esac } diff --git a/hosts/shared/macos/tests/bootstrap-test.sh b/hosts/shared/macos/tests/bootstrap-test.sh index 4bb0b1c3..b0b42633 100755 --- a/hosts/shared/macos/tests/bootstrap-test.sh +++ b/hosts/shared/macos/tests/bootstrap-test.sh @@ -625,6 +625,7 @@ test_profile_names_and_paths() { assert_eq mac-pro "$(canonical_profile mac-pro)" "mac-pro remains canonical" assert_eq mac-mini "$(canonical_profile mac-mini)" "mac-mini remains canonical" + assert_eq mac-studio "$(canonical_profile mac-studio)" "mac-studio is canonical" if canonical_profile mac-vm >/dev/null 2>&1; then fail "removed mac-vm profile should be rejected" fi @@ -643,6 +644,26 @@ test_profile_names_and_paths() { assert_contains "$REPO_DIR/chezmoi/profiles/mac-pro.paths" \ '.config/karabiner|config/karabiner' + load_profile mac-studio "$REPO_DIR" "$home_dir" + assert_eq "$REPO_DIR/hosts/mac-studio/Brewfile" "$PROFILE_BREWFILE" \ + "Mac Studio profile uses its Brewfile" + assert_contains "$PROFILE_BREWFILE" 'cask "ollama-app"' + assert_contains "$PROFILE_BREWFILE" 'cask "vagrant"' + assert_contains "$PROFILE_BREWFILE" 'cask "vagrant-vmware-utility"' + assert_contains "$REPO_DIR/chezmoi/profiles/mac-studio.paths" \ + '.zshrc|hosts/mac-studio/.zshrc' + assert_not_contains "$REPO_DIR/chezmoi/profiles/mac-studio.paths" \ + '.config/karabiner|config/karabiner' + assert_contains "$MAC_BOOTSTRAP_DIR/bootstrap.sh" \ + 'would install Rosetta 2 and vagrant-vmware-desktop 3.0.5 when missing' + assert_contains "$MAC_BOOTSTRAP_DIR/bootstrap.sh" \ + 'brew reinstall --cask vagrant' + assert_contains "$MAC_BOOTSTRAP_DIR/bootstrap.sh" \ + 'vagrant plugin install vagrant-vmware-desktop' + assert_contains "$MAC_BOOTSTRAP_DIR/doctor.sh" 'Vagrant VMware utility service active' + assert_contains "$MAC_BOOTSTRAP_DIR/doctor.sh" 'VMware Fusion.app' + assert_contains "$MAC_BOOTSTRAP_DIR/doctor.sh" 'Ollama.app' + while IFS= read -r helper_path; do [[ -f "$REPO_DIR/$helper_path" ]] \ || fail "missing Work Mac helper: $helper_path" @@ -952,6 +973,105 @@ test_mac_mini_apply() { assert_eq "$(cat "$protected_before")" "$(cat "$protected_after")" "Mac mini apply preserves protected state" } +test_mac_studio_apply() { + local root="$TMP_ROOT/mac-studio-apply" + local home_dir="$root/home" + local fake_bin="$root/bin" + local log="$root/commands.log" + local rosetta_marker="$root/rosetta-installed" + local plugin_marker="$root/vagrant-plugin-installed" + + mkdir -p "$home_dir/Developer" + ln -s "$REPO_DIR" "$home_dir/Developer/dotfiles-hd" + make_fake_toolchain "$fake_bin" + : > "$log" + + cat > "$fake_bin/vagrant" <<'EOF' +#!/usr/bin/env bash +printf 'vagrant %s\n' "$*" >> "${COMMAND_LOG:?}" +if [[ "$*" == 'plugin list' && -f "${VAGRANT_PLUGIN_MARKER:?}" ]]; then + printf 'vagrant-vmware-desktop (3.0.5, global)\n' +elif [[ "$*" == 'plugin install vagrant-vmware-desktop --plugin-version 3.0.5' ]]; then + : > "$VAGRANT_PLUGIN_MARKER" +fi +EOF + chmod +x "$fake_bin/vagrant" + + cat > "$fake_bin/pkgutil" <<'EOF' +#!/usr/bin/env bash +[[ "$*" == '--pkg-info com.apple.pkg.RosettaUpdateAuto' ]] || exit 2 +[[ -f "${ROSETTA_MARKER:?}" ]] +EOF + cat > "$fake_bin/softwareupdate" <<'EOF' +#!/usr/bin/env bash +printf 'softwareupdate %s\n' "$*" >> "${COMMAND_LOG:?}" +[[ "$*" == '--install-rosetta --agree-to-license' ]] || exit 2 +: > "${ROSETTA_MARKER:?}" +EOF + cat > "$fake_bin/sudo" <<'EOF' +#!/usr/bin/env bash +printf 'sudo %s\n' "$*" >> "${COMMAND_LOG:?}" +"$@" +EOF + chmod +x "$fake_bin/pkgutil" "$fake_bin/softwareupdate" "$fake_bin/sudo" + + if HOME="$home_dir" PATH="$fake_bin:$PATH" COMMAND_LOG="$log" \ + DOTFILES_DIR="$REPO_DIR" DOTFILES_ALLOW_DIRTY=1 \ + DOTFILES_ALLOW_NONCANONICAL=1 DOTFILES_MAC_DOCTOR=/usr/bin/true \ + "$MAC_BOOTSTRAP_DIR/bootstrap.sh" --profile mac-studio --apply \ + >/dev/null 2>&1; then + fail "Mac Studio apply should be locked before hardware arrival" + fi + TESTS=$((TESTS + 1)) + assert_eq '' "$(cat "$log")" "arrival gate stops before package managers" + + HOME="$home_dir" PATH="$fake_bin:$PATH" COMMAND_LOG="$log" \ + DOTFILES_DIR="$REPO_DIR" DOTFILES_ALLOW_DIRTY=1 \ + DOTFILES_ALLOW_NONCANONICAL=1 DOTFILES_MAC_DOCTOR=/usr/bin/true \ + DOTFILES_MAC_STUDIO_ARRIVED=1 \ + DOTFILES_PKGUTIL="$fake_bin/pkgutil" \ + DOTFILES_SOFTWAREUPDATE="$fake_bin/softwareupdate" \ + DOTFILES_SUDO="$fake_bin/sudo" ROSETTA_MARKER="$rosetta_marker" \ + VAGRANT_PLUGIN_MARKER="$plugin_marker" \ + "$MAC_BOOTSTRAP_DIR/bootstrap.sh" --profile mac-studio --apply >/dev/null + + assert_file "$rosetta_marker" + assert_file "$plugin_marker" + assert_contains "$log" 'softwareupdate --install-rosetta --agree-to-license' + assert_contains "$log" \ + 'vagrant plugin install vagrant-vmware-desktop --plugin-version 3.0.5' + assert_not_contains "$log" 'brew services' + assert_not_contains "$log" 'ollama serve' + assert_not_contains "$log" 'ollama pull' + + mkdir -p \ + "$root/applications/VMware Fusion.app" \ + "$root/applications/Ollama.app" \ + "$root/vagrant-vmware" + printf '#!/usr/bin/env bash\nexit 0\n' > "$root/vagrant-vmware/utility" + chmod +x "$root/vagrant-vmware/utility" + cat > "$fake_bin/launchctl" <<'EOF' +#!/usr/bin/env bash +printf 'launchctl %s\n' "$*" >> "${COMMAND_LOG:?}" +[[ "$*" == 'print system/com.vagrant.vagrant-vmware-utility' ]] +EOF + chmod +x "$fake_bin/launchctl" + mkdir -p "$home_dir/.ssh" + printf 'Include %s\n' \ + "$REPO_DIR/hosts/mac-studio/ssh/ubuntu-vagrant.conf" \ + > "$home_dir/.ssh/config" + : > "$home_dir/.ssh/id_ed25519_ubuntu_vm" + chmod 600 "$home_dir/.ssh/id_ed25519_ubuntu_vm" + + HOME="$home_dir" PATH="$fake_bin:$PATH" COMMAND_LOG="$log" \ + DOTFILES_DIR="$REPO_DIR" DOTFILES_PKGUTIL="$fake_bin/pkgutil" \ + DOTFILES_APPLICATIONS_DIR="$root/applications" \ + DOTFILES_VAGRANT_VMWARE_UTILITY="$root/vagrant-vmware/utility" \ + DOTFILES_MAC_STUDIO_CUTOVER=1 \ + ROSETTA_MARKER="$rosetta_marker" VAGRANT_PLUGIN_MARKER="$plugin_marker" \ + "$MAC_BOOTSTRAP_DIR/doctor.sh" --profile mac-studio >/dev/null +} + test_xdg_bin_home() { local root="$TMP_ROOT/xdg-bin-home" local home_dir="$root/home" @@ -971,6 +1091,7 @@ test_xdg_bin_home() { for zshrc in \ "$REPO_DIR/hosts/mac-pro/.zshrc" \ + "$REPO_DIR/hosts/mac-studio/.zshrc" \ "$REPO_DIR/hosts/mac-mini/.zshrc"; do resolved="$(HOME="$home_dir" XDG_BIN_HOME="$custom_bin" PATH=/usr/bin:/bin \ zsh -dfc "source '$MAC_BOOTSTRAP_DIR/mise-shims.zsh'; source '$zshrc'; command -v ruff" \ @@ -1678,6 +1799,7 @@ test_neovim_plugin_checkout_integrity test_neovim_parser_manifest test_profile_names_and_paths test_full_bootstrap +test_mac_studio_apply test_mac_mini_apply test_xdg_bin_home test_profile_and_failure_guards diff --git a/hosts/tests/run.sh b/hosts/tests/run.sh index 22c60331..dc21daa5 100755 --- a/hosts/tests/run.sh +++ b/hosts/tests/run.sh @@ -8,6 +8,7 @@ tests=( hosts/shared/macos/tests/bootstrap-test.sh hosts/mac-thin/tests/thin-bootstrap-test.sh hosts/mac-thin/tests/vm-lifecycle-test.sh + hosts/mac-studio/tests/vm-lifecycle-test.sh hosts/mac-work/tests/goodmorning-test.sh hosts/mac-work/herdr/tests/hd-lib-test.sh hosts/mac-work/herdr/tests/hd-pargasite-test.sh diff --git a/hosts/ubuntu-dev/GUIDE.md b/hosts/ubuntu-dev/GUIDE.md index 88cd0ac1..ac543198 100644 --- a/hosts/ubuntu-dev/GUIDE.md +++ b/hosts/ubuntu-dev/GUIDE.md @@ -5,7 +5,9 @@ Linux mental model behind the commands instead of treating Ubuntu like macOS with different spellings. The current workstation is Ubuntu 26.04 ARM64, Zsh, systemd, APT, mise, Docker, -Neovim, and tmux. The thin Mac is the control plane; development stays here. +Neovim, and tmux. The thin Mac is the current control plane. After the approved +hardware cutover, the VM will live inside the Mac Studio; development remains +inside Ubuntu. ## The 60-Second Orientation diff --git a/hosts/ubuntu-dev/README.md b/hosts/ubuntu-dev/README.md index 15234e7e..fa4905f4 100644 --- a/hosts/ubuntu-dev/README.md +++ b/hosts/ubuntu-dev/README.md @@ -1,7 +1,8 @@ # Ubuntu Workstation -Ubuntu 26.04 ARM64 is the primary development workstation. The thin Mac runs -Vagrant and VMware Fusion only as its control plane. +Ubuntu 26.04 ARM64 is the primary development workstation. It currently lives +inside the thin Mac. After an approved cutover, it will be rebuilt inside the +Mac Studio; this staged profile does not move the current VM. ## Rebuild @@ -14,6 +15,9 @@ Prerequisites on the thin Mac: - The tailnet policy defines `tag:ubuntu-dev` and lets `hd719@github` connect to that tag as the `hamel` user. +After Mac Studio cutover, use the equivalent `mac-studio` bootstrap and VM +commands from [`../mac-studio/README.md`](../mac-studio/README.md). + The Forgejo Tailscale route also requires ordinary network access from Ubuntu to TrueNAS. Add this rule to the tailnet policy's `acls` array: From 68d9bfa491c48ef9ba15ebcd41c32d07d4efec49 Mon Sep 17 00:00:00 2001 From: arbiter-hd <288758862+arbiter-hd@users.noreply.github.com> Date: Tue, 15 Sep 2026 13:39:00 -0400 Subject: [PATCH 2/5] feat: stage native Studio development and lightweight Air access --- AGENTS.md | 21 +- README.md | 22 ++- chezmoi/README.md | 21 +- chezmoi/apply.sh | 2 + chezmoi/bootstrap.sh | 6 +- chezmoi/lib.sh | 6 +- chezmoi/preview.sh | 4 +- chezmoi/profiles/mac-air.paths | 4 + chezmoi/profiles/mac-air.toml | 2 + chezmoi/source/.chezmoiignore.tmpl | 15 ++ .../dot_config/homebrew/symlink_Brewfile.tmpl | 2 + .../symlink_config.tmpl | 4 + chezmoi/source/symlink_dot_zshrc.tmpl | 2 + chezmoi/tests/production-test.sh | 36 ++-- hosts/mac-air/.zshrc | 24 +++ hosts/mac-air/Brewfile | 14 ++ hosts/mac-air/README.md | 46 +++++ hosts/mac-air/bootstrap.sh | 48 +++++ hosts/mac-air/doctor.sh | 30 +++ hosts/mac-air/ghostty.conf | 4 + hosts/mac-air/tests/client-profile-test.sh | 81 ++++++++ hosts/mac-mini/Brewfile | 5 + hosts/mac-mini/README.md | 23 +++ hosts/mac-studio/.zshrc | 4 +- hosts/mac-studio/Brewfile | 11 +- hosts/mac-studio/README.md | 183 ++++++++++-------- hosts/mac-studio/tests/vm-lifecycle-test.sh | 101 +++++----- hosts/mac-studio/vm.zsh | 37 ++-- hosts/shared/macos/README.md | 22 ++- hosts/shared/macos/bootstrap.sh | 42 +--- hosts/shared/macos/doctor.sh | 85 +------- hosts/shared/macos/tests/bootstrap-test.sh | 90 ++------- hosts/tests/run.sh | 1 + hosts/ubuntu-dev/GUIDE.md | 7 +- hosts/ubuntu-dev/README.md | 54 +++--- 35 files changed, 615 insertions(+), 444 deletions(-) create mode 100644 chezmoi/profiles/mac-air.paths create mode 100644 chezmoi/profiles/mac-air.toml create mode 100644 hosts/mac-air/.zshrc create mode 100644 hosts/mac-air/Brewfile create mode 100644 hosts/mac-air/README.md create mode 100755 hosts/mac-air/bootstrap.sh create mode 100755 hosts/mac-air/doctor.sh create mode 100644 hosts/mac-air/ghostty.conf create mode 100755 hosts/mac-air/tests/client-profile-test.sh diff --git a/AGENTS.md b/AGENTS.md index 23639348..e0f8e275 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,14 +16,15 @@ constraints, and agent workflow policy here. ## Profile Routing -| Target | Source of truth | -| ------------------------------------------ | ------------------------------------ | -| Thin personal MacBook (`mac-thin`) | `hosts/mac-thin/README.md` | -| Staged primary Mac Studio (`mac-studio`) | `hosts/mac-studio/README.md` | -| Standalone development MacBook (`mac-pro`) | `hosts/mac-pro/README.md` | -| Personal Mac mini (`mac-mini`) | `hosts/mac-mini/README.md` | -| Resilience work Mac (`mac-work`) | `hosts/mac-work/README.md` | -| Ubuntu workstation | `hosts/ubuntu-dev/README.md` | +| Target | Source of truth | +| ------------------------------------------ | ---------------------------- | +| Thin personal MacBook (`mac-thin`) | `hosts/mac-thin/README.md` | +| Future access-only Air (`mac-air`) | `hosts/mac-air/README.md` | +| Staged primary Mac Studio (`mac-studio`) | `hosts/mac-studio/README.md` | +| Standalone development MacBook (`mac-pro`) | `hosts/mac-pro/README.md` | +| Personal Mac mini (`mac-mini`) | `hosts/mac-mini/README.md` | +| Resilience work Mac (`mac-work`) | `hosts/mac-work/README.md` | +| Ubuntu workstation | `hosts/ubuntu-dev/README.md` | `config/` holds portable configuration and must not be reorganized casually. `chezmoi/` owns approved user-config delivery and rollback. `hosts/` owns @@ -66,6 +67,10 @@ The `mac-studio` profile is pre-arrival staging. Do not apply it, move or destroy the Ubuntu VM, or change live host routing until the Studio arrives and Hamel approves the cutover gates in its runbook. Studio apply requires `DOTFILES_MAC_STUDIO_ARRIVED=1`; never set it on another host. +The future `mac-air` client profile requires `DOTFILES_MAC_AIR_ARRIVED=1` on +that device. Follow the Studio runbook for native development, dormant Ubuntu +preservation, mini development isolation, and the later Air rollout. Update +canonical topology and sync policies only after verified live cutover. ## Package Ownership diff --git a/README.md b/README.md index a1d10d97..25ebbfda 100644 --- a/README.md +++ b/README.md @@ -4,14 +4,15 @@ Hamel's profile-aware dotfiles, provisioning, and machine operations. ## Choose a Host -| Host | Role | Entry point | -| --- | --- | --- | -| Thin Mac | Control plane for Codex, SSH, Vagrant, and VMware Fusion | [`hosts/mac-thin/README.md`](hosts/mac-thin/README.md) | -| Ubuntu dev | Primary VM development workstation | [`hosts/ubuntu-dev/README.md`](hosts/ubuntu-dev/README.md) | -| Mac Studio | Staged primary controller, VM host, and local-AI machine | [`hosts/mac-studio/README.md`](hosts/mac-studio/README.md) | -| Mac Pro | Standalone full-development MacBook; local Brew stack, no VM | [`hosts/mac-pro/README.md`](hosts/mac-pro/README.md) | -| Mac mini | Production runtime host | [`hosts/mac-mini/README.md`](hosts/mac-mini/README.md) | -| Work Mac | Company-scoped terminal and editor setup | [`hosts/mac-work/README.md`](hosts/mac-work/README.md) | +| Host | Role | Entry point | +| ---------- | ------------------------------------------------------------------ | ---------------------------------------------------------- | +| Thin Mac | Control plane for Codex, SSH, Vagrant, and VMware Fusion | [`hosts/mac-thin/README.md`](hosts/mac-thin/README.md) | +| Ubuntu dev | Primary VM development workstation | [`hosts/ubuntu-dev/README.md`](hosts/ubuntu-dev/README.md) | +| Mac Studio | Staged native development workstation; dormant Ubuntu and local AI | [`hosts/mac-studio/README.md`](hosts/mac-studio/README.md) | +| Future Air | Lightweight clients and remote Studio access; no VMware | [`hosts/mac-air/README.md`](hosts/mac-air/README.md) | +| Mac Pro | Standalone full-development MacBook; local Brew stack, no VM | [`hosts/mac-pro/README.md`](hosts/mac-pro/README.md) | +| Mac mini | Production with explicitly selected secondary development | [`hosts/mac-mini/README.md`](hosts/mac-mini/README.md) | +| Work Mac | Company-scoped terminal and editor setup | [`hosts/mac-work/README.md`](hosts/mac-work/README.md) | Clone at the canonical path: @@ -34,7 +35,8 @@ dotfiles-hd/ │ ├── tests/ one host-validation runner │ ├── shared/macos/ shared full-Mac provisioning and doctor │ ├── mac-thin/ control plane and VM lifecycle -│ ├── mac-studio/ staged primary controller, VM host, and local AI +│ ├── mac-studio/ staged native development and dormant Ubuntu +│ ├── mac-air/ future lightweight remote clients │ ├── ubuntu-dev/ Vagrant guest provisioning and maintenance │ ├── mac-pro/ standalone development MacBook policy │ ├── mac-mini/ production runtime Mac policy @@ -95,7 +97,7 @@ gates pass. - `chezmoi/` owns approved user configuration links and timestamped rollback. - `hosts/mac-thin/` owns the Vagrant and VMware lifecycle. -- `hosts/mac-studio/` becomes the VM lifecycle owner only after approved cutover. +- `hosts/mac-studio/` owns native development and preserves dormant Ubuntu after approved cutover. - `hosts/ubuntu-dev/` owns guest provisioning and workstation maintenance. - `hosts/shared/macos/` owns common full-Mac packages and operational setup. - `hosts/mac-pro/` and `hosts/mac-mini/` own profile package overlays and shell entry points. diff --git a/chezmoi/README.md b/chezmoi/README.md index 27b18029..dc74891e 100644 --- a/chezmoi/README.md +++ b/chezmoi/README.md @@ -9,14 +9,15 @@ services, secrets, identity, enrollment, macOS preferences, or mutable state. ## Profiles -| Profile | Host | Package behavior | -| --- | --- | --- | -| `ubuntu` | Ubuntu development VM | Installs the declared mise toolchain | -| `mac-thin` | Thin Mac control plane | Installs the thin Brewfile | -| `mac-pro` | Standalone full-development MacBook | Installs shared and profile Brewfiles through the Mac bootstrap | -| `mac-studio` | Staged primary Mac, VM host, and local AI | Installs shared and profile Brewfiles through the Mac bootstrap | -| `mac-mini` | Production Mac mini | Configuration-only inside Chezmoi; packages stay in the guarded Mac bootstrap | -| `mac-work` | Resilience work Mac | Reserved for a later opt-in rollout | +| Profile | Host | Package behavior | +| ------------ | ------------------------------------------------------------ | ----------------------------------------------------------------------------- | +| `ubuntu` | Ubuntu development VM | Installs the declared mise toolchain | +| `mac-thin` | Thin Mac control plane | Installs the thin Brewfile | +| `mac-air` | Future remote-access clients; no VMware or local development | Client packages through the Air bootstrap | +| `mac-pro` | Standalone full-development MacBook | Installs shared and profile Brewfiles through the Mac bootstrap | +| `mac-studio` | Staged native development, dormant Ubuntu and local AI | Installs shared and profile Brewfiles through the Mac bootstrap | +| `mac-mini` | Production Mac mini | Configuration-only inside Chezmoi; packages stay in the guarded Mac bootstrap | +| `mac-work` | Resilience work Mac | Reserved for a later opt-in rollout | Each `profiles/*.paths` file is the exact backup, apply, doctor, and rollback allowlist. A matching `*.ancestors` file records parent directories whose @@ -28,7 +29,7 @@ original type must be restored exactly. ./chezmoi/bootstrap.sh ubuntu --preview ``` -Substitute `mac-thin`, `mac-pro`, `mac-studio`, `mac-mini`, or `mac-work` as +Substitute `mac-thin`, `mac-air`, `mac-pro`, `mac-studio`, `mac-mini`, or `mac-work` as needed. After review and host approval: @@ -81,5 +82,5 @@ CHEZMOI_BIN="$HOME/.local/bin/chezmoi" \ bash chezmoi/tests/production-test.sh ``` -The test renders and exercises all six profiles in temporary homes. It covers +The test renders and exercises all supported profiles in temporary homes. It covers backup, apply, second-apply idempotence, doctor, rollback, and recovery/reapply. diff --git a/chezmoi/apply.sh b/chezmoi/apply.sh index 896d3266..bfb1107a 100755 --- a/chezmoi/apply.sh +++ b/chezmoi/apply.sh @@ -11,6 +11,8 @@ require_canonical_checkout || die "set DOTFILES_CHEZMOI_APPROVED=1 after approving this host and preview" [[ "$PROFILE" != mac-studio || "${DOTFILES_MAC_STUDIO_ARRIVED:-0}" == 1 ]] \ || die "mac-studio apply requires DOTFILES_MAC_STUDIO_ARRIVED=1 after the hardware arrives" +[[ "$PROFILE" != mac-air || "${DOTFILES_MAC_AIR_ARRIVED:-0}" == 1 ]] \ + || die "mac-air apply requires DOTFILES_MAC_AIR_ARRIVED=1 after the hardware arrives" [[ "$PROFILE" != mac-mini || "${DOTFILES_MAC_MINI_CONFIG_ONLY:-0}" == 1 ]] \ || die "Mac mini requires DOTFILES_MAC_MINI_CONFIG_ONLY=1" [[ "$PROFILE" != mac-work || "${DOTFILES_WORK_MAC_OPT_IN:-0}" == 1 ]] \ diff --git a/chezmoi/bootstrap.sh b/chezmoi/bootstrap.sh index a5be51b8..ce5e7203 100755 --- a/chezmoi/bootstrap.sh +++ b/chezmoi/bootstrap.sh @@ -4,11 +4,11 @@ profile="${1:-}"; mode="${2:---preview}"; version="v2.72.0" script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)" bin="${CHEZMOI_BIN:-$HOME/.local/bin/chezmoi}" case "$profile:$mode" in - ubuntu:--preview|ubuntu:--apply|mac-thin:--preview|mac-thin:--apply|mac-pro:--preview|mac-pro:--apply|mac-studio:--preview|mac-studio:--apply|mac-mini:--preview|mac-mini:--apply|mac-work:--preview|mac-work:--apply) ;; - *) echo "usage: $0 ubuntu|mac-thin|mac-pro|mac-studio|mac-mini|mac-work --preview|--apply" >&2; exit 2 ;; + ubuntu:--preview|ubuntu:--apply|mac-thin:--preview|mac-thin:--apply|mac-air:--preview|mac-air:--apply|mac-pro:--preview|mac-pro:--apply|mac-studio:--preview|mac-studio:--apply|mac-mini:--preview|mac-mini:--apply|mac-work:--preview|mac-work:--apply) ;; + *) echo "usage: $0 ubuntu|mac-thin|mac-air|mac-pro|mac-studio|mac-mini|mac-work --preview|--apply" >&2; exit 2 ;; esac case "$profile:$(uname -s)" in - ubuntu:Linux|mac-thin:Darwin|mac-pro:Darwin|mac-studio:Darwin|mac-mini:Darwin|mac-work:Darwin) ;; + ubuntu:Linux|mac-thin:Darwin|mac-air:Darwin|mac-pro:Darwin|mac-studio:Darwin|mac-mini:Darwin|mac-work:Darwin) ;; *) [ "${DOTFILES_CHEZMOI_TEST:-0}" = 1 ] || { echo "profile/OS mismatch" >&2; exit 1; } ;; esac if [ "$mode" = --apply ] || [ "${DOTFILES_CHEZMOI_REQUIRE_REVIEWED:-0}" = 1 ]; then diff --git a/chezmoi/lib.sh b/chezmoi/lib.sh index 41717147..56e4430c 100755 --- a/chezmoi/lib.sh +++ b/chezmoi/lib.sh @@ -21,8 +21,8 @@ die() { load_profile() { PROFILE="${1:-}" case "$PROFILE" in - ubuntu|mac-thin|mac-pro|mac-studio|mac-mini|mac-work) ;; - *) die "profile must be ubuntu, mac-thin, mac-pro, mac-studio, mac-mini, or mac-work" ;; + ubuntu|mac-thin|mac-air|mac-pro|mac-studio|mac-mini|mac-work) ;; + *) die "profile must be ubuntu, mac-thin, mac-air, mac-pro, mac-studio, mac-mini, or mac-work" ;; esac PROFILE_CONFIG="$PROFILES_DIR/$PROFILE.toml" PROFILE_MANIFEST="$PROFILES_DIR/$PROFILE.paths" @@ -46,7 +46,7 @@ activate_profile() { validate_profile_os() { [[ "${DOTFILES_CHEZMOI_TEST:-0}" == 1 ]] && return case "$PROFILE:$(uname -s)" in - ubuntu:Linux|mac-thin:Darwin|mac-pro:Darwin|mac-studio:Darwin|mac-mini:Darwin|mac-work:Darwin) ;; + ubuntu:Linux|mac-thin:Darwin|mac-air:Darwin|mac-pro:Darwin|mac-studio:Darwin|mac-mini:Darwin|mac-work:Darwin) ;; *) die "$PROFILE does not match $(uname -s)" ;; esac } diff --git a/chezmoi/preview.sh b/chezmoi/preview.sh index d499e2bd..1f4033c2 100755 --- a/chezmoi/preview.sh +++ b/chezmoi/preview.sh @@ -75,10 +75,10 @@ if [[ "${DOTFILES_CHEZMOI_CONFIG_ONLY_PREVIEW:-0}" != 1 ]]; then command -v mdformat >/dev/null 2>&1 \ || printf ' install pinned mdformat bundle\n' ;; - mac-thin) + mac-thin|mac-air) if command -v brew >/dev/null 2>&1; then HOMEBREW_NO_AUTO_UPDATE=1 brew bundle check --verbose \ - --file "$REPO_DIR/hosts/mac-thin/Brewfile" || true + --file "$REPO_DIR/hosts/$PROFILE/Brewfile" || true else printf ' Homebrew is required before the approved apply\n' fi diff --git a/chezmoi/profiles/mac-air.paths b/chezmoi/profiles/mac-air.paths new file mode 100644 index 00000000..01f938b6 --- /dev/null +++ b/chezmoi/profiles/mac-air.paths @@ -0,0 +1,4 @@ +.zshrc|hosts/mac-air/.zshrc +.config/starship.toml|config/starship/starship.toml +.config/homebrew/Brewfile|hosts/mac-air/Brewfile +Library/Application Support/com.mitchellh.ghostty/config|hosts/mac-air/ghostty.conf diff --git a/chezmoi/profiles/mac-air.toml b/chezmoi/profiles/mac-air.toml new file mode 100644 index 00000000..4400b6b7 --- /dev/null +++ b/chezmoi/profiles/mac-air.toml @@ -0,0 +1,2 @@ +[data] +profile = "mac-air" diff --git a/chezmoi/source/.chezmoiignore.tmpl b/chezmoi/source/.chezmoiignore.tmpl index 56b26e33..af0f6371 100644 --- a/chezmoi/source/.chezmoiignore.tmpl +++ b/chezmoi/source/.chezmoiignore.tmpl @@ -68,3 +68,18 @@ Library 20-install-ubuntu-tools.sh 30-install-thin-tools.sh {{- end }} +{{- if eq .profile "mac-air" }} +.config/zsh +.config/bookokrat +.config/btop +.config/fastfetch +.config/herdr +.config/hunk +.config/mise +.config/nvim +.hermes +.terminfo +20-install-ubuntu-tools.sh +30-install-thin-tools.sh +40-install-work-tools.sh +{{- end }} diff --git a/chezmoi/source/dot_config/homebrew/symlink_Brewfile.tmpl b/chezmoi/source/dot_config/homebrew/symlink_Brewfile.tmpl index b7ec9ed9..53bf16a2 100644 --- a/chezmoi/source/dot_config/homebrew/symlink_Brewfile.tmpl +++ b/chezmoi/source/dot_config/homebrew/symlink_Brewfile.tmpl @@ -1,5 +1,7 @@ {{- if eq .profile "mac-thin" -}} {{ joinPath .chezmoi.sourceDir ".." ".." "hosts" "mac-thin" "Brewfile" }} +{{- else if eq .profile "mac-air" -}} +{{ joinPath .chezmoi.sourceDir ".." ".." "hosts" "mac-air" "Brewfile" }} {{- else -}} {{ joinPath .chezmoi.sourceDir ".." ".." "hosts" "mac-work" "Brewfile" }} {{- end -}} diff --git a/chezmoi/source/private_Library/private_Application Support/private_com.mitchellh.ghostty/symlink_config.tmpl b/chezmoi/source/private_Library/private_Application Support/private_com.mitchellh.ghostty/symlink_config.tmpl index bdece602..ad1b8ee1 100644 --- a/chezmoi/source/private_Library/private_Application Support/private_com.mitchellh.ghostty/symlink_config.tmpl +++ b/chezmoi/source/private_Library/private_Application Support/private_com.mitchellh.ghostty/symlink_config.tmpl @@ -1 +1,5 @@ +{{- if eq .profile "mac-air" -}} +{{ joinPath .chezmoi.sourceDir ".." ".." "hosts" "mac-air" "ghostty.conf" }} +{{- else -}} {{ joinPath .chezmoi.sourceDir ".." ".." "config" "ghostty" "config" }} +{{- end -}} diff --git a/chezmoi/source/symlink_dot_zshrc.tmpl b/chezmoi/source/symlink_dot_zshrc.tmpl index fb23971b..7a05b668 100644 --- a/chezmoi/source/symlink_dot_zshrc.tmpl +++ b/chezmoi/source/symlink_dot_zshrc.tmpl @@ -3,6 +3,8 @@ {{ joinPath $repo "hosts" "ubuntu-dev" ".zshrc" }} {{- else if eq .profile "mac-thin" -}} {{ joinPath $repo "hosts" "mac-thin" ".zshrc" }} +{{- else if eq .profile "mac-air" -}} +{{ joinPath $repo "hosts" "mac-air" ".zshrc" }} {{- else if eq .profile "mac-pro" -}} {{ joinPath $repo "hosts" "mac-pro" ".zshrc" }} {{- else if eq .profile "mac-studio" -}} diff --git a/chezmoi/tests/production-test.sh b/chezmoi/tests/production-test.sh index 641486f8..c9bfc587 100755 --- a/chezmoi/tests/production-test.sh +++ b/chezmoi/tests/production-test.sh @@ -178,7 +178,7 @@ set -e ((layout_status != 0)) [[ "$layout_output" == *"unapproved mac-thin symlink parent"* ]] -for profile in ubuntu mac-thin mac-pro mac-studio mac-mini mac-work; do +for profile in ubuntu mac-thin mac-air mac-pro mac-studio mac-mini mac-work; do home_dir="$case_dir/$profile/home" state_dir="$case_dir/$profile/state" mkdir -p "$home_dir" "$state_dir" @@ -207,6 +207,7 @@ for profile in ubuntu mac-thin mac-pro mac-studio mac-mini mac-work; do case "$profile" in ubuntu) printf '%s\n' 10-configure-git.sh 20-install-ubuntu-tools.sh ;; mac-thin) printf '%s\n' 10-configure-git.sh 30-install-thin-tools.sh ;; + mac-air) printf '%s\n' 10-configure-git.sh ;; mac-pro) printf '%s\n' 10-configure-git.sh ;; mac-studio) printf '%s\n' 10-configure-git.sh ;; mac-mini) printf '%s\n' 10-configure-git.sh ;; @@ -255,23 +256,22 @@ for profile in ubuntu mac-thin mac-pro mac-studio mac-mini mac-work; do bash "$CHEZMOI_DIR/doctor.sh" "$profile" >/dev/null done -studio_guard_home="$case_dir/mac-studio-guard/home" -studio_guard_state="$case_dir/mac-studio-guard/state" -mkdir -p "$studio_guard_home" "$studio_guard_state" -set +e -studio_guard_output="$({ - DOTFILES_CHEZMOI_TEST=1 \ - DOTFILES_CHEZMOI_APPROVED=1 \ - CHEZMOI_BIN="$CHEZMOI_BIN" \ - CHEZMOI_DESTINATION="$studio_guard_home" \ - CHEZMOI_STATE_DIR="$studio_guard_state" \ - bash "$CHEZMOI_DIR/apply.sh" mac-studio -} 2>&1)" -studio_guard_status=$? -set -e -((studio_guard_status != 0)) -[[ "$studio_guard_output" == \ - *"mac-studio apply requires DOTFILES_MAC_STUDIO_ARRIVED=1"* ]] +for staged_profile in mac-studio mac-air; do + guard_home="$case_dir/$staged_profile-guard/home" + guard_state="$case_dir/$staged_profile-guard/state" + mkdir -p "$guard_home" "$guard_state" + set +e + guard_output="$({ + DOTFILES_CHEZMOI_TEST=1 DOTFILES_CHEZMOI_APPROVED=1 \ + CHEZMOI_BIN="$CHEZMOI_BIN" CHEZMOI_DESTINATION="$guard_home" \ + CHEZMOI_STATE_DIR="$guard_state" \ + bash "$CHEZMOI_DIR/apply.sh" "$staged_profile" + } 2>&1)" + guard_status=$? + set -e + ((guard_status != 0)) + [[ "$guard_output" == *"$staged_profile apply requires DOTFILES_MAC_"* ]] +done rollback_home="$case_dir/rollback/home" rollback_state="$case_dir/rollback/state" diff --git a/hosts/mac-air/.zshrc b/hosts/mac-air/.zshrc new file mode 100644 index 00000000..91cb8a1c --- /dev/null +++ b/hosts/mac-air/.zshrc @@ -0,0 +1,24 @@ +# Lightweight remote-access shell. No local project runtimes or VM helpers. +export DOTFILES_MAC_PROFILE=mac-air +export EDITOR=vi +export VISUAL=vi +export GIT_EDITOR=vi +HISTFILE="$HOME/.zsh_history" +HISTSIZE=10000 +SAVEHIST=10000 +setopt append_history hist_ignore_all_dups share_history + +typeset -gaU path +path=("${HOMEBREW_PREFIX:-/opt/homebrew}/bin" "$HOME/.local/bin" $path) +export PATH + +if [[ -o interactive ]]; then + (( $+commands[zoxide] )) && eval "$(zoxide init --cmd cd zsh)" + (( $+commands[starship] )) && eval "$(starship init zsh)" + ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE='fg=#9399b2' + for air_plugin in zsh-autosuggestions zsh-syntax-highlighting; do + air_plugin_path="${HOMEBREW_PREFIX:-/opt/homebrew}/share/$air_plugin/$air_plugin.zsh" + [[ ! -r "$air_plugin_path" ]] || source "$air_plugin_path" + done + unset air_plugin air_plugin_path +fi diff --git a/hosts/mac-air/Brewfile b/hosts/mac-air/Brewfile new file mode 100644 index 00000000..5a7b5aeb --- /dev/null +++ b/hosts/mac-air/Brewfile @@ -0,0 +1,14 @@ +# Remote clients only. Project tools and execution belong on the Studio. +brew "gh" +brew "starship" +brew "zoxide" +brew "zsh-autosuggestions" +brew "zsh-syntax-highlighting" + +cask "1password" +cask "brave-browser" +cask "chatgpt" +cask "font-maple-mono-nf" +cask "ghostty" +cask "obsidian" +cask "tailscale-app" diff --git a/hosts/mac-air/README.md b/hosts/mac-air/README.md new file mode 100644 index 00000000..b0cc939a --- /dev/null +++ b/hosts/mac-air/README.md @@ -0,0 +1,46 @@ +# Future MacBook Air + +`mac-air` is the future lightweight access machine for Studio. It runs Codex, +Ghostty, a browser and Obsidian locally. Repository development, coding agents, +builds, tests, containers and development databases execute on Studio. +The `chatgpt` Homebrew cask supplies the current Codex desktop app bundle. + +This profile is separate from the current M3 Max's VMware-owning `mac-thin` +profile. It installs no VMware, Vagrant, editor toolchains, project runtimes, +Docker or database server. Studio rollout does not wait for the Air to arrive. + +## Setup After Arrival + +Install Homebrew and its command-line prerequisites, authenticate locally and +clone reviewed `master` at `~/Developer/dotfiles-hd`. Use the personal Apple +Account. Review the client package/configuration plan: + +```bash +hosts/mac-air/bootstrap.sh --dry-run +hosts/mac-air/bootstrap.sh --check +``` + +After the Air arrives and the preview is approved: + +```bash +DOTFILES_MAC_AIR_ARRIVED=1 hosts/mac-air/bootstrap.sh --apply +hosts/mac-air/doctor.sh +``` + +The arrival gate also protects direct Chezmoi apply. The profile owns only the +shell, prompt, lightweight Ghostty configuration and Brewfile link listed in +`chezmoi/profiles/mac-air.paths`. Use the timestamped Chezmoi rollback printed +by apply. Packages, credentials and application data remain machine-owned. + +## Remote Acceptance + +- Enroll Air in Tailscale with fresh machine-local authentication. +- Configure Studio as the remote Codex/terminal execution destination. Use + the verified Studio hostname and username; do not reuse the Ubuntu target. +- Test SSH and Screen Sharing over private Tailscale access from away from home. + Verify remote command execution with `hostname` before running project work. +- Verify Studio restart/reconnection and recovery access. Air's doctor checks + local clients only; it does not connect to or start a remote machine. +- Configure Obsidian synchronization for local reading/editing while keeping + Studio the canonical coding-prompt source. Add Air to applicable sync/readiness + workflows only after these checks pass. diff --git a/hosts/mac-air/bootstrap.sh b/hosts/mac-air/bootstrap.sh new file mode 100755 index 00000000..21ca219a --- /dev/null +++ b/hosts/mac-air/bootstrap.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd -P)" +DOTFILES_DIR="${DOTFILES_DIR:-$REPO_DIR}" +BREWFILE="$DOTFILES_DIR/hosts/mac-air/Brewfile" +CHEZMOI_BOOTSTRAP="${DOTFILES_CHEZMOI_BOOTSTRAP:-$DOTFILES_DIR/chezmoi/bootstrap.sh}" +CHEZMOI_PREVIEW="${DOTFILES_CHEZMOI_PREVIEW:-$DOTFILES_DIR/chezmoi/preview.sh}" +HOST_DOCTOR="${DOTFILES_AIR_DOCTOR:-$SCRIPT_DIR/doctor.sh}" +MODE="${1:---dry-run}" + +die() { printf 'error: %s\n' "$*" >&2; exit 1; } +[[ $# -le 1 ]] || die "usage: bootstrap.sh [--dry-run|--check|--apply]" +case "$MODE" in + --dry-run|--check|--apply) ;; + *) die "usage: bootstrap.sh [--dry-run|--check|--apply]" ;; +esac + +[[ "$(uname -s)" == Darwin ]] || die "mac-air requires macOS" +command -v brew >/dev/null 2>&1 || die "install Homebrew first" +git -C "$DOTFILES_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1 \ + || die "not a Git checkout: $DOTFILES_DIR" +[[ -f "$BREWFILE" ]] || die "missing Brewfile: $BREWFILE" + +if [[ "$MODE" == --dry-run ]]; then + printf 'profile: mac-air\nwould install remote clients from: %s\n' "$BREWFILE" + printf 'would apply client configuration; execution stays on Studio\n' + if [[ -x "${CHEZMOI_BIN:-$HOME/.local/bin/chezmoi}" ]]; then + DOTFILES_CHEZMOI_CONFIG_ONLY_PREVIEW=1 bash "$CHEZMOI_PREVIEW" mac-air + fi + exit 0 +fi + +if [[ "$MODE" == --check ]]; then + exec "$HOST_DOCTOR" +fi + +[[ "${DOTFILES_MAC_AIR_ARRIVED:-0}" == 1 ]] \ + || die "mac-air apply requires DOTFILES_MAC_AIR_ARRIVED=1 after the hardware arrives" + +# Reviewed canonical-checkout and layout validation precede package mutations. +DOTFILES_CHEZMOI_CONFIG_ONLY_PREVIEW=1 DOTFILES_CHEZMOI_REQUIRE_REVIEWED=1 \ + "$CHEZMOI_BOOTSTRAP" mac-air --preview >/dev/null +HOMEBREW_NO_AUTO_UPDATE=1 brew bundle install --no-upgrade --file "$BREWFILE" +DOTFILES_CHEZMOI_APPROVED=1 "$CHEZMOI_BOOTSTRAP" mac-air --apply +"$HOST_DOCTOR" +printf 'Air client setup complete. Verify Studio access manually.\n' diff --git a/hosts/mac-air/doctor.sh b/hosts/mac-air/doctor.sh new file mode 100755 index 00000000..485c42bf --- /dev/null +++ b/hosts/mac-air/doctor.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd -P)" +DOTFILES_DIR="${DOTFILES_DIR:-$REPO_DIR}" +CHEZMOI_DOCTOR="${DOTFILES_CHEZMOI_DOCTOR:-$DOTFILES_DIR/chezmoi/doctor.sh}" +APPLICATIONS_DIR="${DOTFILES_APPLICATIONS_DIR:-/Applications}" +FAILURES=0 +pass() { printf 'PASS %s\n' "$*"; } +fail() { printf 'FAIL %s\n' "$*" >&2; FAILURES=$((FAILURES + 1)); } + +[[ "$(uname -s)" == Darwin ]] && pass macOS || fail 'macOS required' +if HOMEBREW_NO_AUTO_UPDATE=1 brew bundle check --no-upgrade \ + --file "$DOTFILES_DIR/hosts/mac-air/Brewfile"; then + pass 'remote client packages' +else + fail 'remote client packages missing' +fi +if "$CHEZMOI_DOCTOR" mac-air; then + pass 'Air client configuration' +else + fail 'Air client configuration' +fi +for app in ChatGPT Ghostty Obsidian Tailscale; do + [[ -d "$APPLICATIONS_DIR/$app.app" ]] \ + && pass "$app installed" || fail "$app missing" +done +printf 'MANUAL Verify Studio SSH, Screen Sharing and remote execution over Tailscale.\n' +((FAILURES == 0)) diff --git a/hosts/mac-air/ghostty.conf b/hosts/mac-air/ghostty.conf new file mode 100644 index 00000000..ca32ecd1 --- /dev/null +++ b/hosts/mac-air/ghostty.conf @@ -0,0 +1,4 @@ +font-family = Maple Mono NF +font-size = 14 +theme = Nord +copy-on-select = clipboard diff --git a/hosts/mac-air/tests/client-profile-test.sh b/hosts/mac-air/tests/client-profile-test.sh new file mode 100755 index 00000000..f5922c4f --- /dev/null +++ b/hosts/mac-air/tests/client-profile-test.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +set -euo pipefail + +TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +REPO_DIR="$(cd "$TEST_DIR/../../.." && pwd -P)" +TEST_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/dotfiles-air-test.XXXXXX")" +trap 'rm -rf "$TEST_ROOT"' EXIT +FAKE_BIN="$TEST_ROOT/bin" +TEST_HOME="$TEST_ROOT/home" +LOG="$TEST_ROOT/commands.log" +mkdir -p "$FAKE_BIN" "$TEST_HOME" +: > "$LOG" +fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; } + +cat > "$FAKE_BIN/uname" <<'FAKE' +#!/bin/sh +printf 'Darwin\n' +FAKE +cat > "$FAKE_BIN/brew" <<'FAKE' +#!/bin/sh +printf 'brew %s\n' "$*" >> "$COMMAND_LOG" +exit "${BREW_STATUS:-0}" +FAKE +cat > "$FAKE_BIN/chezmoi-bootstrap" <<'FAKE' +#!/bin/sh +printf 'chezmoi %s\n' "$*" >> "$COMMAND_LOG" +if [ "${FAIL_PREVIEW:-0}" = 1 ] && [ "$2" = --preview ]; then exit 1; fi +FAKE +cat > "$FAKE_BIN/chezmoi-doctor" <<'FAKE' +#!/bin/sh +printf 'chezmoi-doctor %s\n' "$*" >> "$COMMAND_LOG" +FAKE +for tool in vagrant mise docker colima ssh launchctl softwareupdate uv npm pnpm; do + cat > "$FAKE_BIN/$tool" <<'FAKE' +#!/bin/sh +printf 'FORBIDDEN %s %s\n' "${0##*/}" "$*" >> "$COMMAND_LOG" +exit 99 +FAKE +done +chmod +x "$FAKE_BIN"/* + +bootstrap() { + HOME="$TEST_HOME" PATH="$FAKE_BIN:$PATH" COMMAND_LOG="$LOG" \ + DOTFILES_CHEZMOI_BOOTSTRAP="$FAKE_BIN/chezmoi-bootstrap" \ + DOTFILES_AIR_DOCTOR=/usr/bin/true CHEZMOI_BIN="$TEST_ROOT/missing-chezmoi" \ + bash "$REPO_DIR/hosts/mac-air/bootstrap.sh" "$@" +} +bootstrap --dry-run >/dev/null +[[ ! -s "$LOG" ]] || fail 'dry-run mutated or called package tools' +if bootstrap --apply >/dev/null 2>&1; then fail 'arrival gate was bypassed'; fi +[[ ! -s "$LOG" ]] || fail 'arrival gate allowed mutations' +if DOTFILES_MAC_AIR_ARRIVED=1 FAIL_PREVIEW=1 bootstrap --apply >/dev/null 2>&1; then + fail 'failed preview did not block apply' +fi +! grep -q '^brew ' "$LOG" || fail 'packages installed before reviewed preview' +: > "$LOG" +DOTFILES_MAC_AIR_ARRIVED=1 bootstrap --apply >/dev/null +grep -Fq "brew bundle install --no-upgrade --file $REPO_DIR/hosts/mac-air/Brewfile" \ + "$LOG" || fail 'client Brewfile was not installed' +grep -Fq 'chezmoi mac-air --apply' "$LOG" || fail 'Air profile was not applied' +[[ "$(head -n 1 "$LOG")" == 'chezmoi mac-air --preview' ]] || fail 'preview order' + +for app in ChatGPT Ghostty Obsidian Tailscale; do mkdir -p "$TEST_ROOT/apps/$app.app"; done +doctor() { + HOME="$TEST_HOME" PATH="$FAKE_BIN:$PATH" COMMAND_LOG="$LOG" \ + DOTFILES_CHEZMOI_DOCTOR="$FAKE_BIN/chezmoi-doctor" \ + DOTFILES_APPLICATIONS_DIR="$TEST_ROOT/apps" \ + bash "$REPO_DIR/hosts/mac-air/doctor.sh" +} +doctor >/dev/null +if BREW_STATUS=1 doctor >/dev/null 2>&1; then fail 'missing packages passed doctor'; fi +rmdir "$TEST_ROOT/apps/ChatGPT.app" +if doctor >/dev/null 2>&1; then fail 'missing client passed doctor'; fi +ln -s "$REPO_DIR/hosts/mac-air/.zshrc" "$TEST_HOME/.zshrc" +HOME="$TEST_HOME" HOMEBREW_PREFIX="$TEST_ROOT" PATH="$FAKE_BIN:/usr/bin:/bin" \ + TERM=xterm-256color COMMAND_LOG="$LOG" /bin/zsh -lic \ + '[[ "$DOTFILES_MAC_PROFILE" == mac-air ]] && (( ! $+functions[uvm-up] ))' +! grep -Fq FORBIDDEN "$LOG" || fail 'Air invoked a development or VM tool' +! grep -Eq '^(brew|cask) "(vagrant|vagrant-vmware-utility|vmware-fusion|docker|colima|mise|neovim|postgresql)' \ + "$REPO_DIR/hosts/mac-air/Brewfile" || fail 'Air installs a local development environment' +printf 'MacBook Air client profile tests passed.\n' diff --git a/hosts/mac-mini/Brewfile b/hosts/mac-mini/Brewfile index b572bf5b..ff075f4b 100644 --- a/hosts/mac-mini/Brewfile +++ b/hosts/mac-mini/Brewfile @@ -1,5 +1,10 @@ tap "steipete/tap" +# Secondary development only; bootstrap never starts the container runtime. +brew "colima" +brew "docker" +brew "docker-buildx" +brew "docker-compose" brew "coreutils" brew "ffmpeg" brew "git-filter-repo" diff --git a/hosts/mac-mini/README.md b/hosts/mac-mini/README.md index 91647d70..e95caf75 100644 --- a/hosts/mac-mini/README.md +++ b/hosts/mac-mini/README.md @@ -26,3 +26,26 @@ The bootstrap does not restart, reload, or migrate Cortana, Hermes, Homebrew services, LaunchAgents, or other production processes. Runtime changes require a separate maintenance window. Use the exact timestamped Chezmoi backup printed by apply for configuration rollback. + +## Secondary Development + +After verified Studio cutover, Studio is the default development host. Until +then, current topology rules apply. Select the mini explicitly when its +secondary development environment is needed. The shared profile already +provides native mise runtimes and editor/agent tools; its overlay also provides +PostgreSQL 17, pgvector and Colima/Docker/Compose/Buildx. Review missing packages +through the normal guarded apply; installation never starts services. + +Keep `/Users/h/Developer/cortana-services` clean on `main` for production. +Create development clones or worktrees under `~/Developer/worktrees/`, with +separate environment files, test databases, data directories and ports. Follow +each project's runtime runbook for the actual port allocation; never develop +against production credentials or mutable production data. Preserve the +production Homebrew Node 22 prefix independently of mise development Node. + +Use `cortana-hd` for explicitly selected mini agent GitHub work and verify the +actor before writes. Deploy reviewed work through the existing approval and +runtime-verification process. Development does not authorize service reloads. +For Docker plugin discovery and manual runtime activation, follow the +[Studio development setup](../mac-studio/README.md#native-development), using +a separately named Colima development profile on the mini. diff --git a/hosts/mac-studio/.zshrc b/hosts/mac-studio/.zshrc index f5f31957..5fc5d51f 100644 --- a/hosts/mac-studio/.zshrc +++ b/hosts/mac-studio/.zshrc @@ -1,4 +1,4 @@ -# Mac Studio primary control plane and compute host. +# Mac Studio primary native development workstation. export DOTFILES_MAC_PROFILE="mac-studio" @@ -18,7 +18,7 @@ _zsh_add_completion_dirs \ /usr/local/share/zsh/site-functions _zsh_init_completions 43200 typeset -gaU path -path=("${XDG_BIN_HOME:-$HOME/.local/bin}" $path) +path=("${XDG_BIN_HOME:-$HOME/.local/bin}" "${HOMEBREW_PREFIX:-/opt/homebrew}/opt/postgresql@17/bin" $path) export PATH _activate_mise diff --git a/hosts/mac-studio/Brewfile b/hosts/mac-studio/Brewfile index a5e04ed1..4872904d 100644 --- a/hosts/mac-studio/Brewfile +++ b/hosts/mac-studio/Brewfile @@ -1,8 +1,16 @@ # Primary personal-Mac applications. Shared development tools live in # ../shared/macos/Brewfile. +brew "colima" +brew "docker" +brew "docker-buildx" +brew "docker-compose" +brew "postgresql@17" +brew "pgvector" + cask "1password" cask "brave-browser" +cask "chatgpt" cask "codex" cask "daisydisk" cask "deskflow" @@ -20,6 +28,7 @@ cask "tableplus" cask "tailscale-app" cask "telegram" cask "vagrant" -cask "vagrant-vmware-utility" +cask "visual-studio-code" +vscode "openai.chatgpt" cask "vlc" cask "zoom" diff --git a/hosts/mac-studio/README.md b/hosts/mac-studio/README.md index ab1f87eb..a327d5f6 100644 --- a/hosts/mac-studio/README.md +++ b/hosts/mac-studio/README.md @@ -1,46 +1,40 @@ # Mac Studio -The Mac Studio is the planned primary machine: full macOS development, local -AI, VM hosting, and control of the Ubuntu VM and Mac mini. +The Studio becomes the default native macOS development workstation for every +repository: editors, coding agents, worktrees, builds, tests, Docker workloads +and development databases. It also owns the main Obsidian vault and canonical +coding-prompt source after cutover. -This profile is staged before the hardware arrives. It does not change the -current thin-Mac, Ubuntu, or Mac-mini routes. Do not apply it until the Studio -is present and Hamel approves the reviewed preview and check. +This is pre-arrival staging for [issue #117](https://github.com/hd719/dotfiles-hd/issues/117). +Current M3 Max, Ubuntu and mini routing stays active until verified cutover. +Keep the M3 Max MacBook Pro until the Studio and migrated data are verified, +then trade it in. Studio completion does not wait for the future Air. -## Topology +## Target Roles -```text -Mac Studio (primary controller and compute) -├── Ubuntu VM (lives inside the Studio; primary development workstation) -├── Ollama (native macOS app; models and cache stay on the Studio) -└── SSH / Screen Sharing -> Mac mini (Hermes runtime) +| Host | Role after cutover | +| ---------------- | ----------------------------------------------------------------------------- | +| Studio | Default native development, main personal workstation and local AI | +| Ubuntu on Studio | Powered-off preserved VM, backed up on NAS; manual use only | +| Mac mini | Hermes/Cortana Services production; explicitly selected secondary development | +| Future Air | Lightweight local clients; SSH and Screen Sharing into Studio; no VMware | -MacBook Air (mobile controller) -├── SSH / Screen Sharing -> Mac Studio -├── SSH -> Ubuntu VM -└── SSH -> Mac mini -``` - -The Studio and Air use Hamel's personal Apple Account. The Mac mini keeps its -existing dedicated Apple Account. Apple Account setup, Remote Login, Screen -Sharing, Tailscale enrollment, SSH keys, and Ollama models are machine-owned -and never copied by dotfiles. - -The future Air reuses the existing `mac-thin` profile. No new Air-specific -profile is needed. +Studio and Air use the personal Apple Account. Mini retains its dedicated +Apple Account. Remote Login, Screen Sharing, Tailscale enrollment, hostnames, +SSH keys, authentication, databases and Ollama models are machine-owned. ## On Arrival -Manually install and finish first-run setup for Xcode Command Line Tools, -Homebrew, and VMware Fusion. Then clone this repository to -`~/Developer/dotfiles-hd` and run: +Install Xcode Command Line Tools, Homebrew and VMware Fusion manually. Finish +Fusion's first-run setup without starting or creating Ubuntu. Clone this repo +at `~/Developer/dotfiles-hd`, then review: ```bash hosts/shared/macos/bootstrap.sh --profile mac-studio --dry-run hosts/shared/macos/bootstrap.sh --profile mac-studio --check ``` -Review both results before approving: +After hardware arrival and approval of the reviewed checks: ```bash DOTFILES_MAC_STUDIO_ARRIVED=1 \ @@ -48,57 +42,82 @@ DOTFILES_MAC_STUDIO_ARRIVED=1 \ hosts/shared/macos/doctor.sh --profile mac-studio ``` -The arrival flag is a hard safety gate. Never set it on another Mac. - -The SSH config is machine-owned. After the Studio exists, manually add this -line before any older `Host ubuntu-vm*` blocks in `~/.ssh/config`: - -```sshconfig -Include ~/Developer/dotfiles-hd/hosts/mac-studio/ssh/ubuntu-vagrant.conf -``` - -The apply installs the Ollama app, Vagrant, the VMware utility, Rosetta 2 when -needed, and `vagrant-vmware-desktop` 3.0.5. It does not launch Ollama, download -models, start services, create the VM, or alter remote-access settings. - -## Ubuntu VM - -After the Studio profile is green, move by rebuilding the Ubuntu VM from the -tracked Vagrant definition. Do not manually move or delete the current Fusion -VM. The cutover is a separate approval gate. - -Cut over in this order: - -1. Back up and verify uncommitted repositories, databases, and other VM-local - data. Record the old VM's SSH host-key and Git-key fingerprints. - -1. Halt the old VM and keep it recoverable. Never run both `ubuntu-dev` - machines on the LAN or tailnet together. - -1. Rebuild on the Studio without a Tailscale auth key. Verify localhost SSH, - provisioning, repositories, tools, and restored data first. - -1. Retire the old Tailscale node, enroll the new VM as `ubuntu-dev`, then - replace known-host entries only after verifying its new fingerprint. - -1. Run the full Ubuntu doctor and the Studio cutover check: - - ```bash - DOTFILES_MAC_STUDIO_CUTOVER=1 \ - hosts/shared/macos/doctor.sh --profile mac-studio - ``` - -1. Destroy the old VM and remove its registered Git keys only after Hamel - approves the verified replacement. - -The Studio shell exposes `uvm-up`, `uvm-stop`, `uvm-suspend`, `uvm-resume`, -`uvm-status`, `uvm-ip`, and interactive `uvm-destroy`. The VM remains the -primary home for project repositories, Docker, databases, and PR work. - -## Ownership - -- Dotfiles own the reviewed profile, shell, packages, and VM commands. -- Vagrant owns the Ubuntu VM definition and lifecycle. -- VMware Fusion owns the local VM runtime. -- Ollama models and `~/.ollama` stay machine-owned. -- The Mac mini remains the Hermes production runtime. +The arrival flag is a hard gate; set it only on Studio. Bootstrap installs the +shared native development tools, the Studio Brewfile and configuration. It +never starts Ubuntu, Colima, PostgreSQL or Ollama. Normal checks audit installed +packages/configuration without requiring any guest, Docker daemon, database +server, VMware utility service or Ubuntu SSH route to be online. + +## Native Development + +Homebrew and the shared mise configuration provide the native toolchain. +Studio adds Colima, Docker, Compose, Buildx, PostgreSQL 17, pgvector and VS Code. +Colima's small VM supplies Docker only; repositories and toolchains stay native +on Studio. [Colima setup](https://github.com/abiosoft/colima#installation) +requires a separate, deliberate `colima start` after installation. + +Merge `/opt/homebrew/lib/docker/cli-plugins` into the machine-owned +`cliPluginsExtraDirs` array in `~/.docker/config.json` to enable `docker compose` +and `docker buildx`; preserve existing authentication and other settings. +See the [Homebrew Compose caveat](https://formulae.brew.sh/formula/docker-compose). +Verify `docker compose version`, `docker buildx version` and, after starting +Colima, `docker info`. Dotfiles does not rewrite Docker configuration or data. + +The Studio shell exposes PostgreSQL 17 client tools. Homebrew creates its +initial empty cluster during package installation; project database setup, +restores and server startup remain deliberate project steps. Verify local +builds/tests and database access for the repositories being moved, including +Cortana Services. Never use the mini's production database for development. + +Use `arbiter-hd` for Studio agent GitHub work, verify the actor before writes, +and preserve current-head `hd719` approval requirements before merging agent +PRs. Deploy to the mini through the existing reviewed deployment process. +For mini development isolation, follow its [runbook](../mac-mini/README.md). + +## Preserve Ubuntu Before Trade-In + +1. Inventory and back up repositories, uncommitted work, unpushed branches, + development databases and other required data on the M3 Max and guest. +1. Halt Ubuntu cleanly through its current Vagrant workflow. Preserve the cold + VMware bundle and associated Vagrant metadata on Studio, with a NAS backup. + Verify archive integrity and record paths and restoration steps before + surrendering the M3 Max. Do not delete the preserved VM or run two copies. +1. Move development work and required data into native Studio workspaces. + Authenticate Studio tools with machine-local credentials; the archived + guest's credentials are not Studio credentials. Verify against the inventory. +1. Leave Ubuntu off. Its restore boot, provider setup, identity changes or + rebuild happen only when Hamel explicitly requests them later. + +The normal profile installs Vagrant but defers its VMware utility, Rosetta +requirement and pinned `vagrant-vmware-desktop` 3.0.5 provider until Ubuntu is +requested. Restore and verify both VM and `.vagrant` metadata before using the +helpers. Adjust host-specific paths and provide the guest's expected login +public key through a reviewed restoration procedure; never overwrite keys. + +The shell exposes `uvm-status`, `uvm-up`, `uvm-stop`, `uvm-suspend`, `uvm-resume` +and `uvm-ip`. Loading the shell does not invoke Vagrant. Start/resume refuse a +missing restored VM; `uvm-up` disables automatic provisioning. There is no +Studio destroy shortcut. These helpers are for deliberate manual use only. + +If Ubuntu is later activated, manually configure its SSH routes using +`hosts/mac-studio/ssh/ubuntu-vagrant.conf` after verifying host fingerprints. +The old `DOTFILES_MAC_STUDIO_CUTOVER` online-VM check is no longer required. + +## Cutover and Remote Acceptance + +Before trading in the M3 Max: + +- Verify native repository builds/tests, development data and both VM copies. +- Move the main vault/canonical prompt source to Studio through a verified + cutover. Update canonical `machine-topology`, `dotfiles-sync`, `personal-ready` + and `sync-coding-prompts` at that point, then use their normal sync workflow. +- Make Studio the default development destination; mini development requires + explicit selection. Exclude dormant Ubuntu and the absent Air from mandatory + online checks. Record accepted routes and rollback procedures without secrets. +- Test Studio SSH and Screen Sharing from the M3 Max, including away-from-home + Tailscale access, sleep/reconnection and restart recovery. +- Keep Hermes/Cortana Services production on mini and verify its readiness. + +Configure the future Air independently when it arrives, using the dedicated +[`mac-air` profile](../mac-air/README.md). Ollama model selection and activation +remain separate from base-host acceptance; its cache stays machine-owned. diff --git a/hosts/mac-studio/tests/vm-lifecycle-test.sh b/hosts/mac-studio/tests/vm-lifecycle-test.sh index 8efeda04..e222b8b9 100644 --- a/hosts/mac-studio/tests/vm-lifecycle-test.sh +++ b/hosts/mac-studio/tests/vm-lifecycle-test.sh @@ -5,72 +5,61 @@ TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" REPO_DIR="$(cd "$TEST_DIR/../../.." && pwd -P)" TEST_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/dotfiles-studio-vm-test.XXXXXX")" trap 'rm -rf "$TEST_ROOT"' EXIT - FAKE_BIN="$TEST_ROOT/bin" TEST_HOME="$TEST_ROOT/home" +PROJECT="$TEST_ROOT/ubuntu" VAGRANT_LOG="$TEST_ROOT/vagrant.log" -mkdir -p "$FAKE_BIN" "$TEST_HOME" - -fail() { - printf 'FAIL: %s\n' "$*" >&2 - exit 1 -} - -assert_contains() { - grep -Fq -- "$2" "$1" || fail "expected '$2' in $1" -} +mkdir -p "$FAKE_BIN" "$TEST_HOME" "$PROJECT" +PROJECT="$(cd "$PROJECT" && pwd -P)" +: > "$PROJECT/Vagrantfile" +: > "$VAGRANT_LOG" +fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; } -assert_not_contains() { - ! grep -Fq -- "$2" "$1" || fail "did not expect '$2' in $1" -} - -cat > "$FAKE_BIN/vagrant" <<'FAKE_VAGRANT' +cat > "$FAKE_BIN/vagrant" <<'FAKE' #!/usr/bin/env bash -printf 'cwd=%s vagrant_cwd=%s gui=%s provider=%s clone=%s args=%s\n' \ - "$PWD" \ - "${VAGRANT_CWD:-unset}" \ - "${UBUNTU_VM_GUI:-unset}" \ - "${VAGRANT_DEFAULT_PROVIDER:-unset}" \ - "${VAGRANT_VMWARE_CLONE_DIRECTORY:-unset}" \ - "$*" >> "${VAGRANT_TEST_LOG:?}" -if [[ "${1:-}" == ssh ]]; then - printf '192.0.2.10\n' +printf 'cwd=%s provider=%s gui=%s args=%s\n' \ + "$PWD" "$VAGRANT_DEFAULT_PROVIDER" "${UBUNTU_VM_GUI:-unset}" "$*" \ + >> "${VAGRANT_TEST_LOG:?}" +if [[ "$*" == 'status --machine-readable' ]]; then + [[ "${VM_STATE:-poweroff}" != error ]] || exit 1 + printf '1,default,state,%s\n' "${VM_STATE:-poweroff}" fi -FAKE_VAGRANT +FAKE chmod +x "$FAKE_BIN/vagrant" -output="$({ - HOME="$TEST_HOME" \ - PATH="$FAKE_BIN:/usr/bin:/bin" \ - DOTFILES_UBUNTU_VAGRANT_DIR="$REPO_DIR/hosts/ubuntu-dev" \ - VAGRANT_TEST_LOG="$VAGRANT_LOG" \ - /bin/zsh -dfc ' - source "$1" - uvm-up - uvm-stop - uvm-suspend - uvm-resume - uvm-status - uvm-ip - uvm-destroy - ' zsh "$REPO_DIR/hosts/mac-studio/vm.zsh" -} 2>&1)" || fail "$output" +run_shell() { + HOME="$TEST_HOME" PATH="$FAKE_BIN:/usr/bin:/bin" \ + DOTFILES_UBUNTU_VAGRANT_DIR="$PROJECT" VAGRANT_TEST_LOG="$VAGRANT_LOG" \ + /bin/zsh -dfc 'source "$1"; eval "$2"' zsh \ + "$REPO_DIR/hosts/mac-studio/vm.zsh" "$1" +} +run_shell ':' +[[ ! -s "$VAGRANT_LOG" ]] || fail 'loading helpers invoked Vagrant' -assert_contains "$VAGRANT_LOG" \ - "cwd=$REPO_DIR/hosts/ubuntu-dev vagrant_cwd=$REPO_DIR/hosts/ubuntu-dev gui=1 provider=vmware_desktop" -for args in up halt suspend resume status 'ssh -c hostname -I' destroy; do - assert_contains "$VAGRANT_LOG" "args=$args" +for action in uvm-up uvm-resume; do + if run_shell "$action" >/dev/null; then fail "$action allowed a missing VM"; fi done -assert_contains "$VAGRANT_LOG" 'sudo -n -u hamel' -assert_not_contains "$VAGRANT_LOG" 'destroy -f' -[[ "$output" == *"Remove this VM's three registered Git public keys"* ]] \ - || fail 'destroy should print the Git-key removal reminder' +[[ ! -s "$VAGRANT_LOG" ]] || fail 'missing metadata reached Vagrant' -assert_contains "$REPO_DIR/hosts/mac-studio/.zshrc" \ - 'source "$HOME/Developer/dotfiles-hd/hosts/mac-studio/vm.zsh"' -for expected in 'Host ubuntu-vm' 'HostName 127.0.0.1' 'ForwardAgent no' \ - 'StrictHostKeyChecking yes'; do - assert_contains "$REPO_DIR/hosts/mac-studio/ssh/ubuntu-vagrant.conf" "$expected" +mkdir -p "$PROJECT/.vagrant/machines/default/vmware_desktop" +printf 'preserved-vm\n' > "$PROJECT/.vagrant/machines/default/vmware_desktop/id" +for state in not_created unknown error; do + for action in uvm-up uvm-resume; do + if VM_STATE="$state" run_shell "$action" >/dev/null; then + fail "$action allowed unavailable state $state" + fi + done done +! grep -Eq 'args=(up|resume)' "$VAGRANT_LOG" || fail 'unavailable VM was started' -printf 'Mac Studio VM lifecycle tests passed.\n' +: > "$VAGRANT_LOG" +run_shell 'uvm-up; uvm-stop; uvm-suspend; uvm-resume; uvm-status; uvm-ip' +for args in 'up --no-provision' halt suspend resume status 'ssh -c hostname -I'; do + grep -Fq "args=$args" "$VAGRANT_LOG" || fail "missing command: $args" +done +grep -Fq "cwd=$PROJECT provider=vmware_desktop gui=1 args=up --no-provision" \ + "$VAGRANT_LOG" || fail 'start did not preserve provider, project and GUI settings' +! grep -Fq 'args=provision' "$VAGRANT_LOG" || fail 'automatic provisioning' +! grep -Fq destroy "$VAGRANT_LOG" || fail 'unexpected deletion' +run_shell '(( ! $+functions[uvm-destroy] ))' || fail 'destroy shortcut remains' +printf 'Mac Studio dormant VM lifecycle tests passed.\n' diff --git a/hosts/mac-studio/vm.zsh b/hosts/mac-studio/vm.zsh index f30b5dd4..436f792e 100644 --- a/hosts/mac-studio/vm.zsh +++ b/hosts/mac-studio/vm.zsh @@ -1,4 +1,4 @@ -# Mac Studio Vagrant shortcuts. Development runs inside the Ubuntu guest. +# Mac Studio Vagrant shortcuts. Ubuntu is preserved for explicitly requested manual use. _ubuntu_vagrant() { emulate -L zsh @@ -20,8 +20,27 @@ _ubuntu_vagrant() { ) } +_require_preserved_ubuntu() { + local project_dir="${DOTFILES_UBUNTU_VAGRANT_DIR:-$HOME/Developer/dotfiles-hd/hosts/ubuntu-dev}" + if [[ ! -s "$project_dir/.vagrant/machines/default/vmware_desktop/id" ]]; then + echo "Restore and verify the preserved VM and Vagrant metadata before starting Ubuntu." + echo "This helper will not create a replacement VM." + return 1 + fi + local vm_status + vm_status="$(_ubuntu_vagrant status --machine-readable)" || return + if ! print -r -- "$vm_status" | awk -F, ' + $2 == "default" && $3 == "state" && $4 ~ /^(poweroff|running|suspended|saved)$/ { found = 1 } + END { exit !found } + '; then + echo "Preserved Ubuntu is unavailable; refusing to create a replacement VM." + return 1 + fi +} + uvm-up() { - UBUNTU_VM_GUI=1 _ubuntu_vagrant up + _require_preserved_ubuntu || return + UBUNTU_VM_GUI=1 _ubuntu_vagrant up --no-provision } uvm-stop() { @@ -33,6 +52,7 @@ uvm-suspend() { } uvm-resume() { + _require_preserved_ubuntu || return _ubuntu_vagrant resume } @@ -43,16 +63,3 @@ uvm-status() { uvm-ip() { _ubuntu_vagrant ssh -c 'hostname -I' } - -uvm-destroy() { - emulate -L zsh - - # Best effort only: the VM may already be stopped. Never start it to inspect keys. - _ubuntu_vagrant ssh -c \ - 'sudo -n -u hamel sh -c '\''for key in /home/hamel/.ssh/id_ed25519_hd719 /home/hamel/.ssh/id_ed25519_arbiter_hd /home/hamel/.ssh/id_ed25519_forgejo_truenas; do test -f "$key.pub" && ssh-keygen -lf "$key.pub"; done'\''' \ - 2>/dev/null || true - - echo "Remove this VM's three registered Git public keys after replacement." - echo "Vagrant will ask for confirmation; this command never forces destroy." - _ubuntu_vagrant destroy -} diff --git a/hosts/shared/macos/README.md b/hosts/shared/macos/README.md index b3354d1f..bbff1ac7 100644 --- a/hosts/shared/macos/README.md +++ b/hosts/shared/macos/README.md @@ -2,11 +2,11 @@ Shared provisioning for three full local macOS profiles: -| Profile | Purpose | -| --- | --- | -| `mac-pro` | Standalone full-development MacBook; all development tools local, no VM | -| `mac-studio` | Staged primary controller with Ubuntu VM hosting and Ollama | -| `mac-mini` | Production runtime Mac with extra apply gates | +| Profile | Purpose | +| ------------ | -------------------------------------------------------------------------------- | +| `mac-pro` | Standalone full-development MacBook; all development tools local, no VM | +| `mac-studio` | Staged native development, dormant Ubuntu and Ollama | +| `mac-mini` | Production and explicitly selected secondary development, with extra apply gates | Do not run this bootstrap on the thin Mac. Use [`../../mac-thin/README.md`](../../mac-thin/README.md) there. @@ -53,10 +53,14 @@ ancestor manifest. Secrets, auth state, Herdr sessions, Hunk state, tmux plugins, services, Docker state, databases, and application data remain machine-owned. -For `mac-studio`, bootstrap also installs Rosetta 2 when needed and pins the -Vagrant VMware provider. It installs the Ollama app but never launches it or -downloads models. VMware Fusion first-run setup and remote access remain -manual. +Studio and mini overlays include Colima/Docker development tools; Studio also +adds PostgreSQL 17, which mini already retains. Package installation does not +start these services. Studio installs Ollama without activation or model +pulls. VMware Fusion setup remains manual; Ubuntu provider installation is +deferred until the preserved guest is explicitly requested. See the +[Studio runbook](../../mac-studio/README.md) for manual Docker plugin setup, +database ownership and remote acceptance. Air uses its separate client +bootstrap, never the shared full-development bootstrap. ## Rollback diff --git a/hosts/shared/macos/bootstrap.sh b/hosts/shared/macos/bootstrap.sh index 3cf7fe37..98aa49cf 100755 --- a/hosts/shared/macos/bootstrap.sh +++ b/hosts/shared/macos/bootstrap.sh @@ -8,10 +8,6 @@ STAMP="${DOTFILES_STAMP:-$(date +%Y%m%d-%H%M%S)}" CHEZMOI_BOOTSTRAP="${DOTFILES_CHEZMOI_BOOTSTRAP:-$DOTFILES_DIR/chezmoi/bootstrap.sh}" CHEZMOI_PREVIEW="${DOTFILES_CHEZMOI_PREVIEW:-$DOTFILES_DIR/chezmoi/preview.sh}" HOST_DOCTOR="${DOTFILES_MAC_DOCTOR:-$SCRIPT_DIR/doctor.sh}" -PKGUTIL="${DOTFILES_PKGUTIL:-/usr/sbin/pkgutil}" -SOFTWAREUPDATE="${DOTFILES_SOFTWAREUPDATE:-/usr/sbin/softwareupdate}" -SUDO="${DOTFILES_SUDO:-/usr/bin/sudo}" -VAGRANT_VMWARE_PLUGIN_VERSION="3.0.5" PROFILE="" MODE="dry-run" @@ -71,17 +67,6 @@ done PROFILE="$(canonical_profile "$PROFILE")" || exit 2 load_profile "$PROFILE" "$DOTFILES_DIR" "$HOME" -rosetta_installed() { - "$PKGUTIL" --pkg-info com.apple.pkg.RosettaUpdateAuto >/dev/null 2>&1 -} - -vagrant_vmware_plugin_current() { - command -v vagrant >/dev/null 2>&1 || return 1 - vagrant plugin list 2>/dev/null \ - | /usr/bin/grep -Eq \ - "^vagrant-vmware-desktop \($VAGRANT_VMWARE_PLUGIN_VERSION([,)])" -} - [[ "$(uname -s)" == "Darwin" ]] || die "personal-Mac bootstrap requires macOS" [[ "$(uname -m)" == "arm64" ]] || die "personal-Mac bootstrap currently supports Apple Silicon only" xcode-select -p >/dev/null 2>&1 || die "install Xcode Command Line Tools first: xcode-select --install" @@ -131,7 +116,7 @@ if [[ "$MODE" == "dry-run" ]]; then say "would restore locked Neovim plugins and required Tree-sitter parsers without changing lazy-lock.json" say "would run the verification doctor" if [[ "$PROFILE" == mac-studio ]]; then - say "would install Rosetta 2 and vagrant-vmware-desktop 3.0.5 when missing" + say "Ubuntu stays dormant; provider setup is deferred until manual use" say "VMware Fusion remains a manual install; Ollama models remain machine-owned" fi @@ -151,12 +136,6 @@ if [[ "$MODE" == "check" ]]; then say "dotfiles: $DOTFILES_DIR" say "mode: check (no installs or managed-config writes)" - if [[ "$PROFILE" == mac-studio ]]; then - rosetta_installed || { say "Rosetta 2 missing"; status=1; } - vagrant_vmware_plugin_current \ - || { say "Vagrant VMware provider 3.0.5 missing"; status=1; } - fi - for brewfile in "$COMMON_BREWFILE" "$PROFILE_BREWFILE"; do if HOMEBREW_NO_AUTO_UPDATE=1 brew bundle check --no-upgrade --file "$brewfile"; then say "Brewfile satisfied: $brewfile" @@ -184,31 +163,12 @@ DOTFILES_CHEZMOI_CONFIG_ONLY_PREVIEW=1 \ DOTFILES_CHEZMOI_REQUIRE_REVIEWED=1 \ "$CHEZMOI_BOOTSTRAP" "$PROFILE" --preview >/dev/null -if [[ "$PROFILE" == mac-studio ]] && ! rosetta_installed; then - say "Installing Rosetta 2 for the Vagrant VMware utility..." - "$SUDO" "$SOFTWAREUPDATE" --install-rosetta --agree-to-license -fi - say "Installing shared Homebrew dependencies without broad upgrades..." HOMEBREW_NO_AUTO_UPDATE=1 brew bundle install --no-upgrade --file "$COMMON_BREWFILE" say "Installing $PROFILE Homebrew overlay without broad upgrades..." HOMEBREW_NO_AUTO_UPDATE=1 brew bundle install --no-upgrade --file "$PROFILE_BREWFILE" hash -r -if [[ "$PROFILE" == mac-studio ]]; then - if ! command -v vagrant >/dev/null 2>&1; then - say "Repairing the incomplete Vagrant package install..." - HOMEBREW_NO_AUTO_UPDATE=1 brew reinstall --cask vagrant - hash -r - fi - - if ! vagrant_vmware_plugin_current; then - say "Installing pinned Vagrant VMware provider..." - vagrant plugin install vagrant-vmware-desktop \ - --plugin-version "$VAGRANT_VMWARE_PLUGIN_VERSION" - fi -fi - say "Installing pinned mise runtimes..." MISE_NO_CONFIG=1 mise install "${MISE_SPECS[@]}" diff --git a/hosts/shared/macos/doctor.sh b/hosts/shared/macos/doctor.sh index 0caaf4ba..14039bce 100755 --- a/hosts/shared/macos/doctor.sh +++ b/hosts/shared/macos/doctor.sh @@ -10,12 +10,6 @@ PROFILE="" FAILURES=0 MISE_RUNTIME_FAILURES=0 APPLICATIONS_DIR="${DOTFILES_APPLICATIONS_DIR:-/Applications}" -PKGUTIL="${DOTFILES_PKGUTIL:-/usr/sbin/pkgutil}" -VAGRANT_VMWARE_PLUGIN_VERSION="3.0.5" -VAGRANT_VMWARE_UTILITY="${DOTFILES_VAGRANT_VMWARE_UTILITY:-/opt/vagrant-vmware-desktop/bin/vagrant-vmware-utility}" -VAGRANT_VMWARE_SERVICE_LABEL="com.vagrant.vagrant-vmware-utility" -SSH_CONFIG="$HOME/.ssh/config" -UBUNTU_LOGIN_KEY="$HOME/.ssh/id_ed25519_ubuntu_vm" # shellcheck source=lib.sh source "$SCRIPT_DIR/lib.sh" @@ -33,37 +27,6 @@ fail() { FAILURES=$((FAILURES + 1)) } -vagrant_vmware_plugin_current() { - command -v vagrant >/dev/null 2>&1 \ - && vagrant plugin list 2>/dev/null \ - | /usr/bin/grep -Eq \ - "^vagrant-vmware-desktop \($VAGRANT_VMWARE_PLUGIN_VERSION([,)])" -} - -check_studio_ubuntu_ssh_alias() { - local ssh_alias="$1" - local expected_hostname="$2" - local expected_port="$3" - local effective - - effective="$(ssh -G -F "$SSH_CONFIG" "$ssh_alias" 2>/dev/null || true)" - if printf '%s\n' "$effective" \ - | /usr/bin/grep -Fxq "hostname $expected_hostname" \ - && printf '%s\n' "$effective" | /usr/bin/grep -Fxq "port $expected_port" \ - && printf '%s\n' "$effective" | /usr/bin/grep -Fxq "user hamel" \ - && printf '%s\n' "$effective" | /usr/bin/grep -Fxq "addressfamily inet" \ - && printf '%s\n' "$effective" | /usr/bin/grep -Fxq "identitiesonly yes" \ - && printf '%s\n' "$effective" | /usr/bin/grep -Fxq "forwardagent no" \ - && printf '%s\n' "$effective" | /usr/bin/grep -Fxq "hostkeyalias ubuntu-dev" \ - && printf '%s\n' "$effective" | /usr/bin/grep -Fxq "stricthostkeychecking true" \ - && printf '%s\n' "$effective" \ - | /usr/bin/grep -Fq "$(basename "$UBUNTU_LOGIN_KEY")"; then - pass "$ssh_alias Ubuntu route" - else - fail "$ssh_alias Ubuntu route is missing or unsafe" - fi -} - while [[ $# -gt 0 ]]; do case "$1" in --profile) @@ -122,36 +85,6 @@ for brewfile in "$COMMON_BREWFILE" "$PROFILE_BREWFILE"; do done if [[ "$PROFILE" == mac-studio ]]; then - if "$PKGUTIL" --pkg-info com.apple.pkg.RosettaUpdateAuto >/dev/null 2>&1; then - pass "Rosetta 2 installed" - else - fail "Rosetta 2 missing" - fi - - if command -v vagrant >/dev/null 2>&1; then - pass "Vagrant available" - else - fail "Vagrant missing" - fi - - if [[ -x "$VAGRANT_VMWARE_UTILITY" ]]; then - pass "Vagrant VMware utility available" - else - fail "Vagrant VMware utility missing" - fi - - if launchctl print "system/$VAGRANT_VMWARE_SERVICE_LABEL" >/dev/null 2>&1; then - pass "Vagrant VMware utility service active" - else - fail "Vagrant VMware utility service is not active" - fi - - if vagrant_vmware_plugin_current; then - pass "Vagrant VMware provider $VAGRANT_VMWARE_PLUGIN_VERSION" - else - fail "Vagrant VMware provider must be $VAGRANT_VMWARE_PLUGIN_VERSION" - fi - for app_name in "VMware Fusion.app" "Ollama.app"; do if [[ -d "$APPLICATIONS_DIR/$app_name" ]]; then pass "$app_name installed" @@ -160,23 +93,7 @@ if [[ "$PROFILE" == mac-studio ]]; then fi done - if [[ "${DOTFILES_MAC_STUDIO_CUTOVER:-0}" == "1" ]]; then - if [[ -r "$SSH_CONFIG" ]]; then - pass "SSH config readable" - check_studio_ubuntu_ssh_alias ubuntu-vm 127.0.0.1 2222 - check_studio_ubuntu_ssh_alias ubuntu-vm-ts ubuntu-dev 22 - else - fail "SSH config missing or unreadable" - fi - if [[ -f "$UBUNTU_LOGIN_KEY" \ - && "$(stat -f '%Lp' "$UBUNTU_LOGIN_KEY" 2>/dev/null)" == "600" ]]; then - pass "Ubuntu login key present with mode 600" - else - fail "Ubuntu login key missing or not mode 600" - fi - else - printf 'SKIP Studio Ubuntu SSH routes until DOTFILES_MAC_STUDIO_CUTOVER=1\n' - fi + printf 'SKIP Ubuntu runtime and SSH checks (manual use only)\n' fi if "$GIT_ALIASES_SCRIPT" --check >/dev/null 2>&1; then diff --git a/hosts/shared/macos/tests/bootstrap-test.sh b/hosts/shared/macos/tests/bootstrap-test.sh index 4f65aa65..9723596c 100755 --- a/hosts/shared/macos/tests/bootstrap-test.sh +++ b/hosts/shared/macos/tests/bootstrap-test.sh @@ -649,18 +649,15 @@ test_profile_names_and_paths() { "Mac Studio profile uses its Brewfile" assert_contains "$PROFILE_BREWFILE" 'cask "ollama-app"' assert_contains "$PROFILE_BREWFILE" 'cask "vagrant"' - assert_contains "$PROFILE_BREWFILE" 'cask "vagrant-vmware-utility"' + for package in colima docker docker-buildx docker-compose postgresql@17 pgvector; do + assert_contains "$PROFILE_BREWFILE" "brew \"$package\"" + assert_contains "$REPO_DIR/hosts/mac-mini/Brewfile" "brew \"$package\"" + done + assert_not_contains "$PROFILE_BREWFILE" 'cask "vagrant-vmware-utility"' assert_contains "$REPO_DIR/chezmoi/profiles/mac-studio.paths" \ '.zshrc|hosts/mac-studio/.zshrc' assert_not_contains "$REPO_DIR/chezmoi/profiles/mac-studio.paths" \ '.config/karabiner|config/karabiner' - assert_contains "$MAC_BOOTSTRAP_DIR/bootstrap.sh" \ - 'would install Rosetta 2 and vagrant-vmware-desktop 3.0.5 when missing' - assert_contains "$MAC_BOOTSTRAP_DIR/bootstrap.sh" \ - 'brew reinstall --cask vagrant' - assert_contains "$MAC_BOOTSTRAP_DIR/bootstrap.sh" \ - 'vagrant plugin install vagrant-vmware-desktop' - assert_contains "$MAC_BOOTSTRAP_DIR/doctor.sh" 'Vagrant VMware utility service active' assert_contains "$MAC_BOOTSTRAP_DIR/doctor.sh" 'VMware Fusion.app' assert_contains "$MAC_BOOTSTRAP_DIR/doctor.sh" 'Ollama.app' @@ -978,42 +975,20 @@ test_mac_studio_apply() { local home_dir="$root/home" local fake_bin="$root/bin" local log="$root/commands.log" - local rosetta_marker="$root/rosetta-installed" - local plugin_marker="$root/vagrant-plugin-installed" - mkdir -p "$home_dir/Developer" ln -s "$REPO_DIR" "$home_dir/Developer/dotfiles-hd" make_fake_toolchain "$fake_bin" : > "$log" - cat > "$fake_bin/vagrant" <<'EOF' -#!/usr/bin/env bash -printf 'vagrant %s\n' "$*" >> "${COMMAND_LOG:?}" -if [[ "$*" == 'plugin list' && -f "${VAGRANT_PLUGIN_MARKER:?}" ]]; then - printf 'vagrant-vmware-desktop (3.0.5, global)\n' -elif [[ "$*" == 'plugin install vagrant-vmware-desktop --plugin-version 3.0.5' ]]; then - : > "$VAGRANT_PLUGIN_MARKER" -fi -EOF - chmod +x "$fake_bin/vagrant" - - cat > "$fake_bin/pkgutil" <<'EOF' -#!/usr/bin/env bash -[[ "$*" == '--pkg-info com.apple.pkg.RosettaUpdateAuto' ]] || exit 2 -[[ -f "${ROSETTA_MARKER:?}" ]] -EOF - cat > "$fake_bin/softwareupdate" <<'EOF' -#!/usr/bin/env bash -printf 'softwareupdate %s\n' "$*" >> "${COMMAND_LOG:?}" -[[ "$*" == '--install-rosetta --agree-to-license' ]] || exit 2 -: > "${ROSETTA_MARKER:?}" -EOF - cat > "$fake_bin/sudo" <<'EOF' + # Any VM/provider or service interaction is a regression while Ubuntu is dormant. + for command_name in vagrant launchctl softwareupdate colima docker ssh; do + cat > "$fake_bin/$command_name" <<'EOF' #!/usr/bin/env bash -printf 'sudo %s\n' "$*" >> "${COMMAND_LOG:?}" -"$@" +printf 'FORBIDDEN %s %s\n' "${0##*/}" "$*" >> "${COMMAND_LOG:?}" +exit 99 EOF - chmod +x "$fake_bin/pkgutil" "$fake_bin/softwareupdate" "$fake_bin/sudo" + chmod +x "$fake_bin/$command_name" + done if HOME="$home_dir" PATH="$fake_bin:$PATH" COMMAND_LOG="$log" \ DOTFILES_DIR="$REPO_DIR" DOTFILES_ALLOW_DIRTY=1 \ @@ -1029,47 +1004,20 @@ EOF DOTFILES_DIR="$REPO_DIR" DOTFILES_ALLOW_DIRTY=1 \ DOTFILES_ALLOW_NONCANONICAL=1 DOTFILES_MAC_DOCTOR=/usr/bin/true \ DOTFILES_MAC_STUDIO_ARRIVED=1 \ - DOTFILES_PKGUTIL="$fake_bin/pkgutil" \ - DOTFILES_SOFTWAREUPDATE="$fake_bin/softwareupdate" \ - DOTFILES_SUDO="$fake_bin/sudo" ROSETTA_MARKER="$rosetta_marker" \ - VAGRANT_PLUGIN_MARKER="$plugin_marker" \ "$MAC_BOOTSTRAP_DIR/bootstrap.sh" --profile mac-studio --apply >/dev/null - assert_file "$rosetta_marker" - assert_file "$plugin_marker" - assert_contains "$log" 'softwareupdate --install-rosetta --agree-to-license' - assert_contains "$log" \ - 'vagrant plugin install vagrant-vmware-desktop --plugin-version 3.0.5' + assert_not_contains "$log" 'FORBIDDEN' assert_not_contains "$log" 'brew services' assert_not_contains "$log" 'ollama serve' assert_not_contains "$log" 'ollama pull' - mkdir -p \ - "$root/applications/VMware Fusion.app" \ - "$root/applications/Ollama.app" \ - "$root/vagrant-vmware" - printf '#!/usr/bin/env bash\nexit 0\n' > "$root/vagrant-vmware/utility" - chmod +x "$root/vagrant-vmware/utility" - cat > "$fake_bin/launchctl" <<'EOF' -#!/usr/bin/env bash -printf 'launchctl %s\n' "$*" >> "${COMMAND_LOG:?}" -[[ "$*" == 'print system/com.vagrant.vagrant-vmware-utility' ]] -EOF - chmod +x "$fake_bin/launchctl" - mkdir -p "$home_dir/.ssh" - printf 'Include %s\n' \ - "$REPO_DIR/hosts/mac-studio/ssh/ubuntu-vagrant.conf" \ - > "$home_dir/.ssh/config" - : > "$home_dir/.ssh/id_ed25519_ubuntu_vm" - chmod 600 "$home_dir/.ssh/id_ed25519_ubuntu_vm" - + mkdir -p "$root/applications/VMware Fusion.app" "$root/applications/Ollama.app" HOME="$home_dir" PATH="$fake_bin:$PATH" COMMAND_LOG="$log" \ - DOTFILES_DIR="$REPO_DIR" DOTFILES_PKGUTIL="$fake_bin/pkgutil" \ - DOTFILES_APPLICATIONS_DIR="$root/applications" \ - DOTFILES_VAGRANT_VMWARE_UTILITY="$root/vagrant-vmware/utility" \ - DOTFILES_MAC_STUDIO_CUTOVER=1 \ - ROSETTA_MARKER="$rosetta_marker" VAGRANT_PLUGIN_MARKER="$plugin_marker" \ - "$MAC_BOOTSTRAP_DIR/doctor.sh" --profile mac-studio >/dev/null + DOTFILES_DIR="$REPO_DIR" DOTFILES_APPLICATIONS_DIR="$root/applications" \ + "$MAC_BOOTSTRAP_DIR/doctor.sh" --profile mac-studio > "$root/doctor.log" + assert_contains "$root/doctor.log" 'Ubuntu runtime and SSH checks (manual use only)' + assert_not_contains "$log" 'FORBIDDEN' + } test_xdg_bin_home() { diff --git a/hosts/tests/run.sh b/hosts/tests/run.sh index dc21daa5..47ad8f11 100755 --- a/hosts/tests/run.sh +++ b/hosts/tests/run.sh @@ -9,6 +9,7 @@ tests=( hosts/mac-thin/tests/thin-bootstrap-test.sh hosts/mac-thin/tests/vm-lifecycle-test.sh hosts/mac-studio/tests/vm-lifecycle-test.sh + hosts/mac-air/tests/client-profile-test.sh hosts/mac-work/tests/goodmorning-test.sh hosts/mac-work/herdr/tests/hd-lib-test.sh hosts/mac-work/herdr/tests/hd-pargasite-test.sh diff --git a/hosts/ubuntu-dev/GUIDE.md b/hosts/ubuntu-dev/GUIDE.md index ac543198..2ba36381 100644 --- a/hosts/ubuntu-dev/GUIDE.md +++ b/hosts/ubuntu-dev/GUIDE.md @@ -6,8 +6,9 @@ with different spellings. The current workstation is Ubuntu 26.04 ARM64, Zsh, systemd, APT, mise, Docker, Neovim, and tmux. The thin Mac is the current control plane. After the approved -hardware cutover, the VM will live inside the Mac Studio; development remains -inside Ubuntu. +hardware cutover, native development moves to Studio and this VM is preserved +there powered off, with a NAS backup. Use this guide only when Linux is +explicitly requested; Studio readiness does not depend on Ubuntu running. ## The 60-Second Orientation @@ -91,7 +92,7 @@ connection, four remote identities, and Codex login. | Read service logs | `journalctl -u SERVICE -n 100 --no-pager` | | Check containers | `docker compose ps` | | Check a repository | `gs` | -| Check the workstation | `bash hosts/ubuntu-dev/doctor.sh` | +| Check the workstation | `bash hosts/ubuntu-dev/doctor.sh` | | Inspect installed tools | `mise current`, `apt-cache policy PACKAGE` | | Preserve a remote shell | `tmux new -s work`, then `tmux attach -t work` | | Return to the Mac | `exit` or `Ctrl-D` | diff --git a/hosts/ubuntu-dev/README.md b/hosts/ubuntu-dev/README.md index fa4905f4..7dbeab6d 100644 --- a/hosts/ubuntu-dev/README.md +++ b/hosts/ubuntu-dev/README.md @@ -1,8 +1,9 @@ # Ubuntu Workstation -Ubuntu 26.04 ARM64 is the primary development workstation. It currently lives -inside the thin Mac. After an approved cutover, it will be rebuilt inside the -Mac Studio; this staged profile does not move the current VM. +Ubuntu 26.04 ARM64 is the current development workstation on the M3 Max. +After verified Studio cutover, development runs natively on Studio. Preserve +this VM powered off on Studio with a NAS backup; start it only when Hamel +requests Linux again. Staging does not move or rebuild the current VM. ## Rebuild @@ -15,8 +16,9 @@ Prerequisites on the thin Mac: - The tailnet policy defines `tag:ubuntu-dev` and lets `hd719@github` connect to that tag as the `hamel` user. -After Mac Studio cutover, use the equivalent `mac-studio` bootstrap and VM -commands from [`../mac-studio/README.md`](../mac-studio/README.md). +Studio cutover preserves the existing guest rather than requiring this rebuild +workflow. Follow the [Studio preservation runbook](../mac-studio/README.md); +provider setup and any later rebuild are separate, explicit operations. The Forgejo Tailscale route also requires ordinary network access from Ubuntu to TrueNAS. Add this rule to the tailnet policy's `acls` array: @@ -162,32 +164,32 @@ folder. | Vagrant | VM lifecycle, box version, resources, disk, and SSH transport | | Ansible | Ubuntu packages, user, services, Tailscale, and Git keys | | mise | Pinned core runtimes; other development tools track latest | -| Chezmoi | Approved user configuration links and timestamped rollback | -| dotfiles | Portable Git alias include and host operational scripts | +| Chezmoi | Approved user configuration links and timestamped rollback | +| dotfiles | Portable Git alias include and host operational scripts | | doctor | Final read-only acceptance check | Managed links: -| Live path | Source | -| -------------------------------------- | ------------------------------- | -| `~/.zshrc` | `hosts/ubuntu-dev/.zshrc` | -| `~/.config/ghostty/config` | `hosts/ubuntu-dev/ghostty.conf` | -| `~/.config/starship.toml` | `config/starship/starship.toml` | -| `~/.gitignore_global` | `config/git/.gitignore_global` | -| `~/.ssh/config` | `hosts/ubuntu-dev/ssh/config` | -| `~/.config/bookokrat` | `config/bookokrat` | -| `~/.config/btop/btop.conf` | `config/btop/btop.conf` | -| `~/.config/btop/themes` | `config/btop/themes` | -| `~/.config/fastfetch/config.jsonc` | `config/fastfetch/config.jsonc` | +| Live path | Source | +| ------------------------------------------ | --------------------------------------- | +| `~/.zshrc` | `hosts/ubuntu-dev/.zshrc` | +| `~/.config/ghostty/config` | `hosts/ubuntu-dev/ghostty.conf` | +| `~/.config/starship.toml` | `config/starship/starship.toml` | +| `~/.gitignore_global` | `config/git/.gitignore_global` | +| `~/.ssh/config` | `hosts/ubuntu-dev/ssh/config` | +| `~/.config/bookokrat` | `config/bookokrat` | +| `~/.config/btop/btop.conf` | `config/btop/btop.conf` | +| `~/.config/btop/themes` | `config/btop/themes` | +| `~/.config/fastfetch/config.jsonc` | `config/fastfetch/config.jsonc` | | `~/.config/fastfetch/logo-anon-glitch.txt` | `config/fastfetch/logo-anon-glitch.txt` | -| `~/.config/fastfetch/logo-anon.txt` | `config/fastfetch/logo-anon.txt` | -| `~/.config/herdr/config.toml` | `config/herdr/config.toml` | -| `~/.config/hunk/config.toml` | `config/hunk/config.toml` | -| `~/.config/mise/config.toml` | `hosts/ubuntu-dev/mise.toml` | -| `~/.config/nvim` | `config/nvim` | -| `~/.config/tmux` | `config/tmux` | -| `~/.local/bin/codex` | `hosts/ubuntu-dev/bin/codex` | -| `~/.local/graphql-lsp/bin/graphql-lsp` | `hosts/ubuntu-dev/bin/graphql-lsp` | +| `~/.config/fastfetch/logo-anon.txt` | `config/fastfetch/logo-anon.txt` | +| `~/.config/herdr/config.toml` | `config/herdr/config.toml` | +| `~/.config/hunk/config.toml` | `config/hunk/config.toml` | +| `~/.config/mise/config.toml` | `hosts/ubuntu-dev/mise.toml` | +| `~/.config/nvim` | `config/nvim` | +| `~/.config/tmux` | `config/tmux` | +| `~/.local/bin/codex` | `hosts/ubuntu-dev/bin/codex` | +| `~/.local/graphql-lsp/bin/graphql-lsp` | `hosts/ubuntu-dev/bin/graphql-lsp` | Hunk is the canonical diff viewer on every profile. Use `hdiff`, `hstaged`, `hshow`, or `hwatch`; the shell does not install a Git pager override. From 9bb6f0178b2e7ede31493ac816c097dfa42b9555 Mon Sep 17 00:00:00 2001 From: arbiter-hd <288758862+arbiter-hd@users.noreply.github.com> Date: Tue, 15 Sep 2026 13:54:07 -0400 Subject: [PATCH 3/5] feat: complete Studio personal and development package baseline --- hosts/mac-studio/.zshrc | 8 +++- hosts/mac-studio/Brewfile | 14 +++++- hosts/mac-studio/README.md | 28 +++++++----- hosts/shared/macos/README.md | 6 ++- hosts/shared/macos/bootstrap.sh | 19 +++++++- hosts/shared/macos/doctor.sh | 6 +++ hosts/shared/macos/tests/bootstrap-test.sh | 50 ++++++++++++++++++++-- 7 files changed, 112 insertions(+), 19 deletions(-) diff --git a/hosts/mac-studio/.zshrc b/hosts/mac-studio/.zshrc index 5fc5d51f..b656ccd5 100644 --- a/hosts/mac-studio/.zshrc +++ b/hosts/mac-studio/.zshrc @@ -18,7 +18,13 @@ _zsh_add_completion_dirs \ /usr/local/share/zsh/site-functions _zsh_init_completions 43200 typeset -gaU path -path=("${XDG_BIN_HOME:-$HOME/.local/bin}" "${HOMEBREW_PREFIX:-/opt/homebrew}/opt/postgresql@17/bin" $path) +path=( + "${XDG_BIN_HOME:-$HOME/.local/bin}" + "${HOMEBREW_PREFIX:-/opt/homebrew}/opt/postgresql@17/bin" + "${HOMEBREW_PREFIX:-/opt/homebrew}/opt/dotnet@9/bin" + $path +) +export DOTNET_ROOT="${HOMEBREW_PREFIX:-/opt/homebrew}/opt/dotnet@9/libexec" export PATH _activate_mise diff --git a/hosts/mac-studio/Brewfile b/hosts/mac-studio/Brewfile index 4872904d..c5161a5e 100644 --- a/hosts/mac-studio/Brewfile +++ b/hosts/mac-studio/Brewfile @@ -2,18 +2,29 @@ # ../shared/macos/Brewfile. brew "colima" +brew "coreutils" brew "docker" brew "docker-buildx" brew "docker-compose" -brew "postgresql@17" +brew "dotnet@9" +brew "ffmpeg" +brew "git-filter-repo" brew "pgvector" +brew "poppler" +brew "postgresql@17" +brew "tesseract" +brew "websocat" +brew "xcodegen" cask "1password" cask "brave-browser" cask "chatgpt" cask "codex" +cask "chromium" cask "daisydisk" cask "deskflow" +cask "font-caskaydia-cove-nerd-font" +cask "font-hasklug-nerd-font" cask "google-chrome" cask "istat-menus" cask "little-snitch" @@ -28,6 +39,7 @@ cask "tableplus" cask "tailscale-app" cask "telegram" cask "vagrant" +cask "vagrant-vmware-utility" cask "visual-studio-code" vscode "openai.chatgpt" cask "vlc" diff --git a/hosts/mac-studio/README.md b/hosts/mac-studio/README.md index a327d5f6..52d5e061 100644 --- a/hosts/mac-studio/README.md +++ b/hosts/mac-studio/README.md @@ -43,7 +43,9 @@ hosts/shared/macos/doctor.sh --profile mac-studio ``` The arrival flag is a hard gate; set it only on Studio. Bootstrap installs the -shared native development tools, the Studio Brewfile and configuration. It +shared native development tools, the Studio Brewfile and configuration. The +VMware utility installer registers its host helper service, using Rosetta 2 +on Apple Silicon; bootstrap installs Rosetta when missing. The profile never starts Ubuntu, Colima, PostgreSQL or Ollama. Normal checks audit installed packages/configuration without requiring any guest, Docker daemon, database server, VMware utility service or Ubuntu SSH route to be online. @@ -51,7 +53,11 @@ server, VMware utility service or Ubuntu SSH route to be online. ## Native Development Homebrew and the shared mise configuration provide the native toolchain. -Studio adds Colima, Docker, Compose, Buildx, PostgreSQL 17, pgvector and VS Code. +Studio includes the current personal-Mac app/font set, Vagrant and its VMware +utility, plus the mini's shared development stack. Its overlay adds .NET 9, +Colima, Docker, Compose, Buildx, PostgreSQL 17, pgvector, VS Code and Chromium, +with the mini's coreutils, FFmpeg, Git filter-repo, Poppler, Tesseract, Websocat +and XcodeGen tools. Homebrew resolves supporting libraries as dependencies. Colima's small VM supplies Docker only; repositories and toolchains stay native on Studio. [Colima setup](https://github.com/abiosoft/colima#installation) requires a separate, deliberate `colima start` after installation. @@ -63,9 +69,10 @@ See the [Homebrew Compose caveat](https://formulae.brew.sh/formula/docker-compos Verify `docker compose version`, `docker buildx version` and, after starting Colima, `docker info`. Dotfiles does not rewrite Docker configuration or data. -The Studio shell exposes PostgreSQL 17 client tools. Homebrew creates its -initial empty cluster during package installation; project database setup, -restores and server startup remain deliberate project steps. Verify local +The Studio shell exposes PostgreSQL 17 client tools and the .NET 9 SDK. +`DOTNET_ROOT` points to the Homebrew .NET 9 installation. Homebrew creates the +initial PostgreSQL cluster during package installation; project database +setup, restores and server startup remain deliberate project steps. Verify local builds/tests and database access for the repositories being moved, including Cortana Services. Never use the mini's production database for development. @@ -88,11 +95,12 @@ For mini development isolation, follow its [runbook](../mac-mini/README.md). 1. Leave Ubuntu off. Its restore boot, provider setup, identity changes or rebuild happen only when Hamel explicitly requests them later. -The normal profile installs Vagrant but defers its VMware utility, Rosetta -requirement and pinned `vagrant-vmware-desktop` 3.0.5 provider until Ubuntu is -requested. Restore and verify both VM and `.vagrant` metadata before using the -helpers. Adjust host-specific paths and provide the guest's expected login -public key through a reviewed restoration procedure; never overwrite keys. +The normal profile installs Vagrant, its VMware utility and Rosetta when +needed. The pinned `vagrant-vmware-desktop` 3.0.5 provider plugin is deferred +until Ubuntu is requested. Restore and verify both VM and `.vagrant` metadata +before using the helpers. Adjust host-specific paths and provide the guest's +expected login public key through a reviewed restoration procedure; never +overwrite keys. The shell exposes `uvm-status`, `uvm-up`, `uvm-stop`, `uvm-suspend`, `uvm-resume` and `uvm-ip`. Loading the shell does not invoke Vagrant. Start/resume refuse a diff --git a/hosts/shared/macos/README.md b/hosts/shared/macos/README.md index bbff1ac7..3c6f8bc7 100644 --- a/hosts/shared/macos/README.md +++ b/hosts/shared/macos/README.md @@ -56,8 +56,10 @@ state, databases, and application data remain machine-owned. Studio and mini overlays include Colima/Docker development tools; Studio also adds PostgreSQL 17, which mini already retains. Package installation does not start these services. Studio installs Ollama without activation or model -pulls. VMware Fusion setup remains manual; Ubuntu provider installation is -deferred until the preserved guest is explicitly requested. See the +pulls. The Studio VMware utility installer registers its host helper service; +Rosetta is installed when missing. VMware Fusion setup remains manual; Ubuntu +provider plugin installation is deferred until the preserved guest is +explicitly requested. See the [Studio runbook](../../mac-studio/README.md) for manual Docker plugin setup, database ownership and remote acceptance. Air uses its separate client bootstrap, never the shared full-development bootstrap. diff --git a/hosts/shared/macos/bootstrap.sh b/hosts/shared/macos/bootstrap.sh index 98aa49cf..633327b0 100755 --- a/hosts/shared/macos/bootstrap.sh +++ b/hosts/shared/macos/bootstrap.sh @@ -8,6 +8,9 @@ STAMP="${DOTFILES_STAMP:-$(date +%Y%m%d-%H%M%S)}" CHEZMOI_BOOTSTRAP="${DOTFILES_CHEZMOI_BOOTSTRAP:-$DOTFILES_DIR/chezmoi/bootstrap.sh}" CHEZMOI_PREVIEW="${DOTFILES_CHEZMOI_PREVIEW:-$DOTFILES_DIR/chezmoi/preview.sh}" HOST_DOCTOR="${DOTFILES_MAC_DOCTOR:-$SCRIPT_DIR/doctor.sh}" +PKGUTIL="${DOTFILES_PKGUTIL:-/usr/sbin/pkgutil}" +SOFTWAREUPDATE="${DOTFILES_SOFTWAREUPDATE:-/usr/sbin/softwareupdate}" +SUDO="${DOTFILES_SUDO:-/usr/bin/sudo}" PROFILE="" MODE="dry-run" @@ -27,7 +30,8 @@ Modes: --apply Install dependencies and apply configuration through Chezmoi. The script never handles credentials, removes packages, cleans Homebrew, or -starts/restarts services. Xcode Command Line Tools, Homebrew, and a clean clone +starts/restarts workload services. The Studio VMware utility installer registers +its host helper service. Xcode Command Line Tools, Homebrew, and a clean clone at ~/Developer/dotfiles-hd are prerequisites. EOF } @@ -67,6 +71,10 @@ done PROFILE="$(canonical_profile "$PROFILE")" || exit 2 load_profile "$PROFILE" "$DOTFILES_DIR" "$HOME" +rosetta_installed() { + "$PKGUTIL" --pkg-info com.apple.pkg.RosettaUpdateAuto >/dev/null 2>&1 +} + [[ "$(uname -s)" == "Darwin" ]] || die "personal-Mac bootstrap requires macOS" [[ "$(uname -m)" == "arm64" ]] || die "personal-Mac bootstrap currently supports Apple Silicon only" xcode-select -p >/dev/null 2>&1 || die "install Xcode Command Line Tools first: xcode-select --install" @@ -116,7 +124,9 @@ if [[ "$MODE" == "dry-run" ]]; then say "would restore locked Neovim plugins and required Tree-sitter parsers without changing lazy-lock.json" say "would run the verification doctor" if [[ "$PROFILE" == mac-studio ]]; then - say "Ubuntu stays dormant; provider setup is deferred until manual use" + say "would install Rosetta 2 when missing for the VMware utility" + say "the utility installer registers a host helper; Ubuntu stays dormant" + say "Vagrant provider plugin setup is deferred until manual use" say "VMware Fusion remains a manual install; Ollama models remain machine-owned" fi @@ -163,6 +173,11 @@ DOTFILES_CHEZMOI_CONFIG_ONLY_PREVIEW=1 \ DOTFILES_CHEZMOI_REQUIRE_REVIEWED=1 \ "$CHEZMOI_BOOTSTRAP" "$PROFILE" --preview >/dev/null +if [[ "$PROFILE" == mac-studio ]] && ! rosetta_installed; then + say "Installing Rosetta 2 for the Vagrant VMware utility..." + "$SUDO" "$SOFTWAREUPDATE" --install-rosetta --agree-to-license +fi + say "Installing shared Homebrew dependencies without broad upgrades..." HOMEBREW_NO_AUTO_UPDATE=1 brew bundle install --no-upgrade --file "$COMMON_BREWFILE" say "Installing $PROFILE Homebrew overlay without broad upgrades..." diff --git a/hosts/shared/macos/doctor.sh b/hosts/shared/macos/doctor.sh index 14039bce..c31348bb 100755 --- a/hosts/shared/macos/doctor.sh +++ b/hosts/shared/macos/doctor.sh @@ -10,6 +10,7 @@ PROFILE="" FAILURES=0 MISE_RUNTIME_FAILURES=0 APPLICATIONS_DIR="${DOTFILES_APPLICATIONS_DIR:-/Applications}" +PKGUTIL="${DOTFILES_PKGUTIL:-/usr/sbin/pkgutil}" # shellcheck source=lib.sh source "$SCRIPT_DIR/lib.sh" @@ -85,6 +86,11 @@ for brewfile in "$COMMON_BREWFILE" "$PROFILE_BREWFILE"; do done if [[ "$PROFILE" == mac-studio ]]; then + if "$PKGUTIL" --pkg-info com.apple.pkg.RosettaUpdateAuto >/dev/null 2>&1; then + pass "Rosetta 2 installed for the VMware utility" + else + fail "Rosetta 2 missing for the VMware utility" + fi for app_name in "VMware Fusion.app" "Ollama.app"; do if [[ -d "$APPLICATIONS_DIR/$app_name" ]]; then pass "$app_name installed" diff --git a/hosts/shared/macos/tests/bootstrap-test.sh b/hosts/shared/macos/tests/bootstrap-test.sh index 9723596c..d506998b 100755 --- a/hosts/shared/macos/tests/bootstrap-test.sh +++ b/hosts/shared/macos/tests/bootstrap-test.sh @@ -653,7 +653,7 @@ test_profile_names_and_paths() { assert_contains "$PROFILE_BREWFILE" "brew \"$package\"" assert_contains "$REPO_DIR/hosts/mac-mini/Brewfile" "brew \"$package\"" done - assert_not_contains "$PROFILE_BREWFILE" 'cask "vagrant-vmware-utility"' + assert_contains "$PROFILE_BREWFILE" 'cask "vagrant-vmware-utility"' assert_contains "$REPO_DIR/chezmoi/profiles/mac-studio.paths" \ '.zshrc|hosts/mac-studio/.zshrc' assert_not_contains "$REPO_DIR/chezmoi/profiles/mac-studio.paths" \ @@ -980,8 +980,8 @@ test_mac_studio_apply() { make_fake_toolchain "$fake_bin" : > "$log" - # Any VM/provider or service interaction is a regression while Ubuntu is dormant. - for command_name in vagrant launchctl softwareupdate colima docker ssh; do + # Bootstrap never starts the guest or workload services. + for command_name in vagrant launchctl colima docker ssh; do cat > "$fake_bin/$command_name" <<'EOF' #!/usr/bin/env bash printf 'FORBIDDEN %s %s\n' "${0##*/}" "$*" >> "${COMMAND_LOG:?}" @@ -990,6 +990,24 @@ EOF chmod +x "$fake_bin/$command_name" done + local rosetta_marker="$root/rosetta-installed" + cat > "$fake_bin/pkgutil" <<'EOF' +#!/usr/bin/env bash +[[ "$*" == '--pkg-info com.apple.pkg.RosettaUpdateAuto' ]] || exit 2 +[[ -f "${ROSETTA_MARKER:?}" ]] +EOF + cat > "$fake_bin/softwareupdate" <<'EOF' +#!/usr/bin/env bash +printf 'softwareupdate %s\n' "$*" >> "${COMMAND_LOG:?}" +[[ "$*" == '--install-rosetta --agree-to-license' ]] || exit 2 +: > "${ROSETTA_MARKER:?}" +EOF + cat > "$fake_bin/sudo" <<'EOF' +#!/usr/bin/env bash +"$@" +EOF + chmod +x "$fake_bin/pkgutil" "$fake_bin/softwareupdate" "$fake_bin/sudo" + if HOME="$home_dir" PATH="$fake_bin:$PATH" COMMAND_LOG="$log" \ DOTFILES_DIR="$REPO_DIR" DOTFILES_ALLOW_DIRTY=1 \ DOTFILES_ALLOW_NONCANONICAL=1 DOTFILES_MAC_DOCTOR=/usr/bin/true \ @@ -1004,8 +1022,22 @@ EOF DOTFILES_DIR="$REPO_DIR" DOTFILES_ALLOW_DIRTY=1 \ DOTFILES_ALLOW_NONCANONICAL=1 DOTFILES_MAC_DOCTOR=/usr/bin/true \ DOTFILES_MAC_STUDIO_ARRIVED=1 \ + DOTFILES_PKGUTIL="$fake_bin/pkgutil" ROSETTA_MARKER="$rosetta_marker" \ + DOTFILES_SOFTWAREUPDATE="$fake_bin/softwareupdate" DOTFILES_SUDO="$fake_bin/sudo" \ "$MAC_BOOTSTRAP_DIR/bootstrap.sh" --profile mac-studio --apply >/dev/null + assert_file "$rosetta_marker" + assert_contains "$log" 'softwareupdate --install-rosetta --agree-to-license' + : > "$log" + HOME="$home_dir" PATH="$fake_bin:$PATH" COMMAND_LOG="$log" \ + DOTFILES_DIR="$REPO_DIR" DOTFILES_ALLOW_DIRTY=1 \ + DOTFILES_ALLOW_NONCANONICAL=1 DOTFILES_MAC_DOCTOR=/usr/bin/true \ + DOTFILES_MAC_STUDIO_ARRIVED=1 \ + DOTFILES_PKGUTIL="$fake_bin/pkgutil" ROSETTA_MARKER="$rosetta_marker" \ + DOTFILES_SOFTWAREUPDATE="$fake_bin/softwareupdate" DOTFILES_SUDO="$fake_bin/sudo" \ + "$MAC_BOOTSTRAP_DIR/bootstrap.sh" --profile mac-studio --apply >/dev/null + assert_not_contains "$log" 'softwareupdate' + assert_not_contains "$log" 'FORBIDDEN' assert_not_contains "$log" 'brew services' assert_not_contains "$log" 'ollama serve' @@ -1014,6 +1046,7 @@ EOF mkdir -p "$root/applications/VMware Fusion.app" "$root/applications/Ollama.app" HOME="$home_dir" PATH="$fake_bin:$PATH" COMMAND_LOG="$log" \ DOTFILES_DIR="$REPO_DIR" DOTFILES_APPLICATIONS_DIR="$root/applications" \ + DOTFILES_PKGUTIL="$fake_bin/pkgutil" ROSETTA_MARKER="$rosetta_marker" \ "$MAC_BOOTSTRAP_DIR/doctor.sh" --profile mac-studio > "$root/doctor.log" assert_contains "$root/doctor.log" 'Ubuntu runtime and SSH checks (manual use only)' assert_not_contains "$log" 'FORBIDDEN' @@ -1046,6 +1079,17 @@ test_xdg_bin_home() { 2>/dev/null | tail -n 1)" assert_eq "$custom_bin/ruff" "$resolved" "$(basename "$(dirname "$zshrc")") zshrc keeps XDG_BIN_HOME first" done + local dotnet_prefix="$root/brew/opt/dotnet@9" + mkdir -p "$dotnet_prefix/bin" "$dotnet_prefix/libexec" + printf '#!/bin/sh\nexit 0\n' > "$dotnet_prefix/bin/dotnet" + chmod +x "$dotnet_prefix/bin/dotnet" + ln -s "$REPO_DIR/hosts/mac-studio/.zshrc" "$home_dir/.zshrc" + resolved="$(HOME="$home_dir" HOMEBREW_PREFIX="$root/brew" PATH=/usr/bin:/bin \ + /bin/zsh -lic 'printf "%s|%s\n" "$(command -v dotnet)" "$DOTNET_ROOT"' \ + 2>/dev/null | tail -n 1)" + assert_eq "$dotnet_prefix/bin/dotnet|$dotnet_prefix/libexec" "$resolved" \ + 'Studio login shell exposes the requested .NET SDK and root' + } test_profile_and_failure_guards() { From 61c125e1097b7da2e16403bd0df325796fd22ea8 Mon Sep 17 00:00:00 2001 From: arbiter-hd <288758862+arbiter-hd@users.noreply.github.com> Date: Tue, 15 Sep 2026 14:03:27 -0400 Subject: [PATCH 4/5] feat: align Air with thin Mac tools without VMware --- chezmoi/README.md | 18 +++---- chezmoi/profiles/mac-air.ancestors | 1 + chezmoi/profiles/mac-air.paths | 13 ++++- chezmoi/source/.chezmoiignore.tmpl | 8 --- ...change_after_30-install-thin-tools.sh.tmpl | 2 +- chezmoi/tests/production-test.sh | 7 ++- hosts/mac-air/.zshrc | 18 +++++-- hosts/mac-air/Brewfile | 29 +++++++++- hosts/mac-air/README.md | 39 ++++++++++---- hosts/mac-air/bootstrap.sh | 5 +- hosts/mac-air/doctor.sh | 28 +++++++++- hosts/mac-air/tests/client-profile-test.sh | 54 ++++++++++++++++--- 12 files changed, 174 insertions(+), 48 deletions(-) create mode 100644 chezmoi/profiles/mac-air.ancestors diff --git a/chezmoi/README.md b/chezmoi/README.md index dc74891e..8b8b4120 100644 --- a/chezmoi/README.md +++ b/chezmoi/README.md @@ -9,15 +9,15 @@ services, secrets, identity, enrollment, macOS preferences, or mutable state. ## Profiles -| Profile | Host | Package behavior | -| ------------ | ------------------------------------------------------------ | ----------------------------------------------------------------------------- | -| `ubuntu` | Ubuntu development VM | Installs the declared mise toolchain | -| `mac-thin` | Thin Mac control plane | Installs the thin Brewfile | -| `mac-air` | Future remote-access clients; no VMware or local development | Client packages through the Air bootstrap | -| `mac-pro` | Standalone full-development MacBook | Installs shared and profile Brewfiles through the Mac bootstrap | -| `mac-studio` | Staged native development, dormant Ubuntu and local AI | Installs shared and profile Brewfiles through the Mac bootstrap | -| `mac-mini` | Production Mac mini | Configuration-only inside Chezmoi; packages stay in the guarded Mac bootstrap | -| `mac-work` | Resilience work Mac | Reserved for a later opt-in rollout | +| Profile | Host | Package behavior | +| ------------ | ------------------------------------------------------ | ----------------------------------------------------------------------------- | +| `ubuntu` | Ubuntu development VM | Installs the declared mise toolchain | +| `mac-thin` | Thin Mac control plane | Installs the thin Brewfile | +| `mac-air` | Future thin-Mac clients and note tools; no VMware | Installs the Air Brewfile and thin Neovim plugins | +| `mac-pro` | Standalone full-development MacBook | Installs shared and profile Brewfiles through the Mac bootstrap | +| `mac-studio` | Staged native development, dormant Ubuntu and local AI | Installs shared and profile Brewfiles through the Mac bootstrap | +| `mac-mini` | Production Mac mini | Configuration-only inside Chezmoi; packages stay in the guarded Mac bootstrap | +| `mac-work` | Resilience work Mac | Reserved for a later opt-in rollout | Each `profiles/*.paths` file is the exact backup, apply, doctor, and rollback allowlist. A matching `*.ancestors` file records parent directories whose diff --git a/chezmoi/profiles/mac-air.ancestors b/chezmoi/profiles/mac-air.ancestors new file mode 100644 index 00000000..f28833ce --- /dev/null +++ b/chezmoi/profiles/mac-air.ancestors @@ -0,0 +1 @@ +.config/fastfetch|config/fastfetch|700 diff --git a/chezmoi/profiles/mac-air.paths b/chezmoi/profiles/mac-air.paths index 01f938b6..081b4e1f 100644 --- a/chezmoi/profiles/mac-air.paths +++ b/chezmoi/profiles/mac-air.paths @@ -1,4 +1,13 @@ .zshrc|hosts/mac-air/.zshrc -.config/starship.toml|config/starship/starship.toml -.config/homebrew/Brewfile|hosts/mac-air/Brewfile +.config/zsh|config/zsh +.config/bookokrat|config/bookokrat +.config/fastfetch/config.jsonc|config/fastfetch/config.jsonc +.config/fastfetch/logo-anon-glitch.txt|config/fastfetch/logo-anon-glitch.txt +.config/fastfetch/logo-anon.txt|config/fastfetch/logo-anon.txt Library/Application Support/com.mitchellh.ghostty/config|hosts/mac-air/ghostty.conf +.config/herdr/config.toml|config/herdr/config.toml +.config/hunk/config.toml|config/hunk/config.toml +.config/homebrew/Brewfile|hosts/mac-air/Brewfile +.config/nvim|config/nvim +.config/starship.toml|config/starship/starship.toml +.terminfo/78/xterm-ghostty|@absolute:/Applications/Ghostty.app/Contents/Resources/terminfo/78/xterm-ghostty diff --git a/chezmoi/source/.chezmoiignore.tmpl b/chezmoi/source/.chezmoiignore.tmpl index af0f6371..aa365b5b 100644 --- a/chezmoi/source/.chezmoiignore.tmpl +++ b/chezmoi/source/.chezmoiignore.tmpl @@ -69,17 +69,9 @@ Library 30-install-thin-tools.sh {{- end }} {{- if eq .profile "mac-air" }} -.config/zsh -.config/bookokrat .config/btop -.config/fastfetch -.config/herdr -.config/hunk .config/mise -.config/nvim .hermes -.terminfo 20-install-ubuntu-tools.sh -30-install-thin-tools.sh 40-install-work-tools.sh {{- end }} diff --git a/chezmoi/source/run_onchange_after_30-install-thin-tools.sh.tmpl b/chezmoi/source/run_onchange_after_30-install-thin-tools.sh.tmpl index d1b2521d..21df8ec7 100644 --- a/chezmoi/source/run_onchange_after_30-install-thin-tools.sh.tmpl +++ b/chezmoi/source/run_onchange_after_30-install-thin-tools.sh.tmpl @@ -2,7 +2,7 @@ set -euo pipefail {{- $repo := joinPath .chezmoi.sourceDir ".." ".." }} -# brew: {{ output "git" "-C" $repo "hash-object" "hosts/mac-thin/Brewfile" | trim }} +# brew: {{ output "git" "-C" $repo "hash-object" (printf "hosts/%s/Brewfile" .profile) | trim }} # nvim: {{ output "git" "-C" $repo "hash-object" "config/nvim/lazy-lock.json" | trim }} brewfile="$HOME/.config/homebrew/Brewfile" lockfile="$HOME/.config/nvim/lazy-lock.json" diff --git a/chezmoi/tests/production-test.sh b/chezmoi/tests/production-test.sh index c9bfc587..9d8596b4 100755 --- a/chezmoi/tests/production-test.sh +++ b/chezmoi/tests/production-test.sh @@ -187,7 +187,7 @@ for profile in ubuntu mac-thin mac-air mac-pro mac-studio mac-mini mac-work; do mac-pro|mac-studio|mac-mini) prepare_mac_mini_home "$home_dir" ;; esac prepare_profile_parents "$profile" "$home_dir" - if [[ "$profile" == mac-thin ]]; then + if [[ "$profile" == mac-thin || "$profile" == mac-air ]]; then chmod 700 "$home_dir/.config" "$home_dir/.config/fastfetch" fi common=( @@ -206,8 +206,7 @@ for profile in ubuntu mac-thin mac-air mac-pro mac-studio mac-mini mac-work; do case "$profile" in ubuntu) printf '%s\n' 10-configure-git.sh 20-install-ubuntu-tools.sh ;; - mac-thin) printf '%s\n' 10-configure-git.sh 30-install-thin-tools.sh ;; - mac-air) printf '%s\n' 10-configure-git.sh ;; + mac-thin|mac-air) printf '%s\n' 10-configure-git.sh 30-install-thin-tools.sh ;; mac-pro) printf '%s\n' 10-configure-git.sh ;; mac-studio) printf '%s\n' 10-configure-git.sh ;; mac-mini) printf '%s\n' 10-configure-git.sh ;; @@ -237,7 +236,7 @@ for profile in ubuntu mac-thin mac-air mac-pro mac-studio mac-mini mac-work; do [[ -z "$("$CHEZMOI_BIN" "${common[@]}" status --exclude=scripts,dirs)" ]] "$CHEZMOI_BIN" "${common[@]}" verify --exclude=scripts,dirs [[ "$(path_mode "$home_dir/.config")" == 700 ]] - if [[ "$profile" == mac-thin ]]; then + if [[ "$profile" == mac-thin || "$profile" == mac-air ]]; then [[ "$(path_mode "$home_dir/.config/fastfetch")" == 700 ]] fi if [[ "$profile" == mac-mini ]]; then diff --git a/hosts/mac-air/.zshrc b/hosts/mac-air/.zshrc index 91cb8a1c..7dd7535d 100644 --- a/hosts/mac-air/.zshrc +++ b/hosts/mac-air/.zshrc @@ -1,8 +1,10 @@ # Lightweight remote-access shell. No local project runtimes or VM helpers. export DOTFILES_MAC_PROFILE=mac-air -export EDITOR=vi -export VISUAL=vi -export GIT_EDITOR=vi +export DOTFILES_NVIM_PROFILE=thin +export EDITOR=nvim +export VISUAL=nvim +export GIT_EDITOR=nvim +unset GIT_PAGER HISTFILE="$HOME/.zsh_history" HISTSIZE=10000 SAVEHIST=10000 @@ -12,6 +14,16 @@ typeset -gaU path path=("${HOMEBREW_PREFIX:-/opt/homebrew}/bin" "$HOME/.local/bin" $path) export PATH +typeset air_zshrc="${${(%):-%N}:A}" +typeset air_repo="${air_zshrc:h:h:h}" +source "$air_repo/config/zsh/shared/functions.zsh" +source "$air_repo/config/zsh/shared/aliases.zsh" +source "$air_repo/config/zsh/shared/codex-aliases.zsh" +source "$air_repo/config/zsh/shared/codex-functions.zsh" +source "$air_repo/config/zsh/mac/aliases.zsh" +alias vault='cd ~/Developer/hd' +unset air_repo air_zshrc + if [[ -o interactive ]]; then (( $+commands[zoxide] )) && eval "$(zoxide init --cmd cd zsh)" (( $+commands[starship] )) && eval "$(starship init zsh)" diff --git a/hosts/mac-air/Brewfile b/hosts/mac-air/Brewfile index 5a7b5aeb..08611908 100644 --- a/hosts/mac-air/Brewfile +++ b/hosts/mac-air/Brewfile @@ -1,6 +1,16 @@ -# Remote clients only. Project tools and execution belong on the Studio. +# Thin-Mac applications and note tools. Project development runs on Studio. + +brew "bookokrat" +brew "fastfetch" brew "gh" +brew "herdr" +brew "hunk" +brew "lsd" +brew "marksman" +brew "neovim" +brew "ripgrep" brew "starship" +brew "tree-sitter-cli" brew "zoxide" brew "zsh-autosuggestions" brew "zsh-syntax-highlighting" @@ -8,7 +18,24 @@ brew "zsh-syntax-highlighting" cask "1password" cask "brave-browser" cask "chatgpt" +cask "codex" +cask "daisydisk" +cask "deskflow" +cask "font-caskaydia-cove-nerd-font" +cask "font-hasklug-nerd-font" cask "font-maple-mono-nf" cask "ghostty" +cask "google-chrome" +cask "istat-menus" +cask "little-snitch" +cask "micro-snitch" +cask "mullvad-vpn" +cask "notunes" cask "obsidian" +cask "pearcleaner" +cask "raycast" +cask "tableplus" cask "tailscale-app" +cask "telegram" +cask "vlc" +cask "zoom" diff --git a/hosts/mac-air/README.md b/hosts/mac-air/README.md index b0cc939a..96a166f5 100644 --- a/hosts/mac-air/README.md +++ b/hosts/mac-air/README.md @@ -1,13 +1,17 @@ # Future MacBook Air -`mac-air` is the future lightweight access machine for Studio. It runs Codex, -Ghostty, a browser and Obsidian locally. Repository development, coding agents, -builds, tests, containers and development databases execute on Studio. +`mac-air` is the future lightweight access machine for Studio. It uses the +thin Mac's apps, fonts and local note tools, including Codex, Ghostty, +browsers, Obsidian, Herdr, Hunk, Bookokrat, Fastfetch and Neovim. Repository +development, coding agents, builds, tests, containers and development +databases execute on Studio. The `chatgpt` Homebrew cask supplies the current Codex desktop app bundle. This profile is separate from the current M3 Max's VMware-owning `mac-thin` -profile. It installs no VMware, Vagrant, editor toolchains, project runtimes, -Docker or database server. Studio rollout does not wait for the Air to arrive. +profile. Its Brewfile matches that profile except for Vagrant and the VMware +utility, and adds the Codex desktop app. VMware Fusion, Rosetta/provider setup +and VM lifecycle helpers are excluded. Project runtimes, Docker and database +servers remain on Studio. Studio rollout does not wait for the Air to arrive. ## Setup After Arrival @@ -27,10 +31,27 @@ DOTFILES_MAC_AIR_ARRIVED=1 hosts/mac-air/bootstrap.sh --apply hosts/mac-air/doctor.sh ``` -The arrival gate also protects direct Chezmoi apply. The profile owns only the -shell, prompt, lightweight Ghostty configuration and Brewfile link listed in -`chezmoi/profiles/mac-air.paths`. Use the timestamped Chezmoi rollback printed -by apply. Packages, credentials and application data remain machine-owned. +The arrival gate also protects direct Chezmoi apply. Chezmoi installs the Air +Brewfile, restores the locked thin Neovim plugins and builds only the two +Markdown parsers. The profile owns the shell, lightweight Ghostty config and +shared note/client configuration listed in `chezmoi/profiles/mac-air.paths`. +Use the timestamped Chezmoi rollback printed by apply. Packages, credentials +and mutable application data remain machine-owned. + +## Local Notes and Clients + +Neovim uses `DOTFILES_NVIM_PROFILE=thin` for Markdown and Obsidian notes; +Marksman is its only language server and is toggled per file with `Space m m`. +Tree-sitter CLI exists to build the Markdown parsers. Use `v` for Neovim, +`hdiff` for Hunk, and the usual thin-Mac navigation and Codex shortcuts. +The editor stays lightweight while project builds, tests and dependencies +live on Studio. + +Herdr uses the shared client configuration. Connect with `herdr --remote` +followed by the verified Studio SSH alias; remote aliases and credentials are +configured after arrival. The Air shell does not inherit Ubuntu shortcuts or +start Herdr/remote sessions automatically. Hermes Desktop can be added as a +remote mini client using the [thin runbook's manual app procedure](../mac-thin/README.md#manual-applications). ## Remote Acceptance diff --git a/hosts/mac-air/bootstrap.sh b/hosts/mac-air/bootstrap.sh index 21ca219a..5bd3db22 100755 --- a/hosts/mac-air/bootstrap.sh +++ b/hosts/mac-air/bootstrap.sh @@ -24,8 +24,8 @@ git -C "$DOTFILES_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1 \ [[ -f "$BREWFILE" ]] || die "missing Brewfile: $BREWFILE" if [[ "$MODE" == --dry-run ]]; then - printf 'profile: mac-air\nwould install remote clients from: %s\n' "$BREWFILE" - printf 'would apply client configuration; execution stays on Studio\n' + printf 'profile: mac-air\nwould install thin-Mac apps and note tools from: %s\n' "$BREWFILE" + printf 'would restore the thin Neovim profile and Markdown parsers; project execution stays on Studio\n' if [[ -x "${CHEZMOI_BIN:-$HOME/.local/bin/chezmoi}" ]]; then DOTFILES_CHEZMOI_CONFIG_ONLY_PREVIEW=1 bash "$CHEZMOI_PREVIEW" mac-air fi @@ -42,7 +42,6 @@ fi # Reviewed canonical-checkout and layout validation precede package mutations. DOTFILES_CHEZMOI_CONFIG_ONLY_PREVIEW=1 DOTFILES_CHEZMOI_REQUIRE_REVIEWED=1 \ "$CHEZMOI_BOOTSTRAP" mac-air --preview >/dev/null -HOMEBREW_NO_AUTO_UPDATE=1 brew bundle install --no-upgrade --file "$BREWFILE" DOTFILES_CHEZMOI_APPROVED=1 "$CHEZMOI_BOOTSTRAP" mac-air --apply "$HOST_DOCTOR" printf 'Air client setup complete. Verify Studio access manually.\n' diff --git a/hosts/mac-air/doctor.sh b/hosts/mac-air/doctor.sh index 485c42bf..92155303 100755 --- a/hosts/mac-air/doctor.sh +++ b/hosts/mac-air/doctor.sh @@ -6,6 +6,10 @@ REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd -P)" DOTFILES_DIR="${DOTFILES_DIR:-$REPO_DIR}" CHEZMOI_DOCTOR="${DOTFILES_CHEZMOI_DOCTOR:-$DOTFILES_DIR/chezmoi/doctor.sh}" APPLICATIONS_DIR="${DOTFILES_APPLICATIONS_DIR:-/Applications}" +# Reuse read-only Neovim checks with the same Markdown-only parser set as thin. +source "$DOTFILES_DIR/hosts/shared/macos/lib.sh" +NEOVIM_PARSERS=(markdown markdown_inline) +NEOVIM_PARSER_BINARIES=(markdown markdown_inline) FAILURES=0 pass() { printf 'PASS %s\n' "$*"; } fail() { printf 'FAIL %s\n' "$*" >&2; FAILURES=$((FAILURES + 1)); } @@ -13,15 +17,35 @@ fail() { printf 'FAIL %s\n' "$*" >&2; FAILURES=$((FAILURES + 1)); } [[ "$(uname -s)" == Darwin ]] && pass macOS || fail 'macOS required' if HOMEBREW_NO_AUTO_UPDATE=1 brew bundle check --no-upgrade \ --file "$DOTFILES_DIR/hosts/mac-air/Brewfile"; then - pass 'remote client packages' + pass 'thin-Mac client and note packages' else - fail 'remote client packages missing' + fail 'thin-Mac client and note packages missing' fi if "$CHEZMOI_DOCTOR" mac-air; then pass 'Air client configuration' else fail 'Air client configuration' fi +for tool in bookokrat fastfetch herdr hunk lsd marksman nvim rg tree-sitter; do + command -v "$tool" >/dev/null 2>&1 && pass "$tool available" || fail "$tool missing" +done +if nvim --headless -u NONE -i NONE --noplugin \ + "+lua if vim.fn.has('nvim-0.12') ~= 1 then vim.cmd('cquit 1') end" \ + '+qa!' >/dev/null 2>&1; then + pass 'Neovim 0.12+' +else + fail 'Neovim 0.12+ required' +fi +if verify_neovim_parsers_restored; then + pass 'Markdown parsers restored' +else + fail 'Markdown parsers missing or unloadable' +fi +if verify_neovim_config_sandboxed "$DOTFILES_DIR/config/nvim" thin; then + pass 'Thin Neovim starts in isolated data' +else + fail 'Thin Neovim isolated startup' +fi for app in ChatGPT Ghostty Obsidian Tailscale; do [[ -d "$APPLICATIONS_DIR/$app.app" ]] \ && pass "$app installed" || fail "$app missing" diff --git a/hosts/mac-air/tests/client-profile-test.sh b/hosts/mac-air/tests/client-profile-test.sh index f5922c4f..f428eba3 100755 --- a/hosts/mac-air/tests/client-profile-test.sh +++ b/hosts/mac-air/tests/client-profile-test.sh @@ -3,12 +3,13 @@ set -euo pipefail TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" REPO_DIR="$(cd "$TEST_DIR/../../.." && pwd -P)" +REAL_CHEZMOI_BIN="${CHEZMOI_BIN:-$HOME/.local/bin/chezmoi}" TEST_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/dotfiles-air-test.XXXXXX")" trap 'rm -rf "$TEST_ROOT"' EXIT FAKE_BIN="$TEST_ROOT/bin" TEST_HOME="$TEST_ROOT/home" LOG="$TEST_ROOT/commands.log" -mkdir -p "$FAKE_BIN" "$TEST_HOME" +mkdir -p "$FAKE_BIN" "$TEST_HOME/.local/share/nvim/lazy" "$TEST_HOME/.local/share/nvim/site" : > "$LOG" fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; } @@ -30,7 +31,18 @@ cat > "$FAKE_BIN/chezmoi-doctor" <<'FAKE' #!/bin/sh printf 'chezmoi-doctor %s\n' "$*" >> "$COMMAND_LOG" FAKE -for tool in vagrant mise docker colima ssh launchctl softwareupdate uv npm pnpm; do +for tool in bookokrat fastfetch hunk lsd marksman rg tree-sitter; do + printf '#!/bin/sh\nexit 0\n' > "$FAKE_BIN/$tool" +done +cat > "$FAKE_BIN/nvim" <<'FAKE' +#!/bin/sh +printf 'nvim profile=%s %s\n' "${DOTFILES_NVIM_PROFILE:-}" "$*" >> "$COMMAND_LOG" +if [ "${MUTATE_LOCK:-0}" = 1 ]; then + printf 'modified by restore\n' > "$HOME/.config/nvim/lazy-lock.json" +fi +exit "${NVIM_STATUS:-0}" +FAKE +for tool in vagrant mise docker colima ssh launchctl softwareupdate uv npm pnpm herdr; do cat > "$FAKE_BIN/$tool" <<'FAKE' #!/bin/sh printf 'FORBIDDEN %s %s\n' "${0##*/}" "$*" >> "$COMMAND_LOG" @@ -55,11 +67,37 @@ fi ! grep -q '^brew ' "$LOG" || fail 'packages installed before reviewed preview' : > "$LOG" DOTFILES_MAC_AIR_ARRIVED=1 bootstrap --apply >/dev/null -grep -Fq "brew bundle install --no-upgrade --file $REPO_DIR/hosts/mac-air/Brewfile" \ - "$LOG" || fail 'client Brewfile was not installed' grep -Fq 'chezmoi mac-air --apply' "$LOG" || fail 'Air profile was not applied' [[ "$(head -n 1 "$LOG")" == 'chezmoi mac-air --preview' ]] || fail 'preview order' +# Exercise the actual shared package/editor template against disposable state. +[[ -x "$REAL_CHEZMOI_BIN" ]] || fail 'Chezmoi binary required' +mkdir -p "$TEST_HOME/.config/homebrew" "$TEST_HOME/.config/nvim" +ln -s "$REPO_DIR/hosts/mac-air/Brewfile" "$TEST_HOME/.config/homebrew/Brewfile" +cp "$REPO_DIR/config/nvim/lazy-lock.json" "$TEST_HOME/.config/nvim/lazy-lock.json" +"$REAL_CHEZMOI_BIN" --source "$REPO_DIR/chezmoi/source" \ + --config "$REPO_DIR/chezmoi/profiles/mac-air.toml" --destination "$TEST_HOME" \ + --persistent-state "$TEST_ROOT/chezmoi.boltdb" execute-template \ + < "$REPO_DIR/chezmoi/source/run_onchange_after_30-install-thin-tools.sh.tmpl" \ + > "$TEST_ROOT/install-tools.sh" +bash -n "$TEST_ROOT/install-tools.sh" +install_tools() { + HOME="$TEST_HOME" PATH="$FAKE_BIN:$PATH" COMMAND_LOG="$LOG" MUTATE_LOCK=1 \ + bash "$TEST_ROOT/install-tools.sh" +} +install_tools +grep -Fq "brew bundle install --no-upgrade --file $TEST_HOME/.config/homebrew/Brewfile" \ + "$LOG" || fail 'Air Brewfile was not installed' +grep -Fq 'nvim profile=thin --headless +Lazy! restore +qa' "$LOG" \ + || fail 'thin editor was not restored' +grep -Fq "install({'markdown','markdown_inline'})" "$LOG" \ + || fail 'Markdown parsers were not restored' +cmp -s "$REPO_DIR/config/nvim/lazy-lock.json" "$TEST_HOME/.config/nvim/lazy-lock.json" \ + || fail 'plugin restore changed the lockfile' +if NVIM_STATUS=1 install_tools; then fail 'editor failure passed installation'; fi +cmp -s "$REPO_DIR/config/nvim/lazy-lock.json" "$TEST_HOME/.config/nvim/lazy-lock.json" \ + || fail 'failed restore changed the lockfile' + for app in ChatGPT Ghostty Obsidian Tailscale; do mkdir -p "$TEST_ROOT/apps/$app.app"; done doctor() { HOME="$TEST_HOME" PATH="$FAKE_BIN:$PATH" COMMAND_LOG="$LOG" \ @@ -68,14 +106,18 @@ doctor() { bash "$REPO_DIR/hosts/mac-air/doctor.sh" } doctor >/dev/null +if NVIM_STATUS=1 doctor >/dev/null 2>&1; then fail 'broken editor passed doctor'; fi if BREW_STATUS=1 doctor >/dev/null 2>&1; then fail 'missing packages passed doctor'; fi rmdir "$TEST_ROOT/apps/ChatGPT.app" if doctor >/dev/null 2>&1; then fail 'missing client passed doctor'; fi ln -s "$REPO_DIR/hosts/mac-air/.zshrc" "$TEST_HOME/.zshrc" HOME="$TEST_HOME" HOMEBREW_PREFIX="$TEST_ROOT" PATH="$FAKE_BIN:/usr/bin:/bin" \ TERM=xterm-256color COMMAND_LOG="$LOG" /bin/zsh -lic \ - '[[ "$DOTFILES_MAC_PROFILE" == mac-air ]] && (( ! $+functions[uvm-up] ))' + '[[ "$DOTFILES_MAC_PROFILE" == mac-air && "$DOTFILES_NVIM_PROFILE" == thin ]] && + [[ "$EDITOR" == nvim && "$VISUAL" == nvim && "$GIT_EDITOR" == nvim ]] && + [[ "$aliases[hdiff]" == "hunk diff" && "$aliases[v]" == nvim ]] && + (( ! $+functions[uvm-up] && ! $+aliases[hu] && ! $+aliases[u] ))' ! grep -Fq FORBIDDEN "$LOG" || fail 'Air invoked a development or VM tool' -! grep -Eq '^(brew|cask) "(vagrant|vagrant-vmware-utility|vmware-fusion|docker|colima|mise|neovim|postgresql)' \ +! grep -Eq '^(brew|cask) "(vagrant|vagrant-vmware-utility|vmware-fusion|docker|colima|mise|postgresql)' \ "$REPO_DIR/hosts/mac-air/Brewfile" || fail 'Air installs a local development environment' printf 'MacBook Air client profile tests passed.\n' From 7fd689de5e7add4d94be4b3e82165b0adb617aa7 Mon Sep 17 00:00:00 2001 From: arbiter-hd <288758862+arbiter-hd@users.noreply.github.com> Date: Tue, 15 Sep 2026 14:14:41 -0400 Subject: [PATCH 5/5] fix: preserve Ubuntu on resume and strengthen rollout tests --- hosts/mac-air/tests/client-profile-test.sh | 28 +++++++++++++++++++++ hosts/mac-studio/README.md | 6 +++-- hosts/mac-studio/tests/vm-lifecycle-test.sh | 8 +++++- hosts/mac-studio/vm.zsh | 2 +- hosts/shared/macos/tests/bootstrap-test.sh | 8 +++--- 5 files changed, 45 insertions(+), 7 deletions(-) diff --git a/hosts/mac-air/tests/client-profile-test.sh b/hosts/mac-air/tests/client-profile-test.sh index f428eba3..cd19efe8 100755 --- a/hosts/mac-air/tests/client-profile-test.sh +++ b/hosts/mac-air/tests/client-profile-test.sh @@ -120,4 +120,32 @@ HOME="$TEST_HOME" HOMEBREW_PREFIX="$TEST_ROOT" PATH="$FAKE_BIN:/usr/bin:/bin" \ ! grep -Fq FORBIDDEN "$LOG" || fail 'Air invoked a development or VM tool' ! grep -Eq '^(brew|cask) "(vagrant|vagrant-vmware-utility|vmware-fusion|docker|colima|mise|postgresql)' \ "$REPO_DIR/hosts/mac-air/Brewfile" || fail 'Air installs a local development environment' + +# Run the actual bootstrap/Chezmoi path from an empty home, not just its +# rendered installer. Package managers and editor execution remain stubbed. +fresh_home="$TEST_ROOT/fresh-home" +fresh_log="$TEST_ROOT/fresh.log" +mkdir -p "$fresh_home" +: > "$fresh_log" +fresh_bootstrap() { + HOME="$fresh_home" PATH="$FAKE_BIN:$PATH" COMMAND_LOG="$fresh_log" \ + DOTFILES_CHEZMOI_TEST=1 DOTFILES_MAC_AIR_ARRIVED=1 \ + CHEZMOI_BIN="$REAL_CHEZMOI_BIN" DOTFILES_AIR_DOCTOR=/usr/bin/true \ + bash "$REPO_DIR/hosts/mac-air/bootstrap.sh" --apply \ + > "$TEST_ROOT/fresh-apply.log" +} +fresh_bootstrap +[[ "$(readlink "$fresh_home/.config/nvim")" == "$REPO_DIR/config/nvim" ]] \ + || fail 'fresh Air did not receive the note editor configuration' +[[ "$(readlink "$fresh_home/.config/homebrew/Brewfile")" == "$REPO_DIR/hosts/mac-air/Brewfile" ]] \ + || fail 'fresh Air selected the wrong packages' +[[ -d "$fresh_home/.config/fastfetch" && ! -L "$fresh_home/.config/fastfetch" ]] \ + || fail 'fresh Air did not prepare its managed parent directory' +grep -Fq 'rollback command:' "$TEST_ROOT/fresh-apply.log" || fail 'fresh Air has no rollback' +[[ "$(grep -c '^brew bundle install ' "$fresh_log")" == 1 ]] \ + || fail 'fresh Air package installation did not run exactly once' +fresh_bootstrap +[[ "$(grep -c '^brew bundle install ' "$fresh_log")" == 1 ]] \ + || fail 'unchanged Air apply repeated package installation' +! grep -Fq FORBIDDEN "$fresh_log" || fail 'fresh Air invoked a VM or development tool' printf 'MacBook Air client profile tests passed.\n' diff --git a/hosts/mac-studio/README.md b/hosts/mac-studio/README.md index 52d5e061..30d087b2 100644 --- a/hosts/mac-studio/README.md +++ b/hosts/mac-studio/README.md @@ -104,8 +104,10 @@ overwrite keys. The shell exposes `uvm-status`, `uvm-up`, `uvm-stop`, `uvm-suspend`, `uvm-resume` and `uvm-ip`. Loading the shell does not invoke Vagrant. Start/resume refuse a -missing restored VM; `uvm-up` disables automatic provisioning. There is no -Studio destroy shortcut. These helpers are for deliberate manual use only. +missing restored VM; both disable automatic provisioning. Vagrant can otherwise +reprovision a powered-off guest on resume when restored metadata contains a +missing or outdated provisioning marker. There is no Studio destroy shortcut. +These helpers are for deliberate manual use only. If Ubuntu is later activated, manually configure its SSH routes using `hosts/mac-studio/ssh/ubuntu-vagrant.conf` after verifying host fingerprints. diff --git a/hosts/mac-studio/tests/vm-lifecycle-test.sh b/hosts/mac-studio/tests/vm-lifecycle-test.sh index e222b8b9..e8ad9e5b 100644 --- a/hosts/mac-studio/tests/vm-lifecycle-test.sh +++ b/hosts/mac-studio/tests/vm-lifecycle-test.sh @@ -24,6 +24,11 @@ if [[ "$*" == 'status --machine-readable' ]]; then [[ "${VM_STATE:-poweroff}" != error ]] || exit 1 printf '1,default,state,%s\n' "${VM_STATE:-poweroff}" fi +# The VMware provider can provision a powered-off restored guest on resume +# when its action_provision sentinel is missing or records its old machine ID. +if [[ "$1" == resume && "$*" != 'resume --no-provision' ]]; then + printf 'UNSAFE automatic provisioning on resume\n' >> "${VAGRANT_TEST_LOG:?}" +fi FAKE chmod +x "$FAKE_BIN/vagrant" @@ -54,12 +59,13 @@ done : > "$VAGRANT_LOG" run_shell 'uvm-up; uvm-stop; uvm-suspend; uvm-resume; uvm-status; uvm-ip' -for args in 'up --no-provision' halt suspend resume status 'ssh -c hostname -I'; do +for args in 'up --no-provision' halt suspend 'resume --no-provision' status 'ssh -c hostname -I'; do grep -Fq "args=$args" "$VAGRANT_LOG" || fail "missing command: $args" done grep -Fq "cwd=$PROJECT provider=vmware_desktop gui=1 args=up --no-provision" \ "$VAGRANT_LOG" || fail 'start did not preserve provider, project and GUI settings' ! grep -Fq 'args=provision' "$VAGRANT_LOG" || fail 'automatic provisioning' +! grep -Fq 'UNSAFE' "$VAGRANT_LOG" || fail 'resume allowed automatic provisioning' ! grep -Fq destroy "$VAGRANT_LOG" || fail 'unexpected deletion' run_shell '(( ! $+functions[uvm-destroy] ))' || fail 'destroy shortcut remains' printf 'Mac Studio dormant VM lifecycle tests passed.\n' diff --git a/hosts/mac-studio/vm.zsh b/hosts/mac-studio/vm.zsh index 436f792e..f86609b4 100644 --- a/hosts/mac-studio/vm.zsh +++ b/hosts/mac-studio/vm.zsh @@ -53,7 +53,7 @@ uvm-suspend() { uvm-resume() { _require_preserved_ubuntu || return - _ubuntu_vagrant resume + _ubuntu_vagrant resume --no-provision } uvm-status() { diff --git a/hosts/shared/macos/tests/bootstrap-test.sh b/hosts/shared/macos/tests/bootstrap-test.sh index d506998b..a1bc927c 100755 --- a/hosts/shared/macos/tests/bootstrap-test.sh +++ b/hosts/shared/macos/tests/bootstrap-test.sh @@ -1028,15 +1028,17 @@ EOF assert_file "$rosetta_marker" assert_contains "$log" 'softwareupdate --install-rosetta --agree-to-license' - : > "$log" - HOME="$home_dir" PATH="$fake_bin:$PATH" COMMAND_LOG="$log" \ + local reapply_log="$root/reapply.log" + : > "$reapply_log" + HOME="$home_dir" PATH="$fake_bin:$PATH" COMMAND_LOG="$reapply_log" \ DOTFILES_DIR="$REPO_DIR" DOTFILES_ALLOW_DIRTY=1 \ DOTFILES_ALLOW_NONCANONICAL=1 DOTFILES_MAC_DOCTOR=/usr/bin/true \ DOTFILES_MAC_STUDIO_ARRIVED=1 \ DOTFILES_PKGUTIL="$fake_bin/pkgutil" ROSETTA_MARKER="$rosetta_marker" \ DOTFILES_SOFTWAREUPDATE="$fake_bin/softwareupdate" DOTFILES_SUDO="$fake_bin/sudo" \ "$MAC_BOOTSTRAP_DIR/bootstrap.sh" --profile mac-studio --apply >/dev/null - assert_not_contains "$log" 'softwareupdate' + assert_not_contains "$reapply_log" 'softwareupdate' + cat "$reapply_log" >> "$log" assert_not_contains "$log" 'FORBIDDEN' assert_not_contains "$log" 'brew services'