← back to Iterm Project Grouper

claude-proj

87 lines

#!/usr/bin/env bash
# claude-proj — open a Claude Code session as a TAB inside its project's
# dedicated iTerm2 window (creating the window the first time). Groups every
# session of the same project (git repo root) into ONE window, going forward,
# without ever touching existing live sessions.
#
# Usage:
#   claude-proj [dir]            # group by the git root of dir (default: $PWD)
#   claude-proj                  # current directory's project
#   CLAUDE_PROJ_CMD='...'  claude-proj [dir]   # override the launched command (testing)
#   claude-proj --list           # show the project→window registry
#   claude-proj --forget PROJ    # drop a stale registry entry
#
# Registry: ~/.claude/iterm-proj-windows.tsv  (lines: <project>\t<iterm-window-id>)
# Non-destructive: only ever CREATES tabs/windows; never closes or moves live ones.
set -euo pipefail

REG="$HOME/.claude/iterm-proj-windows.tsv"
mkdir -p "$(dirname "$REG")"; touch "$REG"

case "${1:-}" in
  --list) printf 'project\twindow-id\n'; cat "$REG"; exit 0 ;;
  --forget) awk -F'\t' -v p="${2:-__none__}" '$1!=p' "$REG" > "$REG.tmp" && mv "$REG.tmp" "$REG"; echo "forgot: ${2:-}"; exit 0 ;;
esac

DIR="${1:-$PWD}"
[ -d "$DIR" ] || { echo "claude-proj: no such dir: $DIR" >&2; exit 1; }
DIR="$(cd "$DIR" && pwd -P)"
ROOT="$(git -C "$DIR" rev-parse --show-toplevel 2>/dev/null || echo "$DIR")"
PROJ="$(basename "$ROOT")"
CMD="${CLAUDE_PROJ_CMD:-claude}"

# look up an existing window id for this project
WINID="$(awk -F'\t' -v p="$PROJ" '$1==p{print $2; exit}' "$REG")"

# ask iTerm2 to place the tab; returns the window id actually used.
# CP_* env vars are read inside AppleScript via `system attribute` — no quoting hell.
USED_ID="$(CP_DIR="$DIR" CP_CMD="$CMD" CP_PROJ="$PROJ" CP_WINID="${WINID:-0}" osascript <<'APPLESCRIPT'
on envv(k)
  try
    return (do shell script "printf %s \"$" & k & "\"")
  on error
    return ""
  end try
end envv
set theDir to envv("CP_DIR")
set theCmd to envv("CP_CMD")
set theProj to envv("CP_PROJ")
set wantId to envv("CP_WINID")
set runLine to "cd " & quoted form of theDir & " && " & theCmd
tell application "iTerm2"
  set targetWin to missing value
  if wantId is not "" and wantId is not "0" then
    repeat with w in windows
      try
        if (id of w as string) is wantId then set targetWin to w
      end try
    end repeat
  end if
  if targetWin is missing value then
    -- no live window for this project → make one
    set targetWin to (create window with default profile)
    tell current session of targetWin to write text runLine
    tell current session of targetWin to set name to theProj
  else
    -- reuse the project's window: add a tab
    tell targetWin
      set newTab to (create tab with default profile)
    end tell
    tell current session of newTab to write text runLine
    tell current session of newTab to set name to theProj
  end if
  return (id of targetWin as string)
end tell
APPLESCRIPT
)"

# persist / refresh the registry mapping
if [ -n "$USED_ID" ]; then
  grep -v -E "^${PROJ}	" "$REG" > "$REG.tmp" 2>/dev/null || true
  printf '%s\t%s\n' "$PROJ" "$USED_ID" >> "$REG.tmp"
  mv "$REG.tmp" "$REG"
  echo "claude-proj: '$PROJ' → iTerm2 window $USED_ID  (dir: $DIR)"
else
  echo "claude-proj: failed to place tab (is iTerm2 running?)" >&2; exit 1
fi