ProjectTracker/tools/scan.sh
StillHammer 13234b0e8b Initial commit: ProjectTracker infrastructure
Multi-repository state tracker and coordinator for managing 18+ Git projects.

Core Components:
- tools/scan.sh: Bash script to scan all repos, pull updates, detect conflicts
- tools/analyze.py: Python generator for human-readable summaries
- tools/config.json: Repository tracking configuration
- README.md: Complete documentation

Features:
- State tracking (uncommitted changes, new commits, conflicts)
- Activity monitoring (3-week lookback window)
- Automatic pulling with conflict detection
- Categorization (META, CONSTANT, WIP, CONCEPT, PAUSE, DONE)
- Summary generation with prioritization

analyze.py created in Session 7 - Cycle 2 (ClaudeSelf external exploration).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-19 01:14:28 +07:00

209 lines
6.3 KiB
Bash

#!/bin/bash
# ProjectTracker - Multi-repo scan script
# Scans all tracked repos, pulls updates, generates state.json
set -euo pipefail
# Paths
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_TRACKER="$(cd "$SCRIPT_DIR/.." && pwd)"
CONFIG_FILE="$SCRIPT_DIR/config.json"
# Check jq availability
if ! command -v jq &> /dev/null; then
echo "ERROR: jq is required but not installed."
echo "Install: https://stedolan.github.io/jq/download/"
exit 1
fi
# Load config
REPOS_ROOT=$(jq -r '.repos_root' "$CONFIG_FILE")
LOOKBACK_DAYS=$(jq -r '.scan_options.commits_lookback_days' "$CONFIG_FILE")
AUTO_PULL=$(jq -r '.scan_options.auto_pull' "$CONFIG_FILE")
# Output files
STATE_FILE="$PROJECT_TRACKER/.planning/state.json"
LOG_FILE="$PROJECT_TRACKER/.planning/scan.log"
CONFLICTS_FILE="$PROJECT_TRACKER/.planning/conflicts.txt"
# Ensure .planning exists
mkdir -p "$PROJECT_TRACKER/.planning"
# Init log
echo "===== Scan started: $(date -Iseconds) =====" > "$LOG_FILE"
> "$CONFLICTS_FILE"
echo "🔍 ProjectTracker - Multi-repo scan"
echo "Repos root: $REPOS_ROOT"
echo ""
# Read tracked repos into array
mapfile -t TRACKED_REPOS < <(jq -r '.tracked_repos[]' "$CONFIG_FILE")
# Start building state.json
cat > "$STATE_FILE" << 'EOF_HEADER'
{
"last_scan": "TIMESTAMP_PLACEHOLDER",
"repos": {
EOF_HEADER
FIRST_REPO=true
CONFLICT_COUNT=0
SCANNED_COUNT=0
ATTENTION_COUNT=0
# Scan each repo
for repo in "${TRACKED_REPOS[@]}"; do
REPO_PATH="$REPOS_ROOT/$repo"
# Check if repo exists
if [ ! -d "$REPO_PATH/.git" ]; then
echo "⚠️ $repo - NOT A GIT REPO (skipping)" | tee -a "$LOG_FILE"
continue
fi
echo -n "📂 $repo... " | tee -a "$LOG_FILE"
cd "$REPO_PATH"
# Fetch origin
git fetch origin &>> "$LOG_FILE" || true
# Pull if enabled
HAS_CONFLICT=false
if [ "$AUTO_PULL" = "true" ]; then
PULL_OUTPUT=$(git pull 2>&1) || true
echo "$PULL_OUTPUT" >> "$LOG_FILE"
if echo "$PULL_OUTPUT" | grep -q "CONFLICT\|Automatic merge failed"; then
HAS_CONFLICT=true
CONFLICT_COUNT=$((CONFLICT_COUNT + 1))
echo "⚠️ CONFLICT: $repo" | tee -a "$CONFLICTS_FILE"
fi
fi
# Collect repo info
CURRENT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
LAST_COMMIT_MSG=$(git log -1 --format="%s" 2>/dev/null || echo "No commits")
LAST_COMMIT_DATE=$(git log -1 --format="%ar" 2>/dev/null || echo "unknown")
# Ahead/behind tracking branch
TRACKING_BRANCH=$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null || echo "")
if [ -n "$TRACKING_BRANCH" ]; then
AHEAD=$(git rev-list --count HEAD..$TRACKING_BRANCH 2>/dev/null || echo 0)
BEHIND=$(git rev-list --count $TRACKING_BRANCH..HEAD 2>/dev/null || echo 0)
else
AHEAD=0
BEHIND=0
fi
# Uncommitted changes
if git diff-index --quiet HEAD -- 2>/dev/null; then
UNCOMMITTED=false
UNCOMMITTED_COUNT=0
else
UNCOMMITTED=true
UNCOMMITTED_COUNT=$(git status --short 2>/dev/null | wc -l)
fi
# Recent commits
COMMITS_3W=$(git rev-list --count --since="$LOOKBACK_DAYS days ago" HEAD 2>/dev/null || echo 0)
# Load previous commit from existing state (if exists)
PREV_COMMIT=$(jq -r ".repos.\"$repo\".last_commit // \"\"" "$STATE_FILE" 2>/dev/null || echo "")
if [ -n "$PREV_COMMIT" ] && [ "$PREV_COMMIT" != "$CURRENT_COMMIT" ] && [ "$PREV_COMMIT" != "null" ]; then
NEW_COMMITS=$(git rev-list --count $PREV_COMMIT..$CURRENT_COMMIT 2>/dev/null || echo 0)
else
NEW_COMMITS=0
fi
# Determine needs_attention
NEEDS_ATTENTION=false
if [ "$NEW_COMMITS" -gt 0 ] || [ "$UNCOMMITTED" = "true" ] || [ "$BEHIND" -gt 0 ] || [ "$HAS_CONFLICT" = "true" ]; then
NEEDS_ATTENTION=true
ATTENTION_COUNT=$((ATTENTION_COUNT + 1))
fi
# Project file mapping
PROJECT_FILE=$(jq -r ".project_mapping.\"$repo\" // \"\"" "$CONFIG_FILE")
# Add comma if not first repo
if [ "$FIRST_REPO" = false ]; then
echo "," >> "$STATE_FILE"
fi
FIRST_REPO=false
# Write JSON entry (properly escaped)
LAST_COMMIT_MSG_ESCAPED=$(echo "$LAST_COMMIT_MSG" | sed 's/\\/\\\\/g; s/"/\\"/g')
cat >> "$STATE_FILE" << EOF
"$repo": {
"last_commit": "$CURRENT_COMMIT",
"branch": "$CURRENT_BRANCH",
"last_commit_msg": "$LAST_COMMIT_MSG_ESCAPED",
"last_commit_date": "$LAST_COMMIT_DATE",
"commits_3w": $COMMITS_3W,
"new_commits_since_last_scan": $NEW_COMMITS,
"ahead": $AHEAD,
"behind": $BEHIND,
"has_uncommitted_changes": $UNCOMMITTED,
"uncommitted_count": $UNCOMMITTED_COUNT,
"has_conflict": $HAS_CONFLICT,
"needs_attention": $NEEDS_ATTENTION,
"project_file": "$PROJECT_FILE"
}
EOF
SCANNED_COUNT=$((SCANNED_COUNT + 1))
# Status indicator
if [ "$HAS_CONFLICT" = "true" ]; then
echo "❌ CONFLICT"
elif [ "$NEEDS_ATTENTION" = "true" ]; then
echo "⚠️ needs attention"
else
echo "✅"
fi
done
# Close repos object and add stats
cat >> "$STATE_FILE" << EOF
},
"stats": {
"total_repos": $SCANNED_COUNT,
"active_3w": 0,
"needs_attention": $ATTENTION_COUNT,
"conflicts": $CONFLICT_COUNT
}
}
EOF
# Calculate active repos count
ACTIVE_COUNT=$(jq '[.repos[] | select(.commits_3w > 0)] | length' "$STATE_FILE")
jq ".stats.active_3w = $ACTIVE_COUNT" "$STATE_FILE" > "$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE"
# Update timestamp
TIMESTAMP=$(date -Iseconds)
jq ".last_scan = \"$TIMESTAMP\"" "$STATE_FILE" > "$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE"
# Summary
echo ""
echo "===== Scan completed: $(date -Iseconds) =====" | tee -a "$LOG_FILE"
echo "✅ Scanned: $SCANNED_COUNT repos" | tee -a "$LOG_FILE"
echo "🔥 Active (3w): $ACTIVE_COUNT repos" | tee -a "$LOG_FILE"
echo "⚠️ Needs attention: $ATTENTION_COUNT repos" | tee -a "$LOG_FILE"
echo "❌ Conflicts: $CONFLICT_COUNT repos" | tee -a "$LOG_FILE"
echo ""
echo "📊 State saved to: $STATE_FILE"
echo "📝 Log saved to: $LOG_FILE"
# Generate summary
if [ -f "$SCRIPT_DIR/analyze.py" ]; then
echo ""
echo "Generating summary..."
python3 "$SCRIPT_DIR/analyze.py"
fi