From d5ef335d61db5e99bbb3a798c0890812da54b12a Mon Sep 17 00:00:00 2001 From: Gabriel Brown Date: Sun, 1 Nov 2026 12:41:58 -0500 Subject: [PATCH] Link the agent half of user/ on the Mac mini Manifest lines can now name an OS. The SSH config, pwswitch and espanso are Linux only, so link-user on the Mac links CLAUDE.md, rules, skills and the opencode commands and nothing else. link-mcp no longer aborts under the Mac's bash 3.2 when a server has no token. --- setup/scripts/link-mcp | 7 +++++-- setup/scripts/link-user | 4 +++- tests/setup/user-content-contract | 10 +++++++++- user/README.md | 6 ++++++ user/manifest | 15 ++++++++++----- 5 files changed, 33 insertions(+), 9 deletions(-) diff --git a/setup/scripts/link-mcp b/setup/scripts/link-mcp index 79067a2..aff2845 100755 --- a/setup/scripts/link-mcp +++ b/setup/scripts/link-mcp @@ -157,14 +157,17 @@ while read -r name transport target token_var _rest; do continue fi + # ${args[@]+...} rather than "${args[@]}": the Mac's bash 3.2 treats an + # empty array as unset under set -u and aborts the whole stage. + # # Re-registering is how a new server or a rotated token reaches the # runtime. Removing first avoids the CLI refusing a name it already knows. # Neither call may print the token, so both are quiet unless they fail. claude mcp remove "$name" -s user >/dev/null 2>&1 || true if [[ "$transport" == stdio ]]; then - add=(claude mcp add "$name" -s user "${args[@]}" -- "$launcher") + add=(claude mcp add "$name" -s user ${args[@]+"${args[@]}"} -- "$launcher") else - add=(claude mcp add --transport "$transport" "$name" "$url" "${args[@]}" -s user) + add=(claude mcp add --transport "$transport" "$name" "$url" ${args[@]+"${args[@]}"} -s user) fi if "${add[@]}" >/dev/null 2>&1; then log "Registered $name." diff --git a/setup/scripts/link-user b/setup/scripts/link-user index 3a45989..0abdb5f 100755 --- a/setup/scripts/link-user +++ b/setup/scripts/link-user @@ -69,10 +69,12 @@ displace() { linked=0 copied=0 +os="$(uname -s | tr '[:upper:]' '[:lower:]')" -while read -r kind source destination; do +while read -r kind source destination only_on _rest; do [[ -n "${kind:-}" ]] || continue [[ "$kind" == \#* ]] && continue + [[ -z "${only_on:-}" || "$only_on" == "$os" ]] || continue src="$USER_DIR/$source" dst="${destination/#\~/$HOME}" diff --git a/tests/setup/user-content-contract b/tests/setup/user-content-contract index 2c4eb5e..a4ff132 100755 --- a/tests/setup/user-content-contract +++ b/tests/setup/user-content-contract @@ -46,13 +46,15 @@ done < <(find "$repo_dir/user" -type f \( -name 'id_*' -o -name '*.pem' -o -name # ── Every manifest line is usable ─────────────────────────────────────────── -while read -r kind source destination; do +while read -r kind source destination only_on; do [[ -e "$repo_dir/user/$source" ]] \ || note "the manifest points at $source, which is not in user/" [[ "$kind" == link || "$kind" == linkdir || "$kind" == copy ]] \ || note "the manifest uses an unknown kind: $kind" [[ "$destination" == '~/'* ]] \ || note "the manifest destination $destination is not under the home directory" + [[ -z "$only_on" || "$only_on" == linux || "$only_on" == darwin ]] \ + || note "the manifest names an unknown os: $only_on" done < <(grep -vE '^\s*(#|$)' "$manifest") # ── The behaviour, against a fake home ────────────────────────────────────── @@ -77,6 +79,7 @@ linkdir agents/skills ~/.agents/skills linkdir agents/skills ~/.claude/skills copy plain.txt ~/.config/plain.txt link missing.txt ~/.config/missing.txt +copy plain.txt ~/.config/elsewhere.txt nosuchos FIXTURE home="$work/home" @@ -141,6 +144,11 @@ fi grep -q 'missing.txt' "$work/log" \ || note 'a skipped manifest entry was not reported' +# A line naming another OS is for another machine. The Mac takes the agent half +# of user/ and none of the Linux desktop half. +[[ -e "$home/.config/elsewhere.txt" ]] \ + && note 'a manifest entry for another OS was applied here' + # ── The answer sticks ─────────────────────────────────────────────────────── # # So that `panama upgrade` on a configured machine relinks without an interview, diff --git a/user/README.md b/user/README.md index e58f5f5..1acd886 100644 --- a/user/README.md +++ b/user/README.md @@ -86,6 +86,12 @@ Put the file under `user/`, add a line to `manifest`, run: Anything already at the destination is moved to `config/old/` rather than deleted, under a name that says where it came from. +A line can end with `linux` or `darwin` to apply only on that OS. The Mac mini +is not a Panama desktop, but it runs agents, so it takes the agent half of this +directory and skips the SSH config, `pwswitch` and espanso. Panama's shipped +skills describe the Linux desktop, so the Mac runs `link-user` and `link-mcp` +only, never `link-skills`. + ## What does not go in here Anything secret. This repository is readable by anyone who finds it, and the diff --git a/user/manifest b/user/manifest index de69538..f94124f 100644 --- a/user/manifest +++ b/user/manifest @@ -1,6 +1,6 @@ # What in user/ goes where, and in what form. # -# +# [os] # # kind is one of: # link symlink the destination at the source, so editing either edits the @@ -14,6 +14,10 @@ # copy copy once, and never touch it again if the destination exists. Use # this for files a machine legitimately diverges on. # +# os is optional: linux or darwin, matched against `uname -s`. A line naming +# one is skipped everywhere else. Leave it off for anything every machine +# wants, which is most of this file; the Mac mini only takes the agent half. +# # Lines beginning with # are comments and blank lines are ignored. ~ in a # destination is this user's home. # @@ -43,21 +47,22 @@ link agents/rules ~/.claude/rules # ── SSH ────────────────────────────────────────────────────────────────────── # Host aliases only. Keys are per-machine and are never tracked; this is the # file that makes `ssh server` work the moment a machine is set up, and it is -# what the SSH Hosts launcher command reads. -link ssh/config ~/.ssh/config +# what the SSH Hosts launcher command reads. Linux only: it names id_ed25519 +# and the desktop keyring's agent socket, and the Mac has neither. +link ssh/config ~/.ssh/config linux # ── Scripts ────────────────────────────────────────────────────────────────── # Personal commands wanted on every machine, desktop or server. ~/.local/bin is # on PATH in both shell configs. pwswitch toggles passwordless sudo through a # sudoers.d drop-in, so an agent can run sudo and the password comes back after. -link bin/pwswitch ~/.local/bin/pwswitch +link bin/pwswitch ~/.local/bin/pwswitch linux # ── Espanso ────────────────────────────────────────────────────────────────── # The personal expansion triggers. Copied rather than linked, because this is a # file each machine may add its own triggers to. It lands before setup-identity # runs, and that stage only seeds a file that does not exist yet, so what is # tracked here wins over the name and email the interview would have written. -copy espanso/identity.yml ~/.config/espanso/match/identity.yml +copy espanso/identity.yml ~/.config/espanso/match/identity.yml linux # ── OpenCode ───────────────────────────────────────────────────────────────── # Claude Code carries its commands and agents as plugins, which opencode cannot