aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile6
-rwxr-xr-xscripts/install-ai.sh165
-rw-r--r--todo.org2
3 files changed, 172 insertions, 1 deletions
diff --git a/Makefile b/Makefile
index f269298..5474c67 100644
--- a/Makefile
+++ b/Makefile
@@ -407,6 +407,12 @@ audit: ## Verify project .ai/ dirs against canonical ([APPLY=1] [FORCE=1] [NO_DO
$(if $(FORCE),--force) \
$(if $(NO_DOCTOR),--no-doctor)
+install-ai: ## Bootstrap .ai/ in a fresh project ([PROJECT=<path>] [TRACK=1 | GITIGNORE=1])
+ @bash scripts/install-ai.sh \
+ $(if $(TRACK),--track) \
+ $(if $(GITIGNORE),--gitignore) \
+ $(PROJECT)
+
test: ## Run the .ai/scripts/ test suites (pytest + ERT)
@cd .ai/scripts/tests && python3 -m pytest
@set -e; for f in .ai/scripts/tests/test-*.el; do \
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"
diff --git a/todo.org b/todo.org
index 6b2a957..42d8e39 100644
--- a/todo.org
+++ b/todo.org
@@ -1864,7 +1864,7 @@ Exit code: =0= if all clean, no skips, no failures. =1= otherwise.
=doctor= has a clean meaning today: "is this machine's =~/.claude/= consistent with rulesets?" Mixing in cross-project =.ai/= drift muddies the exit code. Keep them separate. =audit= can optionally invoke =doctor= as its last check since both ask "did the symlinks keep up with the source?". A future =make all-checks= can wrap both.
-*** TODO [#A] Add =make install-ai PROJECT=<path>= — bootstrap =.ai/= in a fresh project
+*** DOING [#A] Add =make install-ai PROJECT=<path>= — bootstrap =.ai/= in a fresh project
Separate target from =audit= because operating on projects that lack =.ai/= is a distinct action. The absence might be intentional, so =audit= skips them. Bootstrap is explicit opt-in.