aboutsummaryrefslogtreecommitdiff
path: root/scripts/install-ai.sh
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-15 17:22:28 -0500
committerCraig Jennings <c@cjennings.net>2026-05-15 17:22:28 -0500
commitd364cf2a8520f733d9438fa1e32cb3010c655ee2 (patch)
tree23c0a91ab4f5028d0728be708cf45f1b7da59990 /scripts/install-ai.sh
parent94782eea3df22289fb556481f9569a9284c7ac50 (diff)
downloadrulesets-d364cf2a8520f733d9438fa1e32cb3010c655ee2.tar.gz
rulesets-d364cf2a8520f733d9438fa1e32cb3010c655ee2.zip
feat(make): add install-ai target for bootstrapping .ai/ in fresh projects
scripts/install-ai.sh copies canonical .ai/ content from claude-templates/ into a fresh project. Rsyncs protocols.org, workflows/, scripts/, someday-maybe.org as-is; templates notes.org with project-name and date placeholders substituted; creates empty sessions/, references/, retrospectives/ dirs. Two tracking modes: TRACK=1 adds .gitkeep files inside otherwise-empty dirs so they survive in git history; GITIGNORE=1 appends .ai/ to the project's .gitignore so session records stay local. Prompts interactively if neither flag is set. Refuses if PROJECT/.ai/ already exists with a message pointing to `make audit APPLY=1` for sync of existing installs. Without a PROJECT argument, fzf-picks from ~/code/* + ~/projects/* git checkouts that don't already have .ai/.
Diffstat (limited to 'scripts/install-ai.sh')
-rwxr-xr-xscripts/install-ai.sh165
1 files changed, 165 insertions, 0 deletions
diff --git a/scripts/install-ai.sh b/scripts/install-ai.sh
new file mode 100755
index 0000000..d30ab84
--- /dev/null
+++ b/scripts/install-ai.sh
@@ -0,0 +1,165 @@
+#!/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 <<EOF
+Usage: $(basename "$0") [--track | --gitignore] [PROJECT_PATH]
+
+Bootstrap .ai/ in PROJECT_PATH from canonical content at $CANONICAL.
+
+ --track Track .ai/ in the project's git history (with .gitkeep
+ files inside otherwise-empty sessions/, references/,
+ retrospectives/).
+ --gitignore Append .ai/ to the project's .gitignore so session
+ records stay local.
+
+If neither flag is given, the script prompts interactively.
+If PROJECT_PATH is omitted, fzf-picks from ~/code/* + ~/projects/*
+git checkouts without an existing .ai/.
+EOF
+ exit 0
+ ;;
+ -*)
+ echo "ERROR: unknown flag: $arg (use --help for usage)" >&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"