Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244#!/usr/bin/env sh#MISE description="Link the agent config for your harnesses and version control"#MISE alias=["setup:agent", "su:agents", "su:agent"]
set -e
CONFIG=".agents/config.yml"GENERATED="# Generated by mise run setup:agents. Edit the source in .agents/agents instead."HARNESSES="claude codex opencode"PROFILES="git gitbutler jujutsu"overwrite=""
for arg in "$@"; do case "$arg" in --overwrite) overwrite="1" ;; *) echo "unknown argument: $arg" >&2 exit 1 ;; esacdone
read_config() { [ -f "$CONFIG" ] || return 0 sed -n "s/^$1:[[:space:]]*//p" "$CONFIG" | head -n 1}
read_config_list() { [ -f "$CONFIG" ] || return 0
awk -v key="$1:" ' $0 == key { inside = 1; next } /^[^[:space:]-]/ { inside = 0 } inside && /^[[:space:]]*-[[:space:]]*/ { sub(/^[[:space:]]*-[[:space:]]*/, "") print } ' "$CONFIG" | tr '\n' ' '}
guess_vcs() { if [ -d ".jj" ]; then echo "jujutsu" elif [ -d ".git/gitbutler" ]; then echo "gitbutler" else echo "git" fi}
choose() { limit="$1" header="$2" default="$3" shift 3
if [ ! -t 0 ]; then echo "$default" return 0 fi
gum choose --header "$header" --selected "$(echo "$default" | tr ' ' ',')" "$limit" "$@" | tr '\n' ' '}
choose_many() { choose "--no-limit" "$@"}
choose_one() { choose "--limit=1" "$@"}
up_to_root() { prefix="" dir=$(dirname "$1")
while [ "$dir" != "." ]; do prefix="../$prefix" dir=$(dirname "$dir") done
printf '%s' "$prefix"}
link() { source="$1" path="$2"
if [ -e "$path" ] && [ ! -L "$path" ]; then echo "refusing to replace $path: it is not a symlink" >&2 exit 1 fi
mkdir -p "$(dirname "$path")" rm -f "$path" ln -s "$(up_to_root "$path")$source" "$path"}
prune() { [ -d "$1" ] || return 0
find "$1" -type l | while IFS= read -r path; do case "$(readlink "$path")" in *".agents/"*) rm "$path" ;; esac done
find "$1" -type f -name "*.toml" | while IFS= read -r path; do if [ "$(head -n 1 "$path")" = "$GENERATED" ]; then rm "$path" fi done
find "$1" -mindepth 1 -type d -empty -delete rmdir "$1" 2>/dev/null || true}
prune_all() { prune ".claude" prune ".codex"}
link_skills() { for path in .agents/skills/*/SKILL.md; do link "$path" "$1/skills/$(basename "$(dirname "$path")")/SKILL.md" done}
setup_claude() { link ".agents/AGENTS.md" ".claude/CLAUDE.md"
for path in .agents/agents/*.md; do link "$path" ".claude/agents/$(basename "$path")" done
link_skills ".claude"}
frontmatter() { awk -v key="$2" ' NR == 1 && $0 == "---" { inside = 1; next } inside && $0 == "---" { exit } inside && index($0, key ": ") == 1 { print substr($0, length(key) + 3) } ' "$1"}
body() { awk ' NR == 1 && $0 == "---" { inside = 1; next } inside && $0 == "---" { inside = 0; started = 1; next } inside { next } started && !seen && $0 == "" { next } { seen = 1; print } ' "$1"}
toml_string() { printf '"%s"' "$(printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g')"}
# Codex reads agents as TOML, so a symlink will not do. Build one from the# markdown instead, and mark it so prune knows it can remove it.agent_to_toml() { source="$1" path="$2"
if body "$source" | grep -q "'''"; then echo "refusing to convert $source: its body holds ''' and cannot sit in a TOML literal string" >&2 exit 1 fi
if [ -e "$path" ] && [ "$(head -n 1 "$path")" != "$GENERATED" ]; then echo "refusing to replace $path: setup:agents did not write it" >&2 exit 1 fi
mkdir -p "$(dirname "$path")"
{ echo "$GENERATED" echo "name = $(toml_string "$(frontmatter "$source" name)")" echo "description = $(toml_string "$(frontmatter "$source" description)")" echo "developer_instructions = '''" body "$source" echo "'''" } >"$path"}
setup_codex() { for path in .agents/agents/*.md; do agent_to_toml "$path" ".codex/agents/$(basename "$path" .md).toml" done
link_skills ".codex"}
setup_opencode() { echo "opencode reads .agents/skills and AGENTS.md itself, nothing to link"}
harnesses=$(read_config_list "harnesses" | xargs)vcs=$(read_config "vcs")
if [ -z "$harnesses" ] || [ -z "$vcs" ] || [ -n "$overwrite" ]; then # shellcheck disable=SC2086 harnesses=$(choose_many "Which harnesses do you use? Space toggles, enter confirms." "${harnesses:-claude}" $HARNESSES) # shellcheck disable=SC2086 vcs=$(choose_one "Which version control do you use?" "${vcs:-$(guess_vcs)}" $PROFILES)
harnesses=$(echo "$harnesses" | xargs) vcs=$(echo "$vcs" | xargs)
{ echo "# Written by mise run setup:agents. Change it with --overwrite." echo "harnesses:"
for harness in $harnesses; do echo " - $harness" done
echo "vcs: $vcs" } >"$CONFIG"fi
if [ ! -f ".agents/profiles/vcs/$vcs.md" ]; then echo "no profile for $vcs. Pick one of: $PROFILES" >&2 exit 1fi
link ".agents/profiles/vcs/$vcs.md" ".agents/vcs.md"prune_all
for harness in $harnesses; do case " $HARNESSES " in *" $harness "*) "setup_$harness" ;; *) echo "no harness named $harness. Pick from: $HARNESSES" >&2 exit 1 ;; esacdone
echo "set up $harnesses against the $vcs profile"