config.yaml: new repo-root config file with docs_folders list (docs, documentation, doc). apply.sh reads this list and picks the first existing folder per project instead of hardcoding docs/. Instructions: - core-principles: add No Vibe Coding and No Touching .ai/ sections - ai-root-instructions: add mandatory instructions block — rules stay active for the whole session, not just at start; AI must stop and announce if instructions were not loaded - project-context, docs: updated to list all docs folder alternatives and reference config.yaml as the source of truth FR-5.0 added to apply-requirements.md. README step 4 updated.
182 lines
7.4 KiB
Bash
Executable File
182 lines
7.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
REPO_URL="https://gitea.nikos-dev.keskikuja.site/niko/ai-superpower.git"
|
||
REPO_NAME="ai-superpower"
|
||
RAW_URL="https://gitea.nikos-dev.keskikuja.site/niko/ai-superpower/raw/branch/main/apply.sh"
|
||
|
||
# ── Bootstrap mode (curl | bash) ─────────────────────────────────────────────
|
||
# BASH_SOURCE[0] is empty when piped through bash
|
||
if [[ -z "${BASH_SOURCE[0]:-}" ]]; then
|
||
DEV_ROOT="$PWD" # capture before exec replaces the process
|
||
TARGET="$DEV_ROOT/$REPO_NAME"
|
||
EXTRA_FLAGS="${1:-}" # pass through --update if provided
|
||
if [[ -d "$TARGET" ]]; then
|
||
PREV_COMMIT="$(git -C "$TARGET" rev-parse --short HEAD 2>/dev/null || echo "")"
|
||
echo "\u2192 updating $REPO_NAME ..."
|
||
git -C "$TARGET" pull || { echo "\u2717 git pull failed"; exit 1; }
|
||
else
|
||
PREV_COMMIT=""
|
||
echo "\u2192 cloning $REPO_NAME into $TARGET ..."
|
||
git clone "$REPO_URL" "$TARGET" || { echo "\u2717 git clone failed"; exit 1; }
|
||
fi
|
||
# Pass DEV_ROOT and PREV_COMMIT explicitly
|
||
exec bash "$TARGET/apply.sh" --bootstrapped "$DEV_ROOT" ${EXTRA_FLAGS:+"$EXTRA_FLAGS"} "$PREV_COMMIT"
|
||
fi
|
||
|
||
# ── Guard — block direct invocation ──────────────────────────────────────────
|
||
if [[ "${1:-}" != "--bootstrapped" ]]; then
|
||
echo "✗ do not run this script directly."
|
||
echo " run from your dev root folder:"
|
||
echo ""
|
||
echo " curl -fsSL $RAW_URL | bash # full setup"
|
||
echo " curl -fsSL $RAW_URL | bash -s -- --update # update instructions only"
|
||
echo ""
|
||
exit 1
|
||
fi
|
||
|
||
# ── Local mode ────────────────────────────────────────────────────────────────
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
DEV_ROOT="${2:?DEV_ROOT not passed — re-run via curl}"
|
||
AI_TARGET="$SCRIPT_DIR/.ai"
|
||
TEMPLATE_MARKER="<!-- ai-superpower:template"
|
||
|
||
# Read docs folder alternatives from config.yaml
|
||
DOCS_FOLDERS=()
|
||
while IFS= read -r folder; do
|
||
DOCS_FOLDERS+=("$folder")
|
||
done < <(grep '^\s*-\s' "$SCRIPT_DIR/config.yaml" 2>/dev/null | sed 's/^\s*-\s*//')
|
||
[[ ${#DOCS_FOLDERS[@]} -eq 0 ]] && DOCS_FOLDERS=("docs")
|
||
# ── --update mode: instructions only, skip project setup ────────────────────
|
||
# git pull already ran in bootstrap; compare commits and report changes
|
||
if [[ "${3:-}" == "--update" ]]; then
|
||
COMMIT="$(git -C "$SCRIPT_DIR" rev-parse --short HEAD 2>/dev/null || echo "unknown")"
|
||
PREV_COMMIT="${4:-}"
|
||
echo ""
|
||
if [[ -z "$PREV_COMMIT" || "$PREV_COMMIT" == "$COMMIT" ]]; then
|
||
echo " version: $COMMIT (already up to date)"
|
||
else
|
||
echo " version: $PREV_COMMIT → $COMMIT"
|
||
echo " new commits:"
|
||
git -C "$SCRIPT_DIR" log --oneline "${PREV_COMMIT}..HEAD" | sed 's/^/ /'
|
||
fi
|
||
echo ""
|
||
exit 0
|
||
fi
|
||
# Counters
|
||
cnt_found=0
|
||
cnt_no_docs=0
|
||
cnt_templated=0
|
||
cnt_refreshed=0
|
||
|
||
setup_project() {
|
||
local project="$1"
|
||
local name
|
||
name="$(basename "$project")"
|
||
echo ""
|
||
echo "▸ $name"
|
||
|
||
# FR-3: symlink
|
||
local ai_link="$project/.ai"
|
||
if [[ -L "$ai_link" && "$(readlink "$ai_link")" == "$AI_TARGET" ]]; then
|
||
: # correct — skip silently
|
||
else
|
||
ln -sfn "$AI_TARGET" "$ai_link"
|
||
echo " ✓ .ai symlinked"
|
||
fi
|
||
|
||
# FR-4: .gitignore
|
||
local gitignore="$project/.gitignore"
|
||
if ! grep -qxF '.ai' "$gitignore" 2>/dev/null; then
|
||
echo '.ai' >> "$gitignore"
|
||
echo " ✓ .ai added to .gitignore"
|
||
fi
|
||
|
||
# FR-5 + FR-6: docs — find existing folder or use default
|
||
local docs=""
|
||
for folder in "${DOCS_FOLDERS[@]}"; do
|
||
if [[ -d "$project/$folder" ]]; then
|
||
docs="$project/$folder"
|
||
break
|
||
fi
|
||
done
|
||
[[ -z "$docs" ]] && docs="$project/${DOCS_FOLDERS[0]}"
|
||
|
||
if [[ ! -d "$docs" ]]; then
|
||
if [[ -t 0 ]] || [[ -e /dev/tty ]]; then
|
||
printf " ? no docs/ folder — create it? [y/N] "
|
||
read -r answer </dev/tty
|
||
if [[ "${answer,,}" == "y" ]]; then
|
||
mkdir -p "$docs"
|
||
echo " ✓ created docs/"
|
||
else
|
||
echo " – skipped"
|
||
(( cnt_no_docs++ )) || true
|
||
return
|
||
fi
|
||
else
|
||
echo " ⚠ no docs/ folder — skipping context setup"
|
||
(( cnt_no_docs++ )) || true
|
||
return
|
||
fi
|
||
fi
|
||
|
||
local got_template=0
|
||
|
||
if [[ ! -f "$docs/ai-context.md" ]]; then
|
||
cp "$SCRIPT_DIR/templates/ai-context.md" "$docs/ai-context.md"
|
||
echo " ✓ created docs/ai-context.md"
|
||
got_template=1
|
||
elif head -1 "$docs/ai-context.md" | grep -qF "$TEMPLATE_MARKER"; then
|
||
cp "$SCRIPT_DIR/templates/ai-context.md" "$docs/ai-context.md"
|
||
echo " ✓ refreshed docs/ai-context.md (was factory template)"
|
||
(( cnt_refreshed++ )) || true
|
||
fi
|
||
|
||
if [[ ! -f "$docs/architecture.md" ]]; then
|
||
cp "$SCRIPT_DIR/templates/architecture.md" "$docs/architecture.md"
|
||
echo " ✓ created docs/architecture.md"
|
||
got_template=1
|
||
elif head -1 "$docs/architecture.md" | grep -qF "$TEMPLATE_MARKER"; then
|
||
cp "$SCRIPT_DIR/templates/architecture.md" "$docs/architecture.md"
|
||
echo " ✓ refreshed docs/architecture.md (was factory template)"
|
||
(( cnt_refreshed++ )) || true
|
||
fi
|
||
|
||
(( got_template )) && (( cnt_templated++ )) || true
|
||
}
|
||
|
||
# ── Scan and run ──────────────────────────────────────────────────────────────
|
||
echo "→ scanning $DEV_ROOT for projects ..."
|
||
|
||
while IFS= read -r gitdir; do
|
||
project="$(cd "$(dirname "$gitdir")" && pwd)"
|
||
[[ -f "$project/.ai-superpower" ]] && continue
|
||
setup_project "$project"
|
||
(( cnt_found++ )) || true
|
||
done < <(find "$DEV_ROOT" -mindepth 2 -maxdepth 4 -name ".git" -type d 2>/dev/null || true)
|
||
|
||
# ── Summary ───────────────────────────────────────────────────────────────────
|
||
COMMIT="$(git -C "$SCRIPT_DIR" rev-parse --short HEAD 2>/dev/null || echo "unknown")"
|
||
PREV_COMMIT="$(grep '^commit:' "$DEV_ROOT/.ai-superpower.version" 2>/dev/null | awk '{print $2}' || echo "")"
|
||
|
||
echo ""
|
||
echo "────────────────────────────────────────"
|
||
if [[ -n "$PREV_COMMIT" && "$PREV_COMMIT" != "$COMMIT" ]]; then
|
||
echo " version: $PREV_COMMIT → $COMMIT"
|
||
elif [[ -n "$PREV_COMMIT" ]]; then
|
||
echo " version: $COMMIT (no change)"
|
||
else
|
||
echo " version: $COMMIT (first run)"
|
||
fi
|
||
echo " projects: $cnt_found found"
|
||
(( cnt_no_docs > 0 )) && echo " no docs/: $cnt_no_docs project(s) skipped (no docs/ folder)" || true
|
||
(( cnt_templated > 0 )) && echo " templated: $cnt_templated project(s) received new docs templates" || true
|
||
(( cnt_refreshed > 0 )) && echo " refreshed: $cnt_refreshed template file(s) updated (were still factory default)" || true
|
||
echo "────────────────────────────────────────"
|
||
(( cnt_found > 0 )) && echo "✅ done" || echo "⚠ no projects found — are you in the right directory?"
|
||
|
||
# FR-8: write version file
|
||
printf '# Written by apply.sh on every run — shows when it was last run and which version of ai-superpower was used.\ndate: %s\ncommit: %s\n' \
|
||
"$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$COMMIT" > "$DEV_ROOT/.ai-superpower.version"
|