#!/usr/bin/env bash # install-ai.sh — bootstrap .ai/ in a fresh project from canonical source. # # Refuses if PROJECT/.ai/ already exists. Use `make audit APPLY=1` to # update an existing .ai/ instead. # # Usage: install-ai.sh [--track | --gitignore] [PROJECT_PATH] # # If PROJECT_PATH is omitted, fzf-pick from ~/code/* + ~/projects/* git # checkouts that don't already have a .ai/ directory. set -euo pipefail REPO="$(cd "$(dirname "$0")/.." && pwd)" CANONICAL="$REPO/claude-templates/.ai" project="" track_mode="" for arg in "$@"; do case "$arg" in --track) track_mode="track" ;; --gitignore) track_mode="gitignore" ;; -h|--help) cat <&2 exit 2 ;; *) if [ -n "$project" ]; then echo "ERROR: multiple PROJECT_PATH arguments: $project and $arg" >&2 exit 2 fi project="$arg" ;; esac done # Project resolution: argument or fzf-pick. if [ -z "$project" ]; then if ! command -v fzf >/dev/null 2>&1; then echo "ERROR: PROJECT_PATH not given and fzf is not installed" >&2 exit 2 fi project=$( find "$HOME/code" "$HOME/projects" -maxdepth 2 -mindepth 1 -type d -name .git 2>/dev/null \ | sed 's|/\.git$||' \ | while read -r p; do [ ! -e "$p/.ai" ] && echo "$p" done \ | sort \ | fzf --prompt="Target project (without .ai/)> " ) if [ -z "$project" ]; then echo "No target selected." >&2 exit 1 fi fi # Validate project path. if [ ! -d "$project" ]; then echo "ERROR: $project is not a directory" >&2 exit 2 fi project="$(cd "$project" && pwd)" # Refuse if .ai/ already exists. if [ -e "$project/.ai" ]; then echo "ERROR: $project/.ai already exists" >&2 echo " Use 'make audit APPLY=1' to sync an existing .ai/ instead." >&2 exit 2 fi # Warn if not a git checkout. if [ ! -d "$project/.git" ]; then echo "WARN: $project is not a git checkout — track/gitignore handling will be skipped" fi # Decide track vs gitignore (skip if no .git). if [ -d "$project/.git" ] && [ -z "$track_mode" ]; then echo echo "Track .ai/ in git, or add to .gitignore?" echo " [t] track — .ai/ enters git history, session records persist for the team" echo " [g] gitignore — .ai/ stays local, session records private to this machine" printf "Choice [t/g]: " read -r answer case "$answer" in t|T|track) track_mode="track" ;; g|G|gitignore) track_mode="gitignore" ;; *) echo "ERROR: invalid choice (rerun with --track or --gitignore)" >&2 exit 2 ;; esac fi echo echo "Installing .ai/ at $project/.ai/ ..." # Create directory structure. mkdir -p "$project/.ai/sessions" mkdir -p "$project/.ai/references" mkdir -p "$project/.ai/retrospectives" # Rsync canonical content (everything except notes.org, which gets templated). rsync -a "$CANONICAL/protocols.org" "$project/.ai/protocols.org" rsync -a "$CANONICAL/someday-maybe.org" "$project/.ai/someday-maybe.org" rsync -a --delete "$CANONICAL/workflows/" "$project/.ai/workflows/" rsync -a --delete "$CANONICAL/scripts/" "$project/.ai/scripts/" # Seed notes.org with placeholder substitution. project_name="$(basename "$project")" today="$(date +%Y-%m-%d)" sed "s|\[Project Name\]|$project_name|g; s|\[Date\]|$today|g" \ "$CANONICAL/notes.org" > "$project/.ai/notes.org" # Tracking setup. case "$track_mode" in track) # .gitkeep files so otherwise-empty dirs survive in git. touch "$project/.ai/sessions/.gitkeep" touch "$project/.ai/references/.gitkeep" touch "$project/.ai/retrospectives/.gitkeep" ;; gitignore) if [ -f "$project/.gitignore" ] && grep -qFx '.ai/' "$project/.gitignore"; then : # already present else { [ -s "$project/.gitignore" ] && echo "" echo "# Claude Code per-project tooling" echo ".ai/" } >> "$project/.gitignore" fi ;; esac # Banner. echo echo "Done." echo echo " project: $project" echo " tracking: ${track_mode:-not-a-git-repo}" echo " notes.org: project=$project_name, date=$today" echo echo "Next steps:" echo " - Add a language bundle: make install-lang PROJECT=$project" echo " - Start a Claude session: cd $project && claude"