#!/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 ;; esac done 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 1 fi 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 ;; esac done echo "set up $harnesses against the $vcs profile"