Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

mac-native: bootstrap + plan + Darwin guard for config.fish

- scripts/mac-native-bootstrap.sh: idempotent 13-phase bootstrap that
installs brew + fish + fnm + node + mkcert + Claude Code, sets up
/etc/synthetic.conf for /workspaces, a scoped NOPASSWD sudoers file,
fish as login shell, SSL certs, and smoke-tests ac-site. Uses a GUI
askpass helper to avoid sudo friction in non-tty shells.
- .devcontainer/config.fish: skip the Codespaces-style ~/aesthetic-computer
symlink block on Darwin (macOS uses the reverse direction via synthetic.conf,
so the existing block emits a harmless but noisy "File exists" error on
every shell start).
- plans/MAC-NATIVE-DEVENV-PLAN.md: updated with what actually worked —
synthetic.conf (not sudo mkdir), askpass + scoped NOPASSWD for sudo,
notes on the ac-session MongoDB chat-load hang, current DoD checklist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+316 -41
+4 -2
.devcontainer/config.fish
··· 23 23 end 24 24 25 25 # Symlink a VSCode workspace as needed. 26 - 27 - if test -d /workspaces/aesthetic-computer 26 + # On macOS the relationship is reversed: /workspaces is a synthetic.conf symlink 27 + # to the user's home, so the repo already lives at ~/aesthetic-computer and the 28 + # codespaces-style symlink would loop/fail. Skip on Darwin. 29 + if test -d /workspaces/aesthetic-computer; and test (uname) != Darwin 28 30 if test ! -L ~/aesthetic-computer 29 31 echo "Symlinking /workspaces/aesthetic-computer to ~" 30 32 ln -s /workspaces/aesthetic-computer ~
+91 -39
plans/MAC-NATIVE-DEVENV-PLAN.md
··· 92 92 93 93 --- 94 94 95 - ## 2. Path portability — `/workspaces/aesthetic-computer` symlink 95 + ## 2. Path portability — `/workspaces` via `synthetic.conf` 96 96 97 97 [`.devcontainer/config.fish`](../.devcontainer/config.fish) hardcodes 98 98 `/workspaces/aesthetic-computer` in **48 places** (e.g. `ac-tv`, `ac-keeps`, 99 - `ac-kidlisp`, `ac-login`, `ac-pack`). Rather than fork the file, create a 100 - system symlink so both paths resolve to the same tree: 99 + `ac-kidlisp`, `ac-login`, `ac-pack`). macOS has had a read-only root volume 100 + since Catalina, so `sudo mkdir /workspaces` fails with `Read-only file system`. 101 + The sanctioned fix is `/etc/synthetic.conf`: 101 102 102 103 ```sh 103 - sudo mkdir -p /workspaces 104 - sudo ln -s "$HOME/aesthetic-computer" /workspaces/aesthetic-computer 104 + printf 'workspaces\t/Users/%s\n' "$USER" | sudo tee /etc/synthetic.conf 105 + sudo /System/Library/Filesystems/apfs.fs/Contents/Resources/apfs.util -t 105 106 ``` 106 107 107 - The config at [config.fish:27-32](../.devcontainer/config.fish:27) already 108 - checks for `/workspaces/aesthetic-computer` and will symlink the reverse 109 - direction (`~/aesthetic-computer` → `/workspaces/...`); since the home path 110 - already exists as a real directory it's effectively a no-op, and all the 111 - hardcoded paths resolve via the symlink we just created. 108 + This creates `/workspaces` as a synthetic symlink → `/Users/jas`, so 109 + `/workspaces/aesthetic-computer` resolves automatically. No reboot needed. 110 + Survives reboots. 112 111 113 - This keeps future upstream edits to `config.fish` working with zero diff. 112 + The check at [config.fish:27-32](../.devcontainer/config.fish:27) tries to 113 + symlink `/workspaces/aesthetic-computer` → `~` the other direction and fails 114 + with `File exists` on Mac. The plan now includes a one-line upstream fix 115 + (`and test (uname) != Darwin`) so the block is skipped on macOS. 114 116 115 117 --- 116 118 ··· 288 290 ac-session 289 291 ``` 290 292 291 - Should bind `https://localhost:8889`. Confirm with `curl -fsSI 292 - https://localhost:8889/healthz` (or whatever the current health path is — read 293 - `session-server/session.mjs` to be sure). 293 + Should bind `https://localhost:8889`. 294 294 295 - `npm run aesthetic` launches *both* site + session + stripe + url concurrently 296 - via `concurrently`, which is what the devcontainer uses by default. It should 297 - also work on Mac once step 1 installs the `concurrently` / `stripe` globals. 295 + **Known slow path on lightweight laptops:** [`chat-manager.mjs:141-156`](../session-server/chat-manager.mjs:141) runs a MongoDB `$unionWith` aggregate across `chat-system` + `logs` collections at startup, and 296 + [`session.mjs:135`](../session-server/session.mjs:135) awaits it before 297 + `server.listen()`. On this Mac the query takes >90s (possibly never 298 + completes) against the production MongoDB at `silo.aesthetic.computer:27017` 299 + — likely a network/replica-set reachability difference vs. the devcontainer. 300 + 301 + Workarounds if you need session locally: 302 + 1. Add a dev flag to `chat-manager.mjs` that skips the historical load when 303 + `NODE_ENV=development` — messages will only appear for the current session. 304 + 2. Point `MONGODB_CONNECTION_STRING` at a local MongoDB with a `chat-system` 305 + collection containing a small number of documents. 306 + 3. Use the production session-server (jamsocket) and only run lith locally. 307 + 308 + `npm run aesthetic` launches site + session + stripe + url concurrently. Until 309 + the chat-load issue is resolved, prefer `ac-site` standalone. 298 310 299 311 --- 300 312 ··· 321 333 322 334 ## 10. Reproducibility — roll-up script 323 335 324 - Once verified interactively, commit a `scripts/mac-native-bootstrap.fish` 325 - (or extend [`dotfiles/install.sh`](../dotfiles/install.sh) with a `macos` 326 - branch) that performs steps 1–6 idempotently so the next fresh Mac can run: 336 + Committed: [`scripts/mac-native-bootstrap.sh`](../scripts/mac-native-bootstrap.sh). 337 + Idempotent. Run on a fresh Mac after `git clone`ing the repo and the vault: 327 338 328 339 ```sh 329 - curl -fsSL https://raw.githubusercontent.com/.../mac-native-bootstrap.fish | fish 340 + cd ~/aesthetic-computer 341 + ./scripts/mac-native-bootstrap.sh 330 342 ``` 331 343 332 - Suggested layout: 344 + What it does (13 phases, ~4 min on a fresh machine, ~30s on a re-run): 333 345 334 - ``` 335 - scripts/ 336 - mac-native-bootstrap.fish # brew + fnm + npm globals 337 - mac-native-link-workspaces.sh # /workspaces symlink (needs sudo) 338 - mac-native-fish-config.fish # writes ~/.config/fish/config.fish 339 - ``` 346 + 1. Writes a GUI askpass helper at `/tmp/ac-askpass.sh` and primes sudo 347 + 2. Installs Homebrew if missing 348 + 3. Installs brew formulas (fish, fnm, mkcert, jq, ripgrep, ffmpeg, caddy, 349 + stripe, doctl, gnupg, pinentry-mac, …) 350 + 4. Creates `/etc/synthetic.conf` and triggers `apfs.util -t` for `/workspaces` 351 + 5. Installs the scoped NOPASSWD sudoers file 352 + 6. Installs node LTS-jod + 20.5.0 via fnm 353 + 7. npm globals: `@anthropic-ai/claude-code`, `netlify-cli`, `concurrently`, 354 + `kill-port`, … + native `claude` binary from `claude.ai/install.sh` 355 + 8. Adds `/opt/homebrew/bin/fish` to `/etc/shells` and `dscl`-changes the 356 + default shell 357 + 9. Writes `~/.config/fish/config.fish` that sources the repo's 358 + `.devcontainer/config.fish` 359 + 10. Configures `gpg-agent` for `pinentry-mac` 360 + 11. Runs `mkcert -install` and generates `ssl-dev/localhost.pem` 361 + 12. Symlinks `aesthetic-computer-vault/session-server/.env` → 362 + `session-server/.env` (skipped if vault is locked) 363 + 13. Smoke-tests `https://localhost:8888/` end-to-end and kills the test proc 340 364 341 - Keep each step a separate function so reruns are cheap. 365 + Vault unlock is **not** part of the bootstrap — run 366 + `fish aesthetic-computer-vault/vault-tool.fish unlock` separately (see memory 367 + `reference_vault_unlock.md`). 342 368 343 369 --- 344 370 ··· 366 392 367 393 ## 12. Definition of done 368 394 369 - - [ ] `fish` is the login shell; new terminal tab has `ac-help` output ≥ 80 items 370 - - [ ] `ac-site` brings up `https://localhost:8888` with a trusted cert (no 371 - `-k` needed on `curl`) 372 - - [ ] `ac-session` brings up `https://localhost:8889` 373 - - [ ] `npm test` from repo root passes (or fails only on known 374 - vault/Linux-only specs) 375 - - [ ] `git status` in both `aesthetic-computer` and 376 - `aesthetic-computer-vault` is clean (no accidental `.env` commits) 377 - - [ ] Bootstrap script in `scripts/` reproduces the whole setup on a fresh 378 - user account 395 + Status as of 2026-04-17 on jas's 14" MBP (arm64, macOS 26.4): 396 + 397 + - [x] `fish` is the login shell; new terminal tab has **214 `ac-*` commands** 398 + - [x] `ac-site` brings up `https://localhost:8888` with a **trusted cert** 399 + (no `-k` on `curl`); `/`, `/prompt`, `/api/version` all return 200 400 + - [ ] `ac-session` brings up `https://localhost:8889` — **boots but hangs** 401 + on chat-history MongoDB load (see §8) 402 + - [ ] `npm test` from repo root — not yet exercised 403 + - [x] `git status` clean in vault (no accidental plaintext commits) 404 + - [x] [`scripts/mac-native-bootstrap.sh`](../scripts/mac-native-bootstrap.sh) 405 + reproduces the setup 406 + 407 + ## 13. Sudo on macOS — friction and workarounds 408 + 409 + Claude Code (and any non-interactive shell) can't read a sudo password 410 + because it has no tty. Three tiers of mitigation, used in combination: 411 + 412 + 1. **GUI askpass helper** — `/tmp/ac-askpass.sh` pops a native 413 + `osascript display dialog` when `sudo -A` runs. Works for all `sudo -A` 414 + invocations but NOT for third-party tools that call `sudo` directly 415 + (e.g. Homebrew's installer, `mkcert -install`). 416 + 2. **Scoped NOPASSWD sudoers** at `/etc/sudoers.d/claude-ac-setup` covers the 417 + exact commands this workflow invokes via third-party tooling: 418 + `security` (for mkcert), `mkcert`, `tee -a /etc/shells`. Everything else 419 + still prompts. 420 + 3. **One-off priming** — running `sudo -v` in a separate Terminal before 421 + kicking off Homebrew; the timestamp is per-tty so this only helps when 422 + you're running the installer yourself. 423 + 424 + Explicitly **not** enabled: 425 + 426 + - Global sudo timestamp (`Defaults timestamp_type=global`) 427 + - Long `timestamp_timeout` extensions 428 + - Blanket NOPASSWD for the user 429 + 430 + These were considered and rejected as too broad.
+221
scripts/mac-native-bootstrap.sh
··· 1 + #!/bin/bash 2 + # Bootstrap a lightweight Mac (Apple Silicon or Intel) to run aesthetic-computer 3 + # natively — outside the devcontainer — with all `ac-*` fish commands available. 4 + # 5 + # Matches plans/MAC-NATIVE-DEVENV-PLAN.md. Safe to re-run (idempotent). 6 + # Prereqs: macOS 11+, Xcode CLT (git), the aesthetic-computer repo checked 7 + # out at $HOME/aesthetic-computer, the vault at $HOME/aesthetic-computer/aesthetic-computer-vault. 8 + # 9 + # This script does NOT unlock the vault — do that manually with 10 + # `fish aesthetic-computer-vault/vault-tool.fish unlock` before or after. 11 + 12 + set -euo pipefail 13 + 14 + AC_ROOT="$HOME/aesthetic-computer" 15 + VAULT="$AC_ROOT/aesthetic-computer-vault" 16 + ASKPASS="/tmp/ac-askpass.sh" 17 + SUDOERS_FILE="/etc/sudoers.d/claude-ac-setup" 18 + 19 + step() { printf "\n\033[1;34m▶ %s\033[0m\n" "$*"; } 20 + ok() { printf " \033[1;32m✓\033[0m %s\n" "$*"; } 21 + warn() { printf " \033[1;33m!\033[0m %s\n" "$*"; } 22 + die() { printf " \033[1;31m✗\033[0m %s\n" "$*"; exit 1; } 23 + 24 + [[ "$(uname)" == "Darwin" ]] || die "this script is macOS-only" 25 + [[ -d "$AC_ROOT" ]] || die "aesthetic-computer repo not found at $AC_ROOT" 26 + 27 + # ----------------------------------------------------------------------------- 28 + step "1. GUI askpass helper for sudo -A" 29 + # ----------------------------------------------------------------------------- 30 + cat > "$ASKPASS" <<'EOF' 31 + #!/bin/bash 32 + /usr/bin/osascript -e 'display dialog "Bootstrap needs sudo — enter your password:" default answer "" with hidden answer with icon caution' -e 'text returned of result' 2>/dev/null 33 + EOF 34 + chmod +x "$ASKPASS" 35 + export SUDO_ASKPASS="$ASKPASS" 36 + sudo -A -v 37 + ok "sudo primed via askpass" 38 + 39 + # ----------------------------------------------------------------------------- 40 + step "2. Homebrew" 41 + # ----------------------------------------------------------------------------- 42 + if ! command -v brew >/dev/null 2>&1 && ! [[ -x /opt/homebrew/bin/brew ]]; then 43 + NONINTERACTIVE=1 /bin/bash -c \ 44 + "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 45 + fi 46 + eval "$(/opt/homebrew/bin/brew shellenv)" 47 + ok "brew at $(which brew)" 48 + 49 + # ----------------------------------------------------------------------------- 50 + step "3. Brew formulas" 51 + # ----------------------------------------------------------------------------- 52 + # Core — needed for fish, node, mkcert, site tooling 53 + brew install --quiet fish fnm mkcert nss jq ripgrep bat gh tree \ 54 + coreutils gnu-sed wget nmap ffmpeg \ 55 + caddy ngrok/ngrok/ngrok redis \ 56 + stripe/stripe-cli/stripe doctl awscli \ 57 + gnupg pinentry-mac 2>&1 | tail -3 58 + ok "core brew formulas installed" 59 + 60 + # ----------------------------------------------------------------------------- 61 + step "4. /workspaces → /Users/$USER via synthetic.conf" 62 + # ----------------------------------------------------------------------------- 63 + if [[ -L /workspaces ]] && [[ "$(readlink /workspaces)" == "/Users/$USER" ]]; then 64 + ok "/workspaces already symlinked" 65 + else 66 + # macOS SIP prevents writes to /, but synthetic.conf is the sanctioned way. 67 + printf 'workspaces\t/Users/%s\n' "$USER" | sudo -A tee /etc/synthetic.conf >/dev/null 68 + sudo -A chmod 644 /etc/synthetic.conf 69 + sudo -A /System/Library/Filesystems/apfs.fs/Contents/Resources/apfs.util -t || \ 70 + warn "apfs.util trigger failed — a reboot will also apply synthetic.conf" 71 + if [[ -L /workspaces ]]; then 72 + ok "/workspaces → $(readlink /workspaces)" 73 + else 74 + warn "/workspaces not yet present; reboot to activate" 75 + fi 76 + fi 77 + 78 + # ----------------------------------------------------------------------------- 79 + step "5. Scoped NOPASSWD sudoers" 80 + # ----------------------------------------------------------------------------- 81 + SUDOERS_TMP=$(mktemp) 82 + cat > "$SUDOERS_TMP" <<EOF 83 + # Claude Code / aesthetic-computer native dev — narrow NOPASSWD for recurring 84 + # ops. Review with \`sudo visudo -c\`. 85 + Cmnd_Alias AC_DEV_SETUP = \\ 86 + /usr/bin/tee -a /etc/shells, \\ 87 + /usr/bin/security, \\ 88 + /opt/homebrew/bin/mkcert 89 + 90 + $USER ALL=(root) NOPASSWD: AC_DEV_SETUP 91 + EOF 92 + sudo -A visudo -cf "$SUDOERS_TMP" >/dev/null 93 + sudo -A install -o root -g wheel -m 0440 "$SUDOERS_TMP" "$SUDOERS_FILE" 94 + rm -f "$SUDOERS_TMP" 95 + ok "sudoers file installed at $SUDOERS_FILE" 96 + 97 + # ----------------------------------------------------------------------------- 98 + step "6. fnm + Node (lts-jod & 20.5.0)" 99 + # ----------------------------------------------------------------------------- 100 + eval "$(fnm env --shell bash)" 101 + fnm install lts-jod 102 + fnm install 20.5.0 103 + fnm default lts-jod 104 + fnm use lts-jod 105 + ok "node $(node --version) via fnm ($(fnm current))" 106 + 107 + # ----------------------------------------------------------------------------- 108 + step "7. Global npm CLIs (incl. Claude Code)" 109 + # ----------------------------------------------------------------------------- 110 + npm i -g --silent \ 111 + @anthropic-ai/claude-code \ 112 + @devcontainers/cli \ 113 + netlify-cli \ 114 + prettier typescript typescript-language-server \ 115 + concurrently kill-port http-server npm-check-updates 2>&1 | tail -1 116 + ok "npm globals installed" 117 + 118 + # Native Claude Code binary (matches Dockerfile:223) 119 + if ! [[ -x "$HOME/.local/bin/claude" ]]; then 120 + curl -fsSL https://claude.ai/install.sh | bash >/dev/null 121 + fi 122 + ok "claude native: $("$HOME/.local/bin/claude" --version | head -1)" 123 + 124 + # ----------------------------------------------------------------------------- 125 + step "8. fish as default login shell" 126 + # ----------------------------------------------------------------------------- 127 + if ! grep -q "/opt/homebrew/bin/fish" /etc/shells; then 128 + echo "/opt/homebrew/bin/fish" | sudo -n tee -a /etc/shells >/dev/null 129 + fi 130 + CURRENT_SHELL=$(dscl . -read "/Users/$USER" UserShell | awk '{print $2}') 131 + if [[ "$CURRENT_SHELL" != "/opt/homebrew/bin/fish" ]]; then 132 + sudo -A /usr/bin/dscl . -change "/Users/$USER" UserShell "$CURRENT_SHELL" /opt/homebrew/bin/fish 133 + fi 134 + ok "login shell: $(dscl . -read /Users/$USER UserShell | awk '{print $2}')" 135 + 136 + # ----------------------------------------------------------------------------- 137 + step "9. ~/.config/fish/config.fish" 138 + # ----------------------------------------------------------------------------- 139 + mkdir -p "$HOME/.config/fish/conf.d" "$HOME/.config/fish/functions" 140 + if ! [[ -f "$HOME/.config/fish/config.fish" ]] || \ 141 + ! grep -q "$AC_ROOT/.devcontainer/config.fish" "$HOME/.config/fish/config.fish"; then 142 + cat > "$HOME/.config/fish/config.fish" <<FISHCFG 143 + # ~/.config/fish/config.fish — native Mac AC dev env 144 + # Generated by scripts/mac-native-bootstrap.sh 145 + set -gx PATH /opt/homebrew/bin /opt/homebrew/sbin \$HOME/.local/bin \$PATH 146 + set -gx AC_ROOT \$HOME/aesthetic-computer 147 + set -gx AESTHETIC $USER 148 + if not status is-interactive 149 + set -gx nogreet true 150 + end 151 + if type -q fnm 152 + fnm env --use-on-cd --shell fish | source 153 + end 154 + set -gx PAGER cat 155 + set -gx GIT_PAGER cat 156 + set -gx MANPAGER cat 157 + if test -f \$AC_ROOT/.devcontainer/config.fish 158 + source \$AC_ROOT/.devcontainer/config.fish 159 + end 160 + FISHCFG 161 + fi 162 + ok "fish config written" 163 + 164 + # ----------------------------------------------------------------------------- 165 + step "10. GPG agent with pinentry-mac" 166 + # ----------------------------------------------------------------------------- 167 + mkdir -p "$HOME/.gnupg" 168 + chmod 700 "$HOME/.gnupg" 169 + if ! grep -q pinentry-mac "$HOME/.gnupg/gpg-agent.conf" 2>/dev/null; then 170 + cat >> "$HOME/.gnupg/gpg-agent.conf" <<'EOF' 171 + pinentry-program /opt/homebrew/bin/pinentry-mac 172 + default-cache-ttl 3600 173 + max-cache-ttl 7200 174 + EOF 175 + fi 176 + gpgconf --kill gpg-agent >/dev/null 2>&1 || true 177 + gpgconf --launch gpg-agent 178 + ok "gpg-agent uses pinentry-mac" 179 + 180 + # ----------------------------------------------------------------------------- 181 + step "11. mkcert CA + localhost dev certs" 182 + # ----------------------------------------------------------------------------- 183 + mkcert -install >/dev/null 2>&1 184 + ok "mkcert CA installed in System keychain" 185 + cd "$AC_ROOT/ssl-dev" 186 + if ! [[ -f localhost.pem ]]; then 187 + env nogreet=true /opt/homebrew/bin/fish ./ssl-install.fish >/dev/null 2>&1 188 + fi 189 + ok "ssl-dev/localhost.pem ($(date -r localhost.pem +%Y-%m-%d))" 190 + 191 + # ----------------------------------------------------------------------------- 192 + step "12. Vault env links" 193 + # ----------------------------------------------------------------------------- 194 + # session-server reads .env relative to its own dir 195 + if [[ -f "$VAULT/session-server/.env" ]]; then 196 + ln -sfn "$VAULT/session-server/.env" "$AC_ROOT/session-server/.env" 197 + ok "session-server/.env linked from vault" 198 + else 199 + warn "vault locked? $VAULT/session-server/.env missing" 200 + fi 201 + # system/.env is loaded by ac-lith if present — no default, optional for local dev 202 + 203 + # ----------------------------------------------------------------------------- 204 + step "13. Smoke test: boot ac-site briefly" 205 + # ----------------------------------------------------------------------------- 206 + kill-port 8888 >/dev/null 2>&1 || true 207 + (cd "$AC_ROOT/lith" && node server.mjs >/tmp/ac-bootstrap-lith.log 2>&1 &) 208 + LITH_PID=$! 209 + sleep 4 210 + if curl -sSI --fail https://localhost:8888/ -o /dev/null 2>/dev/null; then 211 + ok "ac-site responds on https://localhost:8888 with trusted cert" 212 + else 213 + warn "ac-site smoke test failed — see /tmp/ac-bootstrap-lith.log" 214 + fi 215 + kill "$LITH_PID" 2>/dev/null || true 216 + kill-port 8888 >/dev/null 2>&1 || true 217 + 218 + # ----------------------------------------------------------------------------- 219 + printf "\n\033[1;32m✓ Bootstrap complete.\033[0m\n" 220 + printf " Open a new Terminal tab (fish will be the default).\n" 221 + printf " Run \`ac-help\` to list commands, then \`ac-site\` to boot the site.\n\n"