From 815f0306c700f1a8dd2136e7a15900d3adce8d59 Mon Sep 17 00:00:00 2001 From: bdeshi Date: Tue, 4 Aug 2026 20:32:14 +0600 Subject: [PATCH] dump script --- shannoncoat.sh | 182 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100755 shannoncoat.sh diff --git a/shannoncoat.sh b/shannoncoat.sh new file mode 100755 index 0000000..ff9aae1 --- /dev/null +++ b/shannoncoat.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +# shannoncoat - run more than one claude desktop profile on mac. +# +# claude desktop app seems to keep everything important in one config directory, +# so a different --user-data-dir gives a practically separate install. +# and CLAUDE_CONFIG_DIR env var can isolate claude code sessions natively. +# this script combines these to isolate both desktop sessions and the claude +# code sessions they run. +# +# profiles are yaml files in ~/.shannoncoat: +# # ~/.shannoncoat/client1.yaml -> profile named "client1" +# paths: +# code: ~/work/client1/.claude/code +# desktop: ~/work/client1/.claude/desktop +# +# claude code inside the desktop app can run in some folder which already +# defines a CLAUDE_CONFIG_DIR with direnv etc, that variable can be defined +# this way to allow both individual and desktop sessions work properly: +# export CLAUDE_CONFIG_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}" +# + +set -euo pipefail + +APP="${CLAUDE_APP_PATH:-/Applications/Claude.app}" +BIN="$APP/Contents/MacOS/Claude" +ROOT="$HOME/.shannoncoat" +DEFAULT="default" +APP_DIR="" +CODE_DIR="" +CODE_KEY="code" +DESKTOP_KEY="desktop" + +show_usage() { + cat < [profile] + + launch open a profile, leaving others running + switch quit all other profiles, then open this one + quit quit a profile + list list all running profiles + +Profiles are yaml files in ~/.shannoncoat/. Syntax: + # profile-name.yaml + paths: + $CODE_KEY: ~/path/to/claude/code + $DESKTOP_KEY: ~/path/to/claude/desktop +EOF +} + +# expand leading ~ in path string to $HOME +tilde_path() { + local p="$1" + echo "${p/#\~/$HOME}" +} + +# pull one key out of the yaml, simple job avoid yq for now +read_key() { + local file="$1" key="$2" + sed -EHn "s/^\s+$key:\s+//p" "$file" | tr -d '"' +} + +# set APP_DIR and CODE_DIR for a profile, or keep empty for $DEFAULT +get_dirs() { + local name="$1" file="$ROOT/$1.yaml" + + [[ "$name" == "$DEFAULT" ]] && { return; } + if [[ ! -f "$file" ]]; then + echo "no profile named $name (expected $file)" >&2 + exit 1 + fi + APP_DIR="$(tilde_path "$(read_key "$file" $DESKTOP_KEY)")" + CODE_DIR="$(tilde_path "$(read_key "$file" $CODE_KEY)")" +} + +# match a claude data dir to its profile name +dir_to_profile() { + local dir="$1" file name + [[ "$dir" == "$DEFAULT" ]] && { echo "$DEFAULT"; return; } + for file in "$ROOT"/*.yaml; do + [[ -f "$file" ]] || continue + name="$(basename "$file" .yaml)" + if [[ "$(tilde_path "$(read_key "$file" $DESKTOP_KEY)")" == "$dir" ]]; then + echo "$name" + return + fi + done + echo "$dir" +} + +# get all running claude processes as "piduser-data-dir" +running_info() { + local pid args dir + while read -r pid args; do + # if args has --user-data-dir=, extract it, otherwise use $DEFAULT + if [[ "$args" == *--user-data-dir=* ]]; then + dir="${args#*--user-data-dir=}" + dir="${dir%% *}" + else + dir="$DEFAULT" + fi + printf '%s\t%s\n' "$pid" "$dir" + done < <(pgrep -fl "$BIN" || true) +} + +# get pid of a profile's claude process +profile_to_pid() { + get_dirs "$1" + running_info | awk -F'\t' -v d="${APP_DIR:-default}" '$2 == d { print $1; exit }' +} + +# switch focus to a process window, shell/term needs accessibility permissions +app_focus() { + local pid="$1" + osascript > /dev/null 2>&1 < /dev/null 2>&1 & + disown +} + +# list all running profiles +cmd_list() { + local pid dir + while read -r pid dir; do + printf 'profile: %s\tpid: %s\n' "$(dir_to_profile "$dir")" "$pid" + done < <(running_info) +} + +# launch a profile or focus window if already running +cmd_launch() { + local name="${1:?profile name required}" pid + pid="$(profile_to_pid "$name")" + [[ -n "$pid" ]] && { app_focus "$pid"; return; } + app_start "$name" +} + +# switch to a profile quitting all others +cmd_switch() { + local name="${1:?profile name required}" pid dir keep + # marks which profile to keep running, "default" if empty + get_dirs "$name" + keep="${APP_DIR:-"$DEFAULT"}" + # quit all other running claudes except the one to $keep + while read -r pid dir; do + [[ "$dir" == "$keep" ]] && continue + kill "$pid" 2>/dev/null || true + done < <(running_info) + # then launch selected profile + cmd_launch "$name" +} + +# quit a profile if running +cmd_quit() { + local name="${1:?profile name required}" pid + pid="$(profile_to_pid "$name")" + [[ -z "$pid" ]] && { echo "$name isn't running"; return; } + kill "$pid" +} + +# main entry point +case "${1:-}" in + launch) cmd_launch "${2:-}" ;; + switch) cmd_switch "${2:-}" ;; + quit) cmd_quit "${2:-}" ;; + list) cmd_list ;; + -h|--help) show_usage; exit 0 ;; + *) show_usage; exit 1 ;; +esac