diff --git a/AGENTS.md b/AGENTS.md index aa7c96d1..e0f8e275 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,13 +16,15 @@ constraints, and agent workflow policy here. ## Profile Routing -| Target | Source of truth | -| ------------------------------------------ | ------------------------------------ | -| Thin personal MacBook (`mac-thin`) | `hosts/mac-thin/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 @@ -61,6 +63,15 @@ 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. +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 - Mac package policy lives only in `hosts/shared/macos/Brewfile` and the diff --git a/README.md b/README.md index 6342ea84..25ebbfda 100644 --- a/README.md +++ b/README.md @@ -4,13 +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 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: @@ -33,6 +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 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 @@ -59,6 +63,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 +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/` 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 662d985d..8b8b4120 100644 --- a/chezmoi/README.md +++ b/chezmoi/README.md @@ -9,13 +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-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 @@ -27,7 +29,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-air`, `mac-pro`, `mac-studio`, `mac-mini`, or `mac-work` as +needed. After review and host approval: @@ -79,5 +82,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 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 2e474520..bfb1107a 100755 --- a/chezmoi/apply.sh +++ b/chezmoi/apply.sh @@ -9,6 +9,10 @@ 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-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 ]] \ @@ -34,9 +38,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..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-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-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-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 37e2f77f..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-mini|mac-work) ;; - *) die "profile must be ubuntu, mac-thin, mac-pro, 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-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 a2eb7708..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 @@ -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-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 new file mode 100644 index 00000000..081b4e1f --- /dev/null +++ b/chezmoi/profiles/mac-air.paths @@ -0,0 +1,13 @@ +.zshrc|hosts/mac-air/.zshrc +.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/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/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..aa365b5b 100644 --- a/chezmoi/source/.chezmoiignore.tmpl +++ b/chezmoi/source/.chezmoiignore.tmpl @@ -54,9 +54,24 @@ 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 20-install-ubuntu-tools.sh 30-install-thin-tools.sh {{- end }} +{{- if eq .profile "mac-air" }} +.config/btop +.config/mise +.hermes +20-install-ubuntu-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/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/source/symlink_dot_zshrc.tmpl b/chezmoi/source/symlink_dot_zshrc.tmpl index 631a0fa9..7a05b668 100644 --- a/chezmoi/source/symlink_dot_zshrc.tmpl +++ b/chezmoi/source/symlink_dot_zshrc.tmpl @@ -3,8 +3,12 @@ {{ 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" -}} +{{ 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..9d8596b4 100755 --- a/chezmoi/tests/production-test.sh +++ b/chezmoi/tests/production-test.sh @@ -178,16 +178,16 @@ 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-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" 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 + if [[ "$profile" == mac-thin || "$profile" == mac-air ]]; then chmod 700 "$home_dir/.config" "$home_dir/.config/fastfetch" fi common=( @@ -206,8 +206,9 @@ for profile in ubuntu mac-thin mac-pro 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-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 ;; mac-work) printf '%s\n' 10-configure-git.sh 40-install-work-tools.sh ;; esac > "$case_dir/$profile.expected-scripts" @@ -235,7 +236,7 @@ for profile in ubuntu mac-thin mac-pro 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 @@ -254,6 +255,23 @@ for profile in ubuntu mac-thin mac-pro mac-mini mac-work; do bash "$CHEZMOI_DIR/doctor.sh" "$profile" >/dev/null done +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" rollback_backups="$case_dir/rollback/backups" diff --git a/hosts/mac-air/.zshrc b/hosts/mac-air/.zshrc new file mode 100644 index 00000000..7dd7535d --- /dev/null +++ b/hosts/mac-air/.zshrc @@ -0,0 +1,36 @@ +# Lightweight remote-access shell. No local project runtimes or VM helpers. +export DOTFILES_MAC_PROFILE=mac-air +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 +setopt append_history hist_ignore_all_dups share_history + +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)" + 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..08611908 --- /dev/null +++ b/hosts/mac-air/Brewfile @@ -0,0 +1,41 @@ +# 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" + +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 new file mode 100644 index 00000000..96a166f5 --- /dev/null +++ b/hosts/mac-air/README.md @@ -0,0 +1,67 @@ +# Future MacBook Air + +`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. 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 + +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. 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 + +- 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..5bd3db22 --- /dev/null +++ b/hosts/mac-air/bootstrap.sh @@ -0,0 +1,47 @@ +#!/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 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 + 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 +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..92155303 --- /dev/null +++ b/hosts/mac-air/doctor.sh @@ -0,0 +1,54 @@ +#!/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}" +# 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)); } + +[[ "$(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 'thin-Mac client and note packages' +else + 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" +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..cd19efe8 --- /dev/null +++ b/hosts/mac-air/tests/client-profile-test.sh @@ -0,0 +1,151 @@ +#!/usr/bin/env bash +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/.local/share/nvim/lazy" "$TEST_HOME/.local/share/nvim/site" +: > "$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 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" +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 '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" \ + 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 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 && "$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|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-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 new file mode 100644 index 00000000..b656ccd5 --- /dev/null +++ b/hosts/mac-studio/.zshrc @@ -0,0 +1,32 @@ +# Mac Studio primary native development workstation. + +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}" + "${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 + +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..c5161a5e --- /dev/null +++ b/hosts/mac-studio/Brewfile @@ -0,0 +1,46 @@ +# Primary personal-Mac applications. Shared development tools live in +# ../shared/macos/Brewfile. + +brew "colima" +brew "coreutils" +brew "docker" +brew "docker-buildx" +brew "docker-compose" +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" +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 "visual-studio-code" +vscode "openai.chatgpt" +cask "vlc" +cask "zoom" diff --git a/hosts/mac-studio/README.md b/hosts/mac-studio/README.md new file mode 100644 index 00000000..30d087b2 --- /dev/null +++ b/hosts/mac-studio/README.md @@ -0,0 +1,133 @@ +# Mac Studio + +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 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. + +## Target Roles + +| 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 | + +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 + +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 +``` + +After hardware arrival and approval of the reviewed checks: + +```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 gate; set it only on Studio. Bootstrap installs the +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. + +## Native Development + +Homebrew and the shared mise configuration provide the native toolchain. +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. + +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 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. + +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, 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 +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. +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/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..e8ad9e5b --- /dev/null +++ b/hosts/mac-studio/tests/vm-lifecycle-test.sh @@ -0,0 +1,71 @@ +#!/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" +PROJECT="$TEST_ROOT/ubuntu" +VAGRANT_LOG="$TEST_ROOT/vagrant.log" +mkdir -p "$FAKE_BIN" "$TEST_HOME" "$PROJECT" +PROJECT="$(cd "$PROJECT" && pwd -P)" +: > "$PROJECT/Vagrantfile" +: > "$VAGRANT_LOG" +fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; } + +cat > "$FAKE_BIN/vagrant" <<'FAKE' +#!/usr/bin/env bash +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 +# 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" + +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' + +for action in uvm-up uvm-resume; do + if run_shell "$action" >/dev/null; then fail "$action allowed a missing VM"; fi +done +[[ ! -s "$VAGRANT_LOG" ]] || fail 'missing metadata reached Vagrant' + +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' + +: > "$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 --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 new file mode 100644 index 00000000..f86609b4 --- /dev/null +++ b/hosts/mac-studio/vm.zsh @@ -0,0 +1,65 @@ +# Mac Studio Vagrant shortcuts. Ubuntu is preserved for explicitly requested manual use. + +_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 "$@" + ) +} + +_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() { + _require_preserved_ubuntu || return + UBUNTU_VM_GUI=1 _ubuntu_vagrant up --no-provision +} + +uvm-stop() { + _ubuntu_vagrant halt +} + +uvm-suspend() { + _ubuntu_vagrant suspend +} + +uvm-resume() { + _require_preserved_ubuntu || return + _ubuntu_vagrant resume --no-provision +} + +uvm-status() { + _ubuntu_vagrant status +} + +uvm-ip() { + _ubuntu_vagrant ssh -c 'hostname -I' +} diff --git a/hosts/shared/macos/README.md b/hosts/shared/macos/README.md index 7f6560c8..3c6f8bc7 100644 --- a/hosts/shared/macos/README.md +++ b/hosts/shared/macos/README.md @@ -1,11 +1,12 @@ # 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-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. @@ -52,6 +53,17 @@ ancestor manifest. Secrets, auth state, Herdr sessions, Hunk state, tmux plugins, services, Docker 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. 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. + ## 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..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" @@ -16,7 +19,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 @@ -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" @@ -75,6 +83,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 +123,12 @@ 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 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 write_zprofile_block "$HOME/.zprofile" "$MISE_FRAGMENT" "$STAMP" 1 if [[ -x "${CHEZMOI_BIN:-$HOME/.local/bin/chezmoi}" ]]; then @@ -156,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 3eba0c48..c31348bb 100755 --- a/hosts/shared/macos/doctor.sh +++ b/hosts/shared/macos/doctor.sh @@ -9,12 +9,14 @@ 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}" # 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() { @@ -83,6 +85,23 @@ 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 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" + else + fail "$app_name missing" + fi + done + + printf 'SKIP Ubuntu runtime and SSH checks (manual use only)\n' +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 6c449e22..a1bc927c 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,23 @@ 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"' + 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_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/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 +970,91 @@ 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" + mkdir -p "$home_dir/Developer" + ln -s "$REPO_DIR" "$home_dir/Developer/dotfiles-hd" + make_fake_toolchain "$fake_bin" + : > "$log" + + # 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:?}" +exit 99 +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 \ + "$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" 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' + 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 "$reapply_log" 'softwareupdate' + cat "$reapply_log" >> "$log" + + 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" + 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' + +} + test_xdg_bin_home() { local root="$TMP_ROOT/xdg-bin-home" local home_dir="$root/home" @@ -971,12 +1074,24 @@ 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" \ 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() { @@ -1678,6 +1793,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..47ad8f11 100755 --- a/hosts/tests/run.sh +++ b/hosts/tests/run.sh @@ -8,6 +8,8 @@ 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-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 88cd0ac1..2ba36381 100644 --- a/hosts/ubuntu-dev/GUIDE.md +++ b/hosts/ubuntu-dev/GUIDE.md @@ -5,7 +5,10 @@ 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, 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 @@ -89,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 15234e7e..7dbeab6d 100644 --- a/hosts/ubuntu-dev/README.md +++ b/hosts/ubuntu-dev/README.md @@ -1,7 +1,9 @@ # 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 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 @@ -14,6 +16,10 @@ Prerequisites on the thin Mac: - The tailnet policy defines `tag:ubuntu-dev` and lets `hd719@github` connect to that tag as the `hamel` user. +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: @@ -158,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.