ai-superpower/apply.sh
moilanik 407c0a560c refactor(apply.sh): extract functions + expand clean-code skill
apply.sh:
- refactor entire script into main() + 14 named functions
- fix BSD sed portability: \s → [[:space:]]
- fix set -e traps: replace [[ ]] && cmd with if blocks
- write_version_file in both scan and update mode
- print_summary only in scan mode
- keep 3 justified comments, remove all section headers

clean-code.instructions.md:
- add Newspaper Readability section
- add Single Level of Abstraction (SLAB) section
- add Declarative and Imperative Layers section
- rewrite Comments section (Allowed / Forbidden / The test)
- add guard clause guidance to Functions section
- add "always use braces" rule to Formatting
- add "avoid optical illusions" rule to Formatting
- replace Bash/Python examples with TypeScript
2026-03-09 09:35:27 +02:00

225 lines
7.0 KiB
Bash
Executable File
Raw 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
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 "$@"