|
#!/usr/bin/env bash |
|
# herd - summon a HerdR workspace for the current git repo. |
|
# |
|
# Model: |
|
# π 1 worktree = 1 HerdR workspace |
|
# π€ 1 agentic loop per workspace |
|
# π§Ό no repo-local config |
|
# π³ parallelism across worktrees |
|
# |
|
# Usage: |
|
# herd [DIRECTORY] |
|
# |
|
# DIRECTORY defaults to the current directory. |
|
|
|
set -euo pipefail |
|
|
|
HERDR_BIN="${HERDR_BIN:-herdr}" |
|
|
|
TABS=( |
|
"π€ agent" |
|
"π editor" |
|
"πΏ changes" |
|
"π diff" |
|
"π run" |
|
"π₯οΈ term" |
|
) |
|
|
|
# ---------- colors ---------- |
|
|
|
if [ -t 1 ]; then |
|
BOLD="$(printf '\033[1m')" |
|
DIM="$(printf '\033[2m')" |
|
RED="$(printf '\033[31m')" |
|
GREEN="$(printf '\033[32m')" |
|
YELLOW="$(printf '\033[33m')" |
|
BLUE="$(printf '\033[34m')" |
|
MAGENTA="$(printf '\033[35m')" |
|
CYAN="$(printf '\033[36m')" |
|
RESET="$(printf '\033[0m')" |
|
else |
|
BOLD="" |
|
DIM="" |
|
RED="" |
|
GREEN="" |
|
YELLOW="" |
|
BLUE="" |
|
MAGENTA="" |
|
CYAN="" |
|
RESET="" |
|
fi |
|
|
|
say() { |
|
printf '%b\n' "$*" |
|
} |
|
|
|
die() { |
|
say "${RED}π₯ error:${RESET} $*" >&2 |
|
exit 1 |
|
} |
|
|
|
ok() { |
|
say "${GREEN}β${RESET} $*" |
|
} |
|
|
|
info() { |
|
say "${CYAN}β${RESET} $*" |
|
} |
|
|
|
warn() { |
|
say "${YELLOW}β ${RESET} $*" |
|
} |
|
|
|
# ---------- basics ---------- |
|
|
|
need_cmd() { |
|
command -v "$1" >/dev/null 2>&1 || die "missing command: $1" |
|
} |
|
|
|
repo_root() { |
|
git rev-parse --show-toplevel 2>/dev/null |
|
} |
|
|
|
workspace_name_for_repo() { |
|
local root="$1" |
|
local repo parent |
|
|
|
repo="$(basename "$root")" |
|
parent="$(basename "$(dirname "$root")")" |
|
|
|
# Human-readable and stable: |
|
# carrefour/alertnet-back |
|
# axone-protocol/prolog |
|
echo "$parent/$repo" |
|
} |
|
|
|
workspace_id_by_label() { |
|
local label="$1" |
|
local raw id |
|
|
|
raw="$($HERDR_BIN workspace list 2>/dev/null || true)" |
|
[ -n "$raw" ] || return 1 |
|
|
|
id="$(printf '%s\n' "$raw" | jq -er --arg label "$label" ' |
|
def items: |
|
if type == "array" then . |
|
elif type == "object" then (.result.workspaces // .workspaces // .result // []) |
|
else [] end; |
|
|
|
items[] |
|
| select(type == "object") |
|
| select((.label // .name // .title) == $label) |
|
| (.workspace_id // .id) |
|
| select(. != null and . != "") |
|
' 2>/dev/null | head -n1 || true)" |
|
|
|
if [ -n "$id" ]; then |
|
printf '%s\n' "$id" |
|
return 0 |
|
fi |
|
|
|
# Fallback for human/table output. |
|
id="$(printf '%s\n' "$raw" | |
|
awk -v want="$label" ' |
|
index($0, want) { |
|
for (i = 1; i <= NF; i++) { |
|
if ($i ~ /^w[[:alnum:]]+$/ || $i ~ /^[0-9]+(:[0-9]+)?(-[0-9]+)?$/) { |
|
print $i |
|
found = 1 |
|
exit 0 |
|
} |
|
} |
|
} |
|
END { if (!found) exit 1 } |
|
' || true)" |
|
|
|
if [ -n "$id" ]; then |
|
printf '%s\n' "$id" |
|
return 0 |
|
fi |
|
|
|
return 1 |
|
} |
|
|
|
tab_id_by_label() { |
|
local workspace_id="$1" |
|
local label="$2" |
|
local raw id |
|
|
|
raw="$($HERDR_BIN tab list --workspace "$workspace_id" 2>/dev/null || true)" |
|
[ -n "$raw" ] || return 1 |
|
|
|
id="$(printf '%s\n' "$raw" | jq -er --arg label "$label" ' |
|
def items: |
|
if type == "array" then . |
|
elif type == "object" then (.result.tabs // .tabs // .result // []) |
|
else [] end; |
|
|
|
items[] |
|
| select(type == "object") |
|
| select((.label // .name // .title) == $label) |
|
| (.tab_id // .id) |
|
| select(. != null and . != "") |
|
' 2>/dev/null | head -n1 || true)" |
|
|
|
if [ -n "$id" ]; then |
|
printf '%s\n' "$id" |
|
return 0 |
|
fi |
|
|
|
# Fallback for human/table output. |
|
id="$(printf '%s\n' "$raw" | |
|
awk -v want="$label" ' |
|
index($0, want) { |
|
for (i = 1; i <= NF; i++) { |
|
if ($i ~ /^w[[:alnum:]]+:t[[:alnum:]]+$/ || $i ~ /^[0-9]+:[0-9]+$/ || $i ~ /^[0-9]+$/) { |
|
print $i |
|
found = 1 |
|
exit 0 |
|
} |
|
} |
|
} |
|
END { if (!found) exit 1 } |
|
' || true)" |
|
|
|
if [ -n "$id" ]; then |
|
printf '%s\n' "$id" |
|
return 0 |
|
fi |
|
|
|
return 1 |
|
} |
|
initial_tab_id() { |
|
local workspace_id="$1" |
|
local raw id |
|
|
|
raw="$($HERDR_BIN tab list --workspace "$workspace_id" 2>/dev/null || true)" |
|
[ -n "$raw" ] || return 1 |
|
|
|
id="$(printf '%s\n' "$raw" | jq -er ' |
|
def items: |
|
if type == "array" then . |
|
elif type == "object" then (.result.tabs // .tabs // .result // []) |
|
else [] end; |
|
|
|
items[] |
|
| select(type == "object") |
|
| select((.label // .name // .title) == "1" or (.label // .name // .title) == "") |
|
| (.tab_id // .id) |
|
| select(. != null and . != "") |
|
' 2>/dev/null | head -n1 || true)" |
|
|
|
if [ -n "$id" ]; then |
|
printf '%s\n' "$id" |
|
return 0 |
|
fi |
|
|
|
return 1 |
|
} |
|
|
|
pane_id_by_tab_id() { |
|
local workspace_id="$1" |
|
local tab_id="$2" |
|
local raw id |
|
|
|
raw="$($HERDR_BIN pane list --workspace "$workspace_id" --tab "$tab_id" 2>/dev/null || true)" |
|
[ -n "$raw" ] || return 1 |
|
|
|
id="$(printf '%s\n' "$raw" | jq -er ' |
|
def items: |
|
if type == "array" then . |
|
elif type == "object" then (.result.panes // .panes // .result // []) |
|
else [] end; |
|
|
|
items[] |
|
| select(type == "object") |
|
| (.pane_id // .id) |
|
| select(. != null and . != "") |
|
' 2>/dev/null | head -n1 || true)" |
|
|
|
if [ -n "$id" ]; then |
|
printf '%s\n' "$id" |
|
return 0 |
|
fi |
|
|
|
# Fallback for human/table output. |
|
id="$(printf '%s\n' "$raw" | |
|
awk ' |
|
{ |
|
for (i = 1; i <= NF; i++) { |
|
if ($i ~ /^w[[:alnum:]]+:t[[:alnum:]]+:p[[:alnum:]]+$/ || $i ~ /^[0-9]+-[0-9]+$/ || $i ~ /^[0-9]+$/) { |
|
print $i |
|
found = 1 |
|
exit 0 |
|
} |
|
} |
|
} |
|
END { if (!found) exit 1 } |
|
' || true)" |
|
|
|
if [ -n "$id" ]; then |
|
printf '%s\n' "$id" |
|
return 0 |
|
fi |
|
|
|
return 1 |
|
} |
|
|
|
pane_id_from_create_tab_response() { |
|
local raw="$1" |
|
local id |
|
[ -n "$raw" ] || return 1 |
|
|
|
id="$(printf '%s\n' "$raw" | jq -er ' |
|
def pane_id_from($x): |
|
if ($x | type) == "object" then ($x.pane_id // $x.id // empty) |
|
else empty end; |
|
|
|
. as $root |
|
| (.result // {}) as $result |
|
| ( |
|
pane_id_from($result.root_pane), |
|
pane_id_from($result.pane), |
|
pane_id_from($result.active_pane), |
|
pane_id_from($result.created_pane), |
|
pane_id_from($root.root_pane), |
|
pane_id_from($root.pane), |
|
($result.panes // $result.tab.panes // $root.panes // $root.tab.panes // [] | .[]? | pane_id_from(.)) |
|
) |
|
| select(. != null and . != "") |
|
' 2>/dev/null | head -n1 || true)" |
|
|
|
if [ -n "$id" ]; then |
|
printf '%s\n' "$id" |
|
return 0 |
|
fi |
|
|
|
return 1 |
|
} |
|
|
|
# ---------- herdr ops ---------- |
|
|
|
create_workspace() { |
|
local label="$1" |
|
local cwd="$2" |
|
|
|
"$HERDR_BIN" workspace create --cwd "$cwd" --label "$label" |
|
} |
|
|
|
focus_workspace() { |
|
local workspace_id="$1" |
|
"$HERDR_BIN" workspace focus "$workspace_id" |
|
} |
|
|
|
create_tab() { |
|
local workspace_id="$1" |
|
local label="$2" |
|
|
|
"$HERDR_BIN" tab create --workspace "$workspace_id" --label "$label" --no-focus |
|
} |
|
rename_tab() { |
|
local tab_id="$1" |
|
local label="$2" |
|
|
|
"$HERDR_BIN" tab rename "$tab_id" "$label" |
|
} |
|
|
|
close_tab() { |
|
local tab_id="$1" |
|
|
|
"$HERDR_BIN" tab close "$tab_id" |
|
} |
|
|
|
run_in_pane() { |
|
local pane_id="$1" |
|
local command="$2" |
|
|
|
"$HERDR_BIN" pane run "$pane_id" "$command" |
|
} |
|
|
|
split_pane_horizontal() { |
|
local pane_id="$1" |
|
local output="" |
|
|
|
# HerdR direction names are right/down. "down" creates a visual horizontal split. |
|
if output="$($HERDR_BIN pane split "$pane_id" --direction down --no-focus 2>&1)"; then |
|
return 0 |
|
fi |
|
|
|
if output="$($HERDR_BIN pane split --pane "$pane_id" --direction down --no-focus 2>&1)"; then |
|
return 0 |
|
fi |
|
|
|
if output="$($HERDR_BIN pane split "$pane_id" down 2>&1)"; then |
|
return 0 |
|
fi |
|
|
|
warn "could not split runner pane horizontally: $output" |
|
return 1 |
|
} |
|
|
|
shell_command() { |
|
local root="$1" |
|
local label="$2" |
|
|
|
printf "cd '%s' && clear && printf 'π %%s\\nπ %%s\\n\\n' '%s' '%s' && exec %s" \ |
|
"$root" "$label" "$root" "${SHELL:-/bin/bash}" |
|
} |
|
|
|
omp_command() { |
|
local root="$1" |
|
local label="$2" |
|
|
|
if command -v omp >/dev/null 2>&1; then |
|
printf "cd '%s' && omp" "$root" |
|
else |
|
printf "cd '%s' && clear && printf 'π§ %%s\\nπ %%s\\n\\nβ omp not found in PATH\\n\\n' '%s' '%s' && exec %s" \ |
|
"$root" "$label" "$root" "${SHELL:-/bin/bash}" |
|
fi |
|
} |
|
|
|
editor_command() { |
|
local root="$1" |
|
|
|
if command -v nvim >/dev/null 2>&1; then |
|
printf "cd '%s' && nvim ." "$root" |
|
elif command -v vim >/dev/null 2>&1; then |
|
printf "cd '%s' && vim ." "$root" |
|
else |
|
printf "cd '%s' && clear && printf 'π editor\\nπ %%s\\n\\nβ nvim not found in PATH\\n\\n' '%s' && exec %s" \ |
|
"$root" "$root" "${SHELL:-/bin/bash}" |
|
fi |
|
} |
|
|
|
changes_command() { |
|
local root="$1" |
|
|
|
if command -v tig >/dev/null 2>&1; then |
|
printf "cd '%s' && tig" "$root" |
|
else |
|
printf "cd '%s' && git status && exec %s" "$root" "${SHELL:-/bin/bash}" |
|
fi |
|
} |
|
|
|
diff_command() { |
|
local root="$1" |
|
local label="$2" |
|
|
|
if command -v livediff >/dev/null 2>&1; then |
|
printf "cd '%s' && livediff" "$root" |
|
else |
|
printf "cd '%s' && clear && printf 'π %%s\\nπ %%s\\n\\nβ livediff not found in PATH\\n\\n' '%s' '%s' && exec %s" \ |
|
"$root" "$label" "$root" "${SHELL:-/bin/bash}" |
|
fi |
|
} |
|
|
|
# ---------- main ---------- |
|
|
|
main() { |
|
[ "$#" -le 1 ] || die "usage: herd [DIRECTORY]" |
|
|
|
local target_dir="${1:-.}" |
|
[ -d "$target_dir" ] || die "not a directory: $target_dir" |
|
cd -- "$target_dir" || die "could not enter directory: $target_dir" |
|
|
|
need_cmd git |
|
need_cmd jq |
|
need_cmd "$HERDR_BIN" |
|
|
|
local root workspace workspace_id created="false" |
|
|
|
root="$(repo_root)" || die "not inside a git repository" |
|
workspace="$(workspace_name_for_repo "$root")" |
|
|
|
say |
|
say "${BOLD}${MAGENTA}π herd${RESET} ${DIM}summoning repo workspace${RESET}" |
|
say "${DIM}ββββββββββββββββββββββββββββββββββββββββ${RESET}" |
|
say "${BLUE}repo:${RESET} $root" |
|
say "${BLUE}workspace:${RESET} $workspace" |
|
say |
|
|
|
if workspace_id="$(workspace_id_by_label "$workspace" 2>/dev/null)"; then |
|
ok "workspace exists ${DIM}#$workspace_id${RESET}" |
|
else |
|
info "creating workspace" |
|
create_workspace "$workspace" "$root" >/dev/null |
|
created="true" |
|
|
|
workspace_id="$(workspace_id_by_label "$workspace" 2>/dev/null || true)" |
|
if [ -z "$workspace_id" ]; then |
|
warn "workspace created, but could not resolve its id from HerdR" |
|
warn "using workspace label as fallback: $workspace" |
|
workspace_id="$workspace" |
|
fi |
|
|
|
ok "workspace created ${DIM}#$workspace_id${RESET}" |
|
fi |
|
|
|
say |
|
|
|
local tab tab_id pane_id command response initial_tab |
|
if [ "$created" = "true" ]; then |
|
initial_tab="$(initial_tab_id "$workspace_id" 2>/dev/null || true)" |
|
else |
|
initial_tab="" |
|
fi |
|
|
|
for tab in "${TABS[@]}"; do |
|
tab_id="$(tab_id_by_label "$workspace_id" "$tab" 2>/dev/null || true)" |
|
if [ -n "$tab_id" ]; then |
|
info "tab exists: ${BOLD}$tab${RESET}" |
|
continue |
|
fi |
|
|
|
info "creating tab: ${BOLD}$tab${RESET}" |
|
response="$(create_tab "$workspace_id" "$tab")" |
|
pane_id="$(pane_id_from_create_tab_response "$response" 2>/dev/null || true)" |
|
|
|
tab_id="$(tab_id_by_label "$workspace_id" "$tab" 2>/dev/null || true)" |
|
if [ -z "$tab_id" ]; then |
|
warn "tab created, but could not resolve its id: $tab" |
|
fi |
|
|
|
if [ -z "$pane_id" ] && [ -n "$tab_id" ]; then |
|
pane_id="$(pane_id_by_tab_id "$workspace_id" "$tab_id" 2>/dev/null || true)" |
|
fi |
|
|
|
if [ -z "$pane_id" ]; then |
|
warn "tab created, but could not resolve root pane: $tab" |
|
continue |
|
fi |
|
|
|
command="" |
|
case "$tab" in |
|
"π€ agent") |
|
command="$(omp_command "$root" "$tab")" |
|
;; |
|
"π editor") |
|
command="$(editor_command "$root")" |
|
;; |
|
"πΏ changes") |
|
command="$(changes_command "$root")" |
|
;; |
|
"π diff") |
|
command="$(diff_command "$root" "$tab")" |
|
;; |
|
"π run") |
|
split_pane_horizontal "$pane_id" || true |
|
;; |
|
*) |
|
# Default HerdR shell is enough for generic terminals. |
|
command="" |
|
;; |
|
esac |
|
|
|
if [ -n "$command" ]; then |
|
run_in_pane "$pane_id" "$command" |
|
fi |
|
ok "tab ready: ${BOLD}$tab${RESET}" |
|
|
|
done |
|
|
|
if [ -n "$initial_tab" ]; then |
|
if close_tab "$initial_tab" >/dev/null 2>&1; then |
|
ok "initial tab removed" |
|
else |
|
warn "could not remove initial tab: $initial_tab" |
|
fi |
|
fi |
|
|
|
say |
|
info "focusing workspace" |
|
focus_workspace "$workspace_id" |
|
|
|
say |
|
if [ "$created" = "true" ]; then |
|
say "${GREEN}β¨ herd assembled.${RESET}" |
|
else |
|
say "${GREEN}β¨ herd restored.${RESET}" |
|
fi |
|
say |
|
} |
|
|
|
main "$@" |