Files
bootstrap/tools/bootstrap/agent-upgrade.sh
2026-02-22 17:52:23 +00:00

333 lines
9.6 KiB
Bash
Executable File

#!/bin/bash
# agent-upgrade.sh — Non-destructively upgrade agent configuration in projects
#
# Usage:
# agent-upgrade.sh <project-path> # Upgrade one project
# agent-upgrade.sh --all # Upgrade all projects in ~/src/
# agent-upgrade.sh --all --dry-run # Preview what would change
# agent-upgrade.sh <path> --section conditional-loading # Inject specific section
# agent-upgrade.sh <path> --create-agents # Create AGENTS.md if missing
# agent-upgrade.sh <path> --monorepo-scan # Create sub-AGENTS.md for monorepo dirs
#
# Safety:
# - Creates .bak backup before any modification
# - Append-only — never modifies existing sections
# - --dry-run shows what would change without writing
set -euo pipefail
# Defaults
SRC_DIR="$HOME/src"
FRAGMENTS_DIR="$HOME/.config/mosaic/templates/agent/fragments"
TEMPLATES_DIR="$HOME/.config/mosaic/templates/agent"
DRY_RUN=false
ALL_PROJECTS=false
TARGET_PATH=""
SECTION_ONLY=""
CREATE_AGENTS=false
MONOREPO_SCAN=false
# Exclusion patterns (same as agent-lint.sh)
EXCLUDE_PATTERNS=(
"_worktrees"
".backup"
"_old"
"_bak"
"junk"
"traefik"
"infrastructure"
)
# Colors
GREEN='\033[0;32m' RED='\033[0;31m' YELLOW='\033[0;33m'
NC='\033[0m' BOLD='\033[1m' DIM='\033[2m'
# Parse args
while [[ $# -gt 0 ]]; do
case "$1" in
--all) ALL_PROJECTS=true; shift ;;
--dry-run) DRY_RUN=true; shift ;;
--section) SECTION_ONLY="$2"; shift 2 ;;
--create-agents) CREATE_AGENTS=true; shift ;;
--monorepo-scan) MONOREPO_SCAN=true; shift ;;
--src-dir) SRC_DIR="$2"; shift 2 ;;
-h|--help)
echo "Usage: agent-upgrade.sh [<project-path>|--all] [--dry-run] [--section <name>] [--create-agents] [--monorepo-scan]"
echo ""
echo "Options:"
echo " --all Upgrade all projects in ~/src/"
echo " --dry-run Preview changes without writing"
echo " --section <name> Inject only a specific fragment (conditional-loading, commit-format, secrets, multi-agent, code-review, campsite-rule)"
echo " --create-agents Create AGENTS.md if missing"
echo " --monorepo-scan Create sub-AGENTS.md for monorepo directories"
exit 0
;;
*)
if [[ -d "$1" ]]; then
TARGET_PATH="$1"
else
echo "Unknown option or invalid path: $1"
exit 1
fi
shift
;;
esac
done
if ! $ALL_PROJECTS && [[ -z "$TARGET_PATH" ]]; then
echo "Error: Specify a project path or use --all"
exit 1
fi
# Helpers
is_coding_project() {
local dir="$1"
[[ -f "$dir/package.json" ]] || \
[[ -f "$dir/pyproject.toml" ]] || \
[[ -f "$dir/Cargo.toml" ]] || \
[[ -f "$dir/go.mod" ]] || \
[[ -f "$dir/pom.xml" ]] || \
[[ -f "$dir/build.gradle" ]]
}
is_excluded() {
local dir_name
dir_name=$(basename "$1")
for pattern in "${EXCLUDE_PATTERNS[@]}"; do
[[ "$dir_name" == *"$pattern"* ]] && return 0
done
return 1
}
is_monorepo() {
local dir="$1"
[[ -f "$dir/pnpm-workspace.yaml" ]] || \
[[ -f "$dir/turbo.json" ]] || \
[[ -f "$dir/lerna.json" ]] || \
(grep -q '"workspaces"' "$dir/package.json" 2>/dev/null)
}
has_section() {
local file="$1"
local pattern="$2"
[[ -f "$file" ]] && grep -qi "$pattern" "$file" 2>/dev/null
}
runtime_context_file() {
local project_dir="$1"
if [[ -f "$project_dir/CLAUDE.md" ]]; then
echo "$project_dir/CLAUDE.md"
return
fi
if [[ -f "$project_dir/RUNTIME.md" ]]; then
echo "$project_dir/RUNTIME.md"
return
fi
echo "$project_dir/CLAUDE.md"
}
backup_file() {
local file="$1"
if [[ -f "$file" ]] && ! $DRY_RUN; then
cp "$file" "${file}.bak"
fi
}
# Inject a fragment into CLAUDE.md if the section doesn't exist
inject_fragment() {
local project_dir="$1"
local fragment_name="$2"
local ctx_file
ctx_file="$(runtime_context_file "$project_dir")"
local fragment_file="$FRAGMENTS_DIR/$fragment_name.md"
if [[ ! -f "$fragment_file" ]]; then
echo -e " ${RED}Fragment not found: $fragment_file${NC}"
return 1
fi
# Determine detection pattern for this fragment
local detect_pattern
case "$fragment_name" in
conditional-loading) detect_pattern="agent-guides\|~/.config/mosaic/guides\|Conditional.*Loading\|Conditional.*Documentation\|Conditional.*Context" ;;
commit-format) detect_pattern="<type>.*#issue\|Types:.*feat.*fix" ;;
secrets) detect_pattern="NEVER hardcode secrets\|\.env.example.*committed" ;;
multi-agent) detect_pattern="Multi-Agent Coordination\|pull --rebase.*before" ;;
code-review) detect_pattern="codex-code-review\|codex-security-review\|Code Review" ;;
campsite-rule) detect_pattern="Campsite Rule\|Touching it makes it yours\|was already there.*NEVER" ;;
*) echo "Unknown fragment: $fragment_name"; return 1 ;;
esac
if [[ ! -f "$ctx_file" ]]; then
echo -e " ${YELLOW}No runtime context file (CLAUDE.md/RUNTIME.md) — skipping fragment injection${NC}"
return 0
fi
if has_section "$ctx_file" "$detect_pattern"; then
echo -e " ${DIM}$fragment_name already present${NC}"
return 0
fi
if $DRY_RUN; then
echo -e " ${GREEN}Would inject: $fragment_name${NC}"
else
backup_file "$ctx_file"
echo "" >> "$ctx_file"
cat "$fragment_file" >> "$ctx_file"
echo "" >> "$ctx_file"
echo -e " ${GREEN}Injected: $fragment_name${NC}"
fi
}
# Create AGENTS.md from template
create_agents_md() {
local project_dir="$1"
local agents_md="$project_dir/AGENTS.md"
if [[ -f "$agents_md" ]]; then
echo -e " ${DIM}AGENTS.md already exists${NC}"
return 0
fi
local project_name
project_name=$(basename "$project_dir")
# Detect project type for quality gates
local quality_gates="# Add quality gate commands here"
if [[ -f "$project_dir/package.json" ]]; then
quality_gates="npm run lint && npm run typecheck && npm test"
if grep -q '"pnpm"' "$project_dir/package.json" 2>/dev/null || [[ -f "$project_dir/pnpm-lock.yaml" ]]; then
quality_gates="pnpm lint && pnpm typecheck && pnpm test"
fi
elif [[ -f "$project_dir/pyproject.toml" ]]; then
quality_gates="uv run ruff check src/ tests/ && uv run mypy src/ && uv run pytest --cov"
fi
if $DRY_RUN; then
echo -e " ${GREEN}Would create: AGENTS.md${NC}"
else
# Use generic AGENTS.md template with substitutions
sed -e "s/\${PROJECT_NAME}/$project_name/g" \
-e "s/\${QUALITY_GATES}/$quality_gates/g" \
-e "s/\${TASK_PREFIX}/${project_name^^}/g" \
-e "s|\${SOURCE_DIR}|src|g" \
"$TEMPLATES_DIR/AGENTS.md.template" > "$agents_md"
echo -e " ${GREEN}Created: AGENTS.md${NC}"
fi
}
# Create sub-AGENTS.md for monorepo directories
create_sub_agents() {
local project_dir="$1"
if ! is_monorepo "$project_dir"; then
echo -e " ${DIM}Not a monorepo — skipping sub-AGENTS scan${NC}"
return 0
fi
local created=0
for subdir_type in apps packages services plugins; do
if [[ -d "$project_dir/$subdir_type" ]]; then
for subdir in "$project_dir/$subdir_type"/*/; do
[[ -d "$subdir" ]] || continue
# Only if it has its own manifest
if [[ -f "$subdir/package.json" ]] || [[ -f "$subdir/pyproject.toml" ]]; then
if [[ ! -f "$subdir/AGENTS.md" ]]; then
local dir_name
dir_name=$(basename "$subdir")
if $DRY_RUN; then
echo -e " ${GREEN}Would create: $subdir_type/$dir_name/AGENTS.md${NC}"
else
sed -e "s/\${DIRECTORY_NAME}/$dir_name/g" \
-e "s/\${DIRECTORY_PURPOSE}/Part of the $subdir_type layer./g" \
"$TEMPLATES_DIR/sub-agents.md.template" > "${subdir}AGENTS.md"
echo -e " ${GREEN}Created: $subdir_type/$dir_name/AGENTS.md${NC}"
fi
((created++)) || true
fi
fi
done
fi
done
if [[ $created -eq 0 ]]; then
echo -e " ${DIM}All monorepo sub-AGENTS.md present${NC}"
fi
}
# Upgrade a single project
upgrade_project() {
local dir="$1"
local name
name=$(basename "$dir")
echo -e "\n${BOLD}$name${NC} ${DIM}($dir)${NC}"
if [[ -n "$SECTION_ONLY" ]]; then
inject_fragment "$dir" "$SECTION_ONLY"
return
fi
# Always try conditional-loading (highest impact)
inject_fragment "$dir" "conditional-loading"
# Try other fragments if runtime context exists
if [[ -f "$dir/CLAUDE.md" || -f "$dir/RUNTIME.md" ]]; then
inject_fragment "$dir" "commit-format"
inject_fragment "$dir" "secrets"
inject_fragment "$dir" "multi-agent"
inject_fragment "$dir" "code-review"
inject_fragment "$dir" "campsite-rule"
fi
# Create AGENTS.md if missing (always unless --section was used)
if $CREATE_AGENTS || [[ -z "$SECTION_ONLY" ]]; then
create_agents_md "$dir"
fi
# Monorepo sub-AGENTS.md
if $MONOREPO_SCAN || [[ -z "$SECTION_ONLY" ]]; then
create_sub_agents "$dir"
fi
}
# Main
main() {
local projects=()
if $ALL_PROJECTS; then
for dir in "$SRC_DIR"/*/; do
[[ -d "$dir" ]] || continue
is_excluded "$dir" && continue
is_coding_project "$dir" && projects+=("${dir%/}")
done
else
projects=("$TARGET_PATH")
fi
if [[ ${#projects[@]} -eq 0 ]]; then
echo "No coding projects found."
exit 0
fi
local mode="LIVE"
$DRY_RUN && mode="DRY RUN"
echo -e "${BOLD}Agent Configuration Upgrade — $(date +%Y-%m-%d) [$mode]${NC}"
echo "========================================================"
for dir in "${projects[@]}"; do
upgrade_project "$dir"
done
echo ""
echo -e "${BOLD}Done.${NC}"
if $DRY_RUN; then
echo -e "${DIM}Run without --dry-run to apply changes.${NC}"
else
echo -e "${DIM}Backups saved as .bak files. Run agent-lint.sh to verify.${NC}"
fi
}
main