chore(skills): centralize AI assistant config via symlinks (#9951)

Co-authored-by: Alan Buscaglia <gentlemanprogramming@gmail.com>
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
This commit is contained in:
Javier Grau
2026-04-21 03:29:42 -04:00
committed by GitHub
parent 858dfc2a00
commit d3a1df3473
2 changed files with 67 additions and 29 deletions
+50 -12
View File
@@ -1,10 +1,10 @@
#!/bin/bash #!/bin/bash
# Setup AI Skills for Prowler development # Setup AI Skills for Prowler development
# Configures AI coding assistants that follow agentskills.io standard: # Configures AI coding assistants that follow agentskills.io standard:
# - Claude Code: .claude/skills/ symlink + CLAUDE.md copies # - Claude Code: .claude/skills/ symlink + CLAUDE.md symlink
# - Gemini CLI: .gemini/skills/ symlink + GEMINI.md copies # - Gemini CLI: .gemini/skills/ symlink + GEMINI.md symlink
# - Codex (OpenAI): .codex/skills/ symlink + AGENTS.md (native) # - Codex (OpenAI): .codex/skills/ symlink + AGENTS.md (native)
# - GitHub Copilot: .github/copilot-instructions.md copy # - GitHub Copilot: .github/copilot-instructions.md symlink
# #
# Usage: # Usage:
# ./setup.sh # Interactive mode (select AI assistants) # ./setup.sh # Interactive mode (select AI assistants)
@@ -37,6 +37,28 @@ SETUP_COPILOT=false
# HELPER FUNCTIONS # HELPER FUNCTIONS
# ============================================================================= # =============================================================================
add_to_gitignore() {
local pattern="$1"
local gitignore_file="$REPO_ROOT/.gitignore"
local header="# AI Coding assistants assets"
# Create .gitignore if it doesn't exist
if [ ! -f "$gitignore_file" ]; then
touch "$gitignore_file"
fi
# Check if pattern exists (exact match or at end of file)
if ! grep -qxF "$pattern" "$gitignore_file"; then
# Check if header exists
if ! grep -qxF "$header" "$gitignore_file"; then
echo -e "\n\n$header" >> "$gitignore_file"
fi
echo "$pattern" >> "$gitignore_file"
echo -e "${GREEN} ✓ Added $pattern to .gitignore${NC}"
fi
}
show_help() { show_help() {
echo "Usage: $0 [OPTIONS]" echo "Usage: $0 [OPTIONS]"
echo "" echo ""
@@ -109,6 +131,7 @@ setup_claude() {
if [ ! -d "$REPO_ROOT/.claude" ]; then if [ ! -d "$REPO_ROOT/.claude" ]; then
mkdir -p "$REPO_ROOT/.claude" mkdir -p "$REPO_ROOT/.claude"
fi fi
add_to_gitignore ".claude/skills"
if [ -L "$target" ]; then if [ -L "$target" ]; then
rm "$target" rm "$target"
@@ -119,8 +142,9 @@ setup_claude() {
ln -s "$SKILLS_SOURCE" "$target" ln -s "$SKILLS_SOURCE" "$target"
echo -e "${GREEN} ✓ .claude/skills -> skills/${NC}" echo -e "${GREEN} ✓ .claude/skills -> skills/${NC}"
# Copy AGENTS.md to CLAUDE.md # Link AGENTS.md to CLAUDE.md
copy_agents_md "CLAUDE.md" link_agents_md "CLAUDE.md"
add_to_gitignore "CLAUDE.md"
} }
setup_gemini() { setup_gemini() {
@@ -129,6 +153,7 @@ setup_gemini() {
if [ ! -d "$REPO_ROOT/.gemini" ]; then if [ ! -d "$REPO_ROOT/.gemini" ]; then
mkdir -p "$REPO_ROOT/.gemini" mkdir -p "$REPO_ROOT/.gemini"
fi fi
add_to_gitignore ".gemini/skills"
if [ -L "$target" ]; then if [ -L "$target" ]; then
rm "$target" rm "$target"
@@ -139,8 +164,9 @@ setup_gemini() {
ln -s "$SKILLS_SOURCE" "$target" ln -s "$SKILLS_SOURCE" "$target"
echo -e "${GREEN} ✓ .gemini/skills -> skills/${NC}" echo -e "${GREEN} ✓ .gemini/skills -> skills/${NC}"
# Copy AGENTS.md to GEMINI.md # Link AGENTS.md to GEMINI.md
copy_agents_md "GEMINI.md" link_agents_md "GEMINI.md"
add_to_gitignore "GEMINI.md"
} }
setup_codex() { setup_codex() {
@@ -149,6 +175,7 @@ setup_codex() {
if [ ! -d "$REPO_ROOT/.codex" ]; then if [ ! -d "$REPO_ROOT/.codex" ]; then
mkdir -p "$REPO_ROOT/.codex" mkdir -p "$REPO_ROOT/.codex"
fi fi
add_to_gitignore ".codex/skills"
if [ -L "$target" ]; then if [ -L "$target" ]; then
rm "$target" rm "$target"
@@ -164,12 +191,19 @@ setup_codex() {
setup_copilot() { setup_copilot() {
if [ -f "$REPO_ROOT/AGENTS.md" ]; then if [ -f "$REPO_ROOT/AGENTS.md" ]; then
mkdir -p "$REPO_ROOT/.github" mkdir -p "$REPO_ROOT/.github"
cp "$REPO_ROOT/AGENTS.md" "$REPO_ROOT/.github/copilot-instructions.md"
# Link AGENTS.md -> .github/copilot-instructions.md
local target="$REPO_ROOT/.github/copilot-instructions.md"
ln -sf "../AGENTS.md" "$target"
echo -e "${GREEN} ✓ AGENTS.md -> .github/copilot-instructions.md${NC}" echo -e "${GREEN} ✓ AGENTS.md -> .github/copilot-instructions.md${NC}"
# Add specifically the file, NOT the .github folder
add_to_gitignore ".github/copilot-instructions.md"
fi fi
} }
copy_agents_md() { link_agents_md() {
local target_name="$1" local target_name="$1"
local agents_files local agents_files
local count=0 local count=0
@@ -179,11 +213,15 @@ copy_agents_md() {
for agents_file in $agents_files; do for agents_file in $agents_files; do
local agents_dir local agents_dir
agents_dir=$(dirname "$agents_file") agents_dir=$(dirname "$agents_file")
cp "$agents_file" "$agents_dir/$target_name"
# Create relative symlink
# Since files are in same dir, we can just link to basename
(cd "$agents_dir" && ln -sf "$(basename "$agents_file")" "$target_name")
count=$((count + 1)) count=$((count + 1))
done done
echo -e "${GREEN}Copied $count AGENTS.md -> $target_name${NC}" echo -e "${GREEN}Linked $count AGENTS.md -> $target_name${NC}"
} }
# ============================================================================= # =============================================================================
@@ -302,4 +340,4 @@ echo "Configured:"
[ "$SETUP_COPILOT" = true ] && echo " • GitHub Copilot: .github/copilot-instructions.md" [ "$SETUP_COPILOT" = true ] && echo " • GitHub Copilot: .github/copilot-instructions.md"
echo "" echo ""
echo -e "${BLUE}Note: Restart your AI assistant to load the skills.${NC}" echo -e "${BLUE}Note: Restart your AI assistant to load the skills.${NC}"
echo -e "${BLUE} AGENTS.md is the source of truth - edit it, then re-run this script.${NC}" echo -e "${BLUE} AGENTS.md is the source of truth - changes are reflected automatically via symlinks.${NC}"
+17 -17
View File
@@ -201,40 +201,40 @@ test_symlink_not_created_without_flag() {
} }
# ============================================================================= # =============================================================================
# TESTS: AGENTS.md COPYING # TESTS: AGENTS.md LINKING
# ============================================================================= # =============================================================================
test_copy_claude_agents_md() { test_link_claude_agents_md() {
run_setup --claude > /dev/null run_setup --claude > /dev/null
assert_file_exists "$TEST_DIR/CLAUDE.md" "Root CLAUDE.md should exist" && \ assert_symlink_exists "$TEST_DIR/CLAUDE.md" "Root CLAUDE.md should be a symlink" && \
assert_file_exists "$TEST_DIR/api/CLAUDE.md" "api/CLAUDE.md should exist" && \ assert_symlink_exists "$TEST_DIR/api/CLAUDE.md" "api/CLAUDE.md should be a symlink" && \
assert_file_exists "$TEST_DIR/ui/CLAUDE.md" "ui/CLAUDE.md should exist" assert_symlink_exists "$TEST_DIR/ui/CLAUDE.md" "ui/CLAUDE.md should be a symlink"
} }
test_copy_gemini_agents_md() { test_link_gemini_agents_md() {
run_setup --gemini > /dev/null run_setup --gemini > /dev/null
assert_file_exists "$TEST_DIR/GEMINI.md" "Root GEMINI.md should exist" && \ assert_symlink_exists "$TEST_DIR/GEMINI.md" "Root GEMINI.md should be a symlink" && \
assert_file_exists "$TEST_DIR/api/GEMINI.md" "api/GEMINI.md should exist" && \ assert_symlink_exists "$TEST_DIR/api/GEMINI.md" "api/GEMINI.md should be a symlink" && \
assert_file_exists "$TEST_DIR/ui/GEMINI.md" "ui/GEMINI.md should exist" assert_symlink_exists "$TEST_DIR/ui/GEMINI.md" "ui/GEMINI.md should be a symlink"
} }
test_copy_copilot_to_github() { test_link_copilot_to_github() {
run_setup --copilot > /dev/null run_setup --copilot > /dev/null
assert_file_exists "$TEST_DIR/.github/copilot-instructions.md" "Copilot instructions should exist" assert_symlink_exists "$TEST_DIR/.github/copilot-instructions.md" "Copilot instructions should be a symlink"
} }
test_copy_codex_no_extra_files() { test_link_codex_no_extra_files() {
run_setup --codex > /dev/null run_setup --codex > /dev/null
assert_file_not_exists "$TEST_DIR/CODEX.md" "CODEX.md should not be created" assert_file_not_exists "$TEST_DIR/CODEX.md" "CODEX.md should not be created"
} }
test_copy_not_created_without_flag() { test_link_not_created_without_flag() {
run_setup --codex > /dev/null run_setup --codex > /dev/null
assert_file_not_exists "$TEST_DIR/CLAUDE.md" "CLAUDE.md should not exist" && \ assert_symlink_not_exists "$TEST_DIR/CLAUDE.md" "CLAUDE.md should not exist" && \
assert_file_not_exists "$TEST_DIR/GEMINI.md" "GEMINI.md should not exist" assert_symlink_not_exists "$TEST_DIR/GEMINI.md" "GEMINI.md should not exist"
} }
test_copy_content_matches_source() { test_link_content_matches_source() {
run_setup --claude > /dev/null run_setup --claude > /dev/null
local source_content target_content local source_content target_content
source_content=$(cat "$TEST_DIR/AGENTS.md") source_content=$(cat "$TEST_DIR/AGENTS.md")
@@ -272,7 +272,7 @@ test_idempotent_multiple_runs() {
run_setup --claude > /dev/null run_setup --claude > /dev/null
run_setup --claude > /dev/null run_setup --claude > /dev/null
assert_symlink_exists "$TEST_DIR/.claude/skills" "Symlink should still exist after second run" && \ assert_symlink_exists "$TEST_DIR/.claude/skills" "Symlink should still exist after second run" && \
assert_file_exists "$TEST_DIR/CLAUDE.md" "CLAUDE.md should still exist after second run" assert_symlink_exists "$TEST_DIR/CLAUDE.md" "CLAUDE.md should still be a symlink after second run"
} }
# ============================================================================= # =============================================================================