Files
dotfiles/pi/files/agent/skills/jj-issue-workspaces/scripts/jj-workspace-fanout.sh
T
2026-04-16 09:12:51 +01:00

293 lines
7.5 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Create one jj workspace per issue, optionally create bookmarks, and optionally open zellij tabs running pi.
Usage:
jj-workspace-fanout.sh [options] --issue "KEY=Title" [--issue "KEY=Title" ...]
Options:
--repo PATH Repo root (default: current directory)
--base REV Base revision/bookmark (default: master)
--remote NAME Git remote to fetch from (default: origin)
--issue KEY=TITLE Issue key and title (repeatable)
--session NAME Zellij session name (defaults to ZELLIJ_SESSION_NAME if set)
--open-pi Open a zellij tab per workspace and launch pi
--no-fetch Skip jj git fetch
--no-bookmarks Do not create feature/<issue> bookmarks
--keep-existing Skip creation for existing workspaces instead of failing
--reset-existing Forget and delete existing workspaces before recreating them
--prompt-suffix TEXT Extra text appended to each pi prompt
--pi-cmd CMD pi command to launch (default: pi)
--dry-run Print planned actions without making changes
--help Show this help
Examples:
jj-workspace-fanout.sh \
--repo /path/to/Phoenix \
--base master \
--issue "SPA-748=Wrap text in credits line items" \
--issue "SPA-754=Resize seat count picker"
jj-workspace-fanout.sh \
--repo /path/to/Phoenix \
--base master \
--session attio \
--open-pi \
--issue "SPA-748=Wrap text in credits line items"
EOF
}
require_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "error: missing required command: $1" >&2
exit 1
fi
}
shell_escape() {
printf '%q' "$1"
}
log() {
printf '[jj-issue-workspaces] %s\n' "$*"
}
run() {
if [[ "$DRY_RUN" -eq 1 ]]; then
printf '[dry-run] '
printf '%q ' "$@"
printf '\n'
else
"$@"
fi
}
workspace_exists() {
local workspace_name="$1"
jj -R "$REPO" workspace list | awk -F: '{print $1}' | grep -Fxq "$workspace_name"
}
bookmark_exists() {
local workspace_dir="$1"
local bookmark_name="$2"
jj -R "$workspace_dir" bookmark list "$bookmark_name" 2>/dev/null | grep -Eq "^${bookmark_name}:"
}
close_tab_if_exists() {
local session_name="$1"
local tab_name="$2"
local tabs
tabs=$(zellij --session "$session_name" action query-tab-names 2>/dev/null || true)
if printf '%s\n' "$tabs" | grep -Fxq "$tab_name"; then
log "closing existing zellij tab $tab_name"
run zellij --session "$session_name" action go-to-tab-name "$tab_name"
run zellij --session "$session_name" action close-tab
fi
}
launch_pi_tab() {
local session_name="$1"
local tab_name="$2"
local workspace_dir="$3"
local prompt="$4"
local cmd
cmd="cd $(shell_escape "$workspace_dir") && pwd && $PI_CMD $(shell_escape "$prompt")"
close_tab_if_exists "$session_name" "$tab_name"
run zellij --session "$session_name" action new-tab --name "$tab_name"
run zellij --session "$session_name" action write-chars "$cmd"
run zellij --session "$session_name" action write 10
}
REPO="$(pwd)"
BASE="master"
REMOTE="origin"
SESSION="${ZELLIJ_SESSION_NAME:-}"
OPEN_PI=0
FETCH=1
CREATE_BOOKMARKS=1
KEEP_EXISTING=0
RESET_EXISTING=0
DRY_RUN=0
PROMPT_SUFFIX=""
PI_CMD="pi"
declare -a ISSUES=()
while [[ $# -gt 0 ]]; do
case "$1" in
--repo)
REPO="$2"
shift 2
;;
--base)
BASE="$2"
shift 2
;;
--remote)
REMOTE="$2"
shift 2
;;
--issue)
ISSUES+=("$2")
shift 2
;;
--session)
SESSION="$2"
shift 2
;;
--open-pi)
OPEN_PI=1
shift
;;
--no-fetch)
FETCH=0
shift
;;
--no-bookmarks)
CREATE_BOOKMARKS=0
shift
;;
--keep-existing)
KEEP_EXISTING=1
shift
;;
--reset-existing)
RESET_EXISTING=1
shift
;;
--prompt-suffix)
PROMPT_SUFFIX="$2"
shift 2
;;
--pi-cmd)
PI_CMD="$2"
shift 2
;;
--dry-run)
DRY_RUN=1
shift
;;
--help|-h)
usage
exit 0
;;
*)
echo "error: unknown argument: $1" >&2
usage >&2
exit 1
;;
esac
done
if [[ ${#ISSUES[@]} -eq 0 ]]; then
echo "error: at least one --issue KEY=TITLE is required" >&2
exit 1
fi
if [[ "$KEEP_EXISTING" -eq 1 && "$RESET_EXISTING" -eq 1 ]]; then
echo "error: --keep-existing and --reset-existing cannot be combined" >&2
exit 1
fi
REPO="$(cd "$REPO" && pwd)"
PARENT_DIR="$(dirname "$REPO")"
REPO_BASENAME="$(basename "$REPO")"
require_cmd jj
if [[ "$OPEN_PI" -eq 1 ]]; then
require_cmd zellij
if [[ -z "$SESSION" ]]; then
echo "error: --open-pi requires --session <name> or ZELLIJ_SESSION_NAME" >&2
exit 1
fi
fi
if [[ ! -d "$REPO/.jj" ]]; then
echo "error: repo is not a jj repository: $REPO" >&2
exit 1
fi
if [[ "$FETCH" -eq 1 ]]; then
log "fetching $BASE from $REMOTE"
run jj -R "$REPO" git fetch --remote "$REMOTE" --branch "$BASE"
fi
log "validating base revision $BASE"
run jj -R "$REPO" log -r "$BASE" --no-pager
created_workspaces=()
for issue in "${ISSUES[@]}"; do
if [[ "$issue" != *=* ]]; then
echo "error: issue must be formatted as KEY=TITLE: $issue" >&2
exit 1
fi
issue_key="${issue%%=*}"
issue_title="${issue#*=}"
issue_slug="$(printf '%s' "$issue_key" | tr '[:upper:]' '[:lower:]')"
workspace_name="$issue_slug"
workspace_dir="$PARENT_DIR/${REPO_BASENAME}-${issue_slug}"
bookmark_name="feature/$issue_slug"
prompt="Work on ${issue_key}: ${issue_title}. You are in the dedicated jj workspace for this issue. First inspect the relevant code, identify the main components involved, and propose a short plan before editing."
if [[ -n "$PROMPT_SUFFIX" ]]; then
prompt+=" ${PROMPT_SUFFIX}"
fi
if workspace_exists "$workspace_name" || [[ -e "$workspace_dir" ]]; then
if [[ "$RESET_EXISTING" -eq 1 ]]; then
log "resetting existing workspace $workspace_name"
if workspace_exists "$workspace_name"; then
run jj -R "$REPO" workspace forget "$workspace_name"
fi
run rm -rf "$workspace_dir"
elif [[ "$KEEP_EXISTING" -eq 1 ]]; then
log "keeping existing workspace $workspace_name at $workspace_dir"
else
echo "error: workspace already exists: $workspace_name ($workspace_dir). Use --keep-existing or --reset-existing." >&2
exit 1
fi
fi
if ! workspace_exists "$workspace_name"; then
log "creating workspace $workspace_name at $workspace_dir"
run jj -R "$REPO" workspace add --name "$workspace_name" -r "$BASE" "$workspace_dir"
fi
if [[ "$CREATE_BOOKMARKS" -eq 1 ]]; then
log "ensuring bookmark $bookmark_name exists"
if bookmark_exists "$workspace_dir" "$bookmark_name"; then
run jj -R "$workspace_dir" bookmark set "$bookmark_name" -r @
else
run jj -R "$workspace_dir" bookmark create "$bookmark_name"
fi
fi
if [[ "$OPEN_PI" -eq 1 ]]; then
log "opening zellij tab $workspace_name in session $SESSION"
run launch_pi_tab "$SESSION" "$workspace_name" "$workspace_dir" "$prompt"
fi
created_workspaces+=("$workspace_name:$workspace_dir:$bookmark_name")
done
printf '\nCreated/updated workspaces:\n'
for item in "${created_workspaces[@]}"; do
IFS=':' read -r workspace_name workspace_dir bookmark_name <<<"$item"
printf ' - %s -> %s' "$workspace_name" "$workspace_dir"
if [[ "$CREATE_BOOKMARKS" -eq 1 ]]; then
printf ' [%s]' "$bookmark_name"
fi
printf '\n'
done
if [[ "$OPEN_PI" -eq 1 ]]; then
printf '\nZellij session: %s\n' "$SESSION"
fi