diff --git a/.agents/config b/.agents/config new file mode 100644 index 0000000..6c9f7b9 --- /dev/null +++ b/.agents/config @@ -0,0 +1,4 @@ +# sync-agents configuration +# Comma-separated list of sync targets (available: claude, windsurf, cursor, copilot) +# Override per-command with: sync-agents sync --targets claude,cursor +targets = claude,windsurf,cursor,copilot diff --git a/.agents/rules/bash.md b/.agents/rules/bash.md index 312b63e..d4e89d4 100644 --- a/.agents/rules/bash.md +++ b/.agents/rules/bash.md @@ -1,4 +1,3 @@ - --- trigger: always_on --- diff --git a/.agents/rules/commitlint.md b/.agents/rules/commitlint.md new file mode 100644 index 0000000..fe8eb4a --- /dev/null +++ b/.agents/rules/commitlint.md @@ -0,0 +1,21 @@ +--- +trigger: git commit +--- + +# commitlint Rule + +Trigger: On git commit or PR event + +Purpose: +- Enforce commit message standards as per commitlint + +Conditions: +- Trigger on new commit or PR creation + +Actions: +- Run commitlint CLI against commit message +- If commit message fails, flag as non-compliant +- Notify agent or user for correction +- Block merge or commit if possible + +--- diff --git a/.agents/rules/security.md b/.agents/rules/security.md index a934148..9991c97 100644 --- a/.agents/rules/security.md +++ b/.agents/rules/security.md @@ -1,4 +1,3 @@ - --- trigger: always_on --- diff --git a/.gitignore b/.gitignore index cf3ce67..ebdfb44 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,15 @@ temp yarn.lock # package-lock.json # OIDC dist/ +# sync-agents — ignore tool artifacts, keep symlinks +.cursor/* +!.cursor/rules +.codex/* +!.codex/instructions.md +.github/copilot/* +!.github/copilot/instructions.md +.claude/ +.windsurf/ +.cursor/ +.github/copilot/ +CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md index 5d82217..a23ad2e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,6 +13,7 @@ This file indexes all rules, skills, and workflows defined in `.agents/`. ## Rules - [bash](.agents/rules/bash.md) +- [commitlint](.agents/rules/commitlint.md) - [security](.agents/rules/security.md) ## Skills diff --git a/src/sh/sync-agents.sh b/src/sh/sync-agents.sh index 53508ec..c23b70d 100755 --- a/src/sh/sync-agents.sh +++ b/src/sh/sync-agents.sh @@ -184,7 +184,6 @@ cmd_init() { else # Inline fallback if template not found cat > "$PROJECT_ROOT/$AGENTS_DIR/STATE.md" <<'STATE_EOF' - --- trigger: always_on --- @@ -222,6 +221,9 @@ CONFIG_EOF warn "$AGENTS_MD already exists, skipping (run 'sync-agents index' to regenerate)" fi + # Add default .gitignore entries for agent tool directories + add_default_gitignore_entries + info "Initialization complete. Directory structure:" print_tree "$PROJECT_ROOT/$AGENTS_DIR" } @@ -270,7 +272,6 @@ cmd_add() { sed "s/\${NAME}/$name/g" "$TEMPLATES_DIR/RULE_TEMPLATE.md" > "$filepath" else cat > "$filepath" </dev/null; then + # Add .DS_Store if not present + if [[ -s "$gitignore" ]] && ! tail -c1 "$gitignore" | grep -q '^$'; then + echo "" >> "$gitignore" + fi + echo ".DS_Store" >> "$gitignore" + info "Added .DS_Store to .gitignore" + fi + + # Define default entries (tool artifacts, not symlinks) + # Using pattern: ignore everything in dir, except specific files we want to track + local marker="# sync-agents — ignore tool artifacts, keep symlinks" + + # Check if sync-agents section already exists + if grep -qF "$marker" "$gitignore"; then + # Section exists - check if we need to add any missing entries + local needs_update=false + + # Check for each pattern + if ! grep -qF ".cursor/*" "$gitignore"; then needs_update=true; fi + if ! grep -qF "!.cursor/rules" "$gitignore"; then needs_update=true; fi + if ! grep -qF ".codex/*" "$gitignore"; then needs_update=true; fi + if ! grep -qF "!.codex/instructions.md" "$gitignore"; then needs_update=true; fi + if ! grep -qF ".github/copilot/*" "$gitignore"; then needs_update=true; fi + if ! grep -qF "!.github/copilot/instructions.md" "$gitignore"; then needs_update=true; fi + + if [[ "$needs_update" == "true" ]]; then + # Rebuild section by reading the file, preserving everything else + local tmp + tmp="$(mktemp)" + local in_section=false + + while IFS= read -r line; do + if [[ "$line" == "$marker" ]]; then + in_section=true + # Output the marker + { + echo "$line" + echo ".cursor/*" + echo "!.cursor/rules" + echo ".codex/*" + echo "!.codex/instructions.md" + echo ".github/copilot/*" + echo "!.github/copilot/instructions.md" + } >> "$tmp" + continue + fi + + # Skip old entries in the sync-agents section (until we hit empty line or new section) + if [[ "$in_section" == "true" ]]; then + if [[ -z "$line" ]] || [[ "$line" == "#"* ]]; then + in_section=false + echo "$line" >> "$tmp" + fi + # Skip old entry lines (they're replaced above) + continue + fi + + echo "$line" >> "$tmp" + done < "$gitignore" + + mv "$tmp" "$gitignore" + info "Updated sync-agents section in .gitignore" + fi + else + # Section doesn't exist, add entire block + # Add separator if file is non-empty + if [[ -s "$gitignore" ]] && ! tail -c1 "$gitignore" | grep -q '^$'; then + echo "" >> "$gitignore" + fi + + # Add all entries + { + echo "$marker" + echo ".cursor/*" + echo "!.cursor/rules" + echo ".codex/*" + echo "!.codex/instructions.md" + echo ".github/copilot/*" + echo "!.github/copilot/instructions.md" + } >> "$gitignore" + + info "Added sync-agents section to .gitignore with 7 entries" + fi +} + +update_gitignore() { + local gitignore="$PROJECT_ROOT/.gitignore" + + # Build list of entries that should be ignored (synced symlinks) + local entries=() + for target in "${ACTIVE_TARGETS[@]}"; do + local target_dir + target_dir="$(resolve_target_dir "$target" "$PROJECT_ROOT")" + local rel_path="${target_dir#"$PROJECT_ROOT"/}/" + entries+=("$rel_path") + done + entries+=("CLAUDE.md") + + if [[ "$DRY_RUN" == "true" ]]; then + for entry in "${entries[@]}"; do + if [[ ! -f "$gitignore" ]] || ! grep -qxF "$entry" "$gitignore"; then + echo " would add to .gitignore: $entry" + fi + done + return 0 + fi + + # Create .gitignore if it doesn't exist + [[ -f "$gitignore" ]] || touch "$gitignore" + + local added=0 + for entry in "${entries[@]}"; do + if ! grep -qxF "$entry" "$gitignore"; then + # Add sync-agents header on first addition + if [[ "$added" -eq 0 ]]; then + # Check if header already exists + if ! grep -qF "# sync-agents" "$gitignore"; then + # Add a blank line separator if file is non-empty + if [[ -s "$gitignore" ]]; then + echo "" >> "$gitignore" + fi + echo "# sync-agents (generated symlinks)" >> "$gitignore" + fi + fi + echo "$entry" >> "$gitignore" + added=$((added + 1)) + fi + done + + if [[ "$added" -gt 0 ]]; then + info "Added $added entries to .gitignore" + fi +} + cmd_status() { echo -e "${BOLD}sync-agents${RESET} v${VERSION}" echo "" @@ -749,7 +903,6 @@ generate_agents_md() { fi cat > "$outfile" <<'HEADER' - --- trigger: always_on --- diff --git a/test/sync-agents.bats b/test/sync-agents.bats index 4716769..32d8fa1 100644 --- a/test/sync-agents.bats +++ b/test/sync-agents.bats @@ -588,6 +588,60 @@ teardown() { [ ! -d "$TEST_DIR/.windsurf" ] } +# -------------------------------------------------------------------------- +# .gitignore +# -------------------------------------------------------------------------- + +@test "sync adds symlink entries to .gitignore" { + bash "$SCRIPT" -d "$TEST_DIR" init + bash "$SCRIPT" -d "$TEST_DIR" sync + [ -f "$TEST_DIR/.gitignore" ] + grep -qxF ".claude/" "$TEST_DIR/.gitignore" + grep -qxF ".windsurf/" "$TEST_DIR/.gitignore" + grep -qxF ".cursor/" "$TEST_DIR/.gitignore" + grep -qxF ".github/copilot/" "$TEST_DIR/.gitignore" + grep -qxF "CLAUDE.md" "$TEST_DIR/.gitignore" +} + +@test "sync adds header comment to .gitignore" { + bash "$SCRIPT" -d "$TEST_DIR" init + bash "$SCRIPT" -d "$TEST_DIR" sync + grep -qF "# sync-agents" "$TEST_DIR/.gitignore" +} + +@test "sync does not duplicate .gitignore entries" { + bash "$SCRIPT" -d "$TEST_DIR" init + bash "$SCRIPT" -d "$TEST_DIR" sync + bash "$SCRIPT" -d "$TEST_DIR" sync + local count + count=$(grep -cxF "CLAUDE.md" "$TEST_DIR/.gitignore") + [ "$count" -eq 1 ] +} + +@test "sync preserves existing .gitignore content" { + bash "$SCRIPT" -d "$TEST_DIR" init + echo "node_modules/" > "$TEST_DIR/.gitignore" + bash "$SCRIPT" -d "$TEST_DIR" sync + grep -qxF "node_modules/" "$TEST_DIR/.gitignore" + grep -qxF "CLAUDE.md" "$TEST_DIR/.gitignore" +} + +@test "sync --targets only adds relevant entries to .gitignore" { + bash "$SCRIPT" -d "$TEST_DIR" init + bash "$SCRIPT" -d "$TEST_DIR" sync --targets claude + grep -qxF ".claude/" "$TEST_DIR/.gitignore" + grep -qxF "CLAUDE.md" "$TEST_DIR/.gitignore" + ! grep -qxF ".windsurf/" "$TEST_DIR/.gitignore" +} + +@test "sync --dry-run does not modify .gitignore" { + bash "$SCRIPT" -d "$TEST_DIR" init + bash "$SCRIPT" -d "$TEST_DIR" sync --dry-run + if [ -f "$TEST_DIR/.gitignore" ]; then + ! grep -qxF "CLAUDE.md" "$TEST_DIR/.gitignore" + fi +} + # -------------------------------------------------------------------------- # Inheritance # --------------------------------------------------------------------------