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