dump script
This commit is contained in:
Executable
+182
@@ -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 <<EOF
|
||||
Usage: $0 <command> [profile]
|
||||
|
||||
launch <profile> open a profile, leaving others running
|
||||
switch <profile> quit all other profiles, then open this one
|
||||
quit <profile> 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 "pid<TAB>user-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 <<EOF || true
|
||||
tell application "System Events"
|
||||
set proc to first process whose unix id is $pid
|
||||
set frontmost of proc to true
|
||||
try
|
||||
perform action "AXRaise" of window 1 of proc
|
||||
end try
|
||||
end tell
|
||||
EOF
|
||||
}
|
||||
|
||||
# start a claude desktop process with the right env vars and user-data-dir
|
||||
app_start() {
|
||||
get_dirs "$1"
|
||||
# app dir empty means default profile, just open normally
|
||||
[[ -z "$APP_DIR" ]] && { open -a "$APP"; return; }
|
||||
mkdir -p "$APP_DIR" "$CODE_DIR"
|
||||
CLAUDE_CONFIG_DIR="$CODE_DIR" "$BIN" --user-data-dir="$APP_DIR" > /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
|
||||
Reference in New Issue
Block a user