ai-superpower/apply.sh
moilanik 0e6c748374 fix: skip ai-superpower repo itself during project scan
apply.sh was scanning its own directory and creating a recursive .ai
symlink inside .ai/, causing infinite folder nesting in VS Code.

- Skip $SCRIPT_DIR in scan_projects() to prevent self-symlinking
- Remove incorrectly created .ai/.ai symlink and .ai/.gitignore
- Add -root session reset command to ai-root-instructions.md
- Document -root command in README
2026-03-12 10:29:55 +02:00

226 lines
7.1 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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"
# BASH_SOURCE[0] is empty when piped through bash — this is the bootstrap entry point
if [[ -z "${BASH_SOURCE[0]:-}" ]]; then
DEV_ROOT="$PWD" # capture before exec replaces the process
TARGET="$DEV_ROOT/$REPO_NAME"
EXTRA_FLAGS="${1:-}"
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
exec bash "$TARGET/apply.sh" --bootstrapped "$DEV_ROOT" ${EXTRA_FLAGS:+"$EXTRA_FLAGS"} "$PREV_COMMIT"
fi
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
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
AI_TARGET="$SCRIPT_DIR/.ai"
TEMPLATE_MARKER="<!-- ai-superpower:template"
DOCS_FOLDERS=()
RESOLVED_DOCS_DIR=""
projects_found=0
projects_skipped_no_docs=0
projects_templated=0
doc_files_refreshed=0
load_docs_folders() {
while IFS= read -r folder; do
DOCS_FOLDERS+=("$folder")
done < <(grep '^\ *-\ ' "$SCRIPT_DIR/config.yaml" 2>/dev/null | sed 's/^[[:space:]]*-[[:space:]]*//')
if [[ ${#DOCS_FOLDERS[@]} -eq 0 ]]; then DOCS_FOLDERS=("docs"); fi
}
current_commit() {
git -C "$SCRIPT_DIR" rev-parse --short HEAD 2>/dev/null || echo "unknown"
}
print_version_line() {
local prev="$1" current="$2"
if [[ -z "$prev" ]]; then echo " version: $current (first run)"
elif [[ "$prev" == "$current" ]]; then echo " version: $current (no change)"
else echo " version: $prev$current"
fi
}
handle_update_mode() {
local prev_commit="$1"
local commit
commit="$(current_commit)"
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 ""
}
ensure_symlink() {
local project="$1"
local ai_link="$project/.ai"
if [[ -L "$ai_link" && "$(readlink "$ai_link")" == "$AI_TARGET" ]]; then
return
fi
ln -sfn "$AI_TARGET" "$ai_link"
echo " ✓ .ai symlinked"
}
ensure_gitignore() {
local project="$1"
local gitignore="$project/.gitignore"
if ! grep -qxF '.ai' "$gitignore" 2>/dev/null; then
echo '.ai' >> "$gitignore"
echo " ✓ .ai added to .gitignore"
fi
}
resolve_docs_dir() {
local project="$1"
RESOLVED_DOCS_DIR=""
local docs=""
for folder in "${DOCS_FOLDERS[@]}"; do
if [[ -d "$project/$folder" ]]; then
docs="$project/$folder"
break
fi
done
if [[ -z "$docs" ]]; then docs="$project/${DOCS_FOLDERS[0]}"; fi
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"
(( projects_skipped_no_docs++ )) || true
return 1
fi
else
echo " ⚠ no docs/ folder — skipping context setup"
(( projects_skipped_no_docs++ )) || true
return 1
fi
fi
RESOLVED_DOCS_DIR="$docs"
}
# sets got_new=1 via dynamic scope — caller must declare 'local got_new' before calling
ensure_doc_file() {
local docs_dir="$1" filename="$2"
local target="$docs_dir/$filename"
if [[ ! -f "$target" ]]; then
cp "$SCRIPT_DIR/templates/$filename" "$target"
echo " ✓ created $filename"
got_new=1
elif head -1 "$target" | grep -qF "$TEMPLATE_MARKER"; then
cp "$SCRIPT_DIR/templates/$filename" "$target"
echo " ✓ refreshed $filename (was factory template)"
(( doc_files_refreshed++ )) || true
fi
}
ensure_doc_files() {
local docs_dir="$1"
local got_new=0
ensure_doc_file "$docs_dir" "ai-context.md"
ensure_doc_file "$docs_dir" "architecture.md"
(( got_new )) && (( projects_templated++ )) || true
}
setup_project() {
local project="$1"
echo ""
echo "$(basename "$project")"
ensure_symlink "$project"
ensure_gitignore "$project"
resolve_docs_dir "$project" || return 0
ensure_doc_files "$RESOLVED_DOCS_DIR"
}
update_mode=false
prev_commit=""
parse_args() {
for arg in "$@"; do
case "$arg" in
--update) update_mode=true ;;
*) if [[ -n "$arg" ]]; then prev_commit="$arg"; fi ;;
esac
done
}
scan_projects() {
local dev_root="$1"
echo "→ scanning $dev_root for projects ..."
while IFS= read -r gitdir; do
project="$(cd "$(dirname "$gitdir")" && pwd)"
if [[ -f "$project/.ai-superpower" ]]; then continue; fi
if [[ "$project" == "$SCRIPT_DIR" ]]; then continue; fi
setup_project "$project"
(( projects_found++ )) || true
done < <(find "$dev_root" -mindepth 2 -maxdepth 4 -name ".git" -type d 2>/dev/null || true)
}
print_summary() {
local dev_root="$1"
local commit saved_commit
commit="$(current_commit)"
saved_commit="$(grep '^commit:' "$dev_root/.ai-superpower.version" 2>/dev/null | awk '{print $2}' || echo "")"
echo ""
echo "────────────────────────────────────────"
print_version_line "$saved_commit" "$commit"
echo " projects: $projects_found found"
(( projects_skipped_no_docs > 0 )) && echo " no docs/: $projects_skipped_no_docs project(s) skipped (no docs/ folder)" || true
(( projects_templated > 0 )) && echo " templated: $projects_templated project(s) received new docs templates" || true
(( doc_files_refreshed > 0 )) && echo " refreshed: $doc_files_refreshed template file(s) updated (were still factory default)" || true
echo "────────────────────────────────────────"
(( projects_found > 0 )) && echo "✅ done" || echo "⚠ no projects found — are you in the right directory?"
}
write_version_file() {
local dev_root="$1"
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')" "$(current_commit)" > "$dev_root/.ai-superpower.version"
}
main() {
local dev_root="${2:?DEV_ROOT not passed — re-run via curl}"
parse_args "${@:3}"
load_docs_folders
if [[ "$update_mode" == true ]]; then
handle_update_mode "$prev_commit"
else
scan_projects "$dev_root"
print_summary "$dev_root"
fi
write_version_file "$dev_root"
}
main "$@"