From d364cf2a8520f733d9438fa1e32cb3010c655ee2 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Fri, 15 May 2026 17:22:28 -0500 Subject: 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/. --- Makefile | 6 ++ scripts/install-ai.sh | 165 ++++++++++++++++++++++++++++++++++++++++++++++++++ todo.org | 2 +- 3 files changed, 172 insertions(+), 1 deletion(-) create mode 100755 scripts/install-ai.sh 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=] [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 <&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== — bootstrap =.ai/= in a fresh project +*** DOING [#A] Add =make install-ai PROJECT== — 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. -- cgit v1.2.3