Monorepo for Aesthetic.Computer
aesthetic.computer
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
12set -euo pipefail
13
14AC_ROOT="$HOME/aesthetic-computer"
15VAULT="$AC_ROOT/aesthetic-computer-vault"
16ASKPASS="/tmp/ac-askpass.sh"
17SUDOERS_FILE="/etc/sudoers.d/claude-ac-setup"
18
19step() { printf "\n\033[1;34m▶ %s\033[0m\n" "$*"; }
20ok() { printf " \033[1;32m✓\033[0m %s\n" "$*"; }
21warn() { printf " \033[1;33m!\033[0m %s\n" "$*"; }
22die() { 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# -----------------------------------------------------------------------------
28step "1. GUI askpass helper for sudo -A"
29# -----------------------------------------------------------------------------
30cat > "$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
33EOF
34chmod +x "$ASKPASS"
35export SUDO_ASKPASS="$ASKPASS"
36sudo -A -v
37ok "sudo primed via askpass"
38
39# -----------------------------------------------------------------------------
40step "2. Homebrew"
41# -----------------------------------------------------------------------------
42if ! 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)"
45fi
46eval "$(/opt/homebrew/bin/brew shellenv)"
47ok "brew at $(which brew)"
48
49# -----------------------------------------------------------------------------
50step "3. Brew formulas"
51# -----------------------------------------------------------------------------
52# Core — needed for fish, node, mkcert, site tooling
53brew 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
58ok "core brew formulas installed"
59
60# -----------------------------------------------------------------------------
61step "4. /workspaces → /Users/$USER via synthetic.conf"
62# -----------------------------------------------------------------------------
63if [[ -L /workspaces ]] && [[ "$(readlink /workspaces)" == "/Users/$USER" ]]; then
64 ok "/workspaces already symlinked"
65else
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
76fi
77
78# -----------------------------------------------------------------------------
79step "5. Scoped NOPASSWD sudoers"
80# -----------------------------------------------------------------------------
81SUDOERS_TMP=$(mktemp)
82cat > "$SUDOERS_TMP" <<EOF
83# Claude Code / aesthetic-computer native dev — narrow NOPASSWD for recurring
84# ops. Review with \`sudo visudo -c\`.
85Cmnd_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
91EOF
92sudo -A visudo -cf "$SUDOERS_TMP" >/dev/null
93sudo -A install -o root -g wheel -m 0440 "$SUDOERS_TMP" "$SUDOERS_FILE"
94rm -f "$SUDOERS_TMP"
95ok "sudoers file installed at $SUDOERS_FILE"
96
97# -----------------------------------------------------------------------------
98step "6. fnm + Node (lts-jod & 20.5.0)"
99# -----------------------------------------------------------------------------
100eval "$(fnm env --shell bash)"
101fnm install lts-jod
102fnm install 20.5.0
103fnm default lts-jod
104fnm use lts-jod
105ok "node $(node --version) via fnm ($(fnm current))"
106
107# -----------------------------------------------------------------------------
108step "7. Global npm CLIs (incl. Claude Code)"
109# -----------------------------------------------------------------------------
110npm 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
116ok "npm globals installed"
117
118# Native Claude Code binary (matches Dockerfile:223)
119if ! [[ -x "$HOME/.local/bin/claude" ]]; then
120 curl -fsSL https://claude.ai/install.sh | bash >/dev/null
121fi
122ok "claude native: $("$HOME/.local/bin/claude" --version | head -1)"
123
124# -----------------------------------------------------------------------------
125step "8. fish as default login shell"
126# -----------------------------------------------------------------------------
127if ! grep -q "/opt/homebrew/bin/fish" /etc/shells; then
128 echo "/opt/homebrew/bin/fish" | sudo -n tee -a /etc/shells >/dev/null
129fi
130CURRENT_SHELL=$(dscl . -read "/Users/$USER" UserShell | awk '{print $2}')
131if [[ "$CURRENT_SHELL" != "/opt/homebrew/bin/fish" ]]; then
132 sudo -A /usr/bin/dscl . -change "/Users/$USER" UserShell "$CURRENT_SHELL" /opt/homebrew/bin/fish
133fi
134ok "login shell: $(dscl . -read /Users/$USER UserShell | awk '{print $2}')"
135
136# -----------------------------------------------------------------------------
137step "9. ~/.config/fish/config.fish"
138# -----------------------------------------------------------------------------
139mkdir -p "$HOME/.config/fish/conf.d" "$HOME/.config/fish/functions"
140if ! [[ -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
145set -gx PATH /opt/homebrew/bin /opt/homebrew/sbin \$HOME/.local/bin \$PATH
146set -gx AC_ROOT \$HOME/aesthetic-computer
147set -gx AESTHETIC $USER
148if not status is-interactive
149 set -gx nogreet true
150end
151if type -q fnm
152 fnm env --use-on-cd --shell fish | source
153end
154set -gx PAGER cat
155set -gx GIT_PAGER cat
156set -gx MANPAGER cat
157if test -f \$AC_ROOT/.devcontainer/config.fish
158 source \$AC_ROOT/.devcontainer/config.fish
159end
160FISHCFG
161fi
162ok "fish config written"
163
164# -----------------------------------------------------------------------------
165step "10. GPG agent with pinentry-mac"
166# -----------------------------------------------------------------------------
167mkdir -p "$HOME/.gnupg"
168chmod 700 "$HOME/.gnupg"
169if ! grep -q pinentry-mac "$HOME/.gnupg/gpg-agent.conf" 2>/dev/null; then
170 cat >> "$HOME/.gnupg/gpg-agent.conf" <<'EOF'
171pinentry-program /opt/homebrew/bin/pinentry-mac
172default-cache-ttl 3600
173max-cache-ttl 7200
174EOF
175fi
176gpgconf --kill gpg-agent >/dev/null 2>&1 || true
177gpgconf --launch gpg-agent
178ok "gpg-agent uses pinentry-mac"
179
180# -----------------------------------------------------------------------------
181step "11. mkcert CA + localhost dev certs"
182# -----------------------------------------------------------------------------
183mkcert -install >/dev/null 2>&1
184ok "mkcert CA installed in System keychain"
185cd "$AC_ROOT/ssl-dev"
186if ! [[ -f localhost.pem ]]; then
187 env nogreet=true /opt/homebrew/bin/fish ./ssl-install.fish >/dev/null 2>&1
188fi
189ok "ssl-dev/localhost.pem ($(date -r localhost.pem +%Y-%m-%d))"
190
191# -----------------------------------------------------------------------------
192step "12. Vault env links"
193# -----------------------------------------------------------------------------
194# session-server reads .env relative to its own dir
195if [[ -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"
198else
199 warn "vault locked? $VAULT/session-server/.env missing"
200fi
201# system/.env is loaded by ac-lith if present — no default, optional for local dev
202
203# -----------------------------------------------------------------------------
204step "13. Smoke test: boot ac-site briefly"
205# -----------------------------------------------------------------------------
206kill-port 8888 >/dev/null 2>&1 || true
207(cd "$AC_ROOT/lith" && node server.mjs >/tmp/ac-bootstrap-lith.log 2>&1 &)
208LITH_PID=$!
209sleep 4
210if 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"
212else
213 warn "ac-site smoke test failed — see /tmp/ac-bootstrap-lith.log"
214fi
215kill "$LITH_PID" 2>/dev/null || true
216kill-port 8888 >/dev/null 2>&1 || true
217
218# -----------------------------------------------------------------------------
219printf "\n\033[1;32m✓ Bootstrap complete.\033[0m\n"
220printf " Open a new Terminal tab (fish will be the default).\n"
221printf " Run \`ac-help\` to list commands, then \`ac-site\` to boot the site.\n\n"