From 13234b0e8b53135870b233d0530dc0de4db1fe5e Mon Sep 17 00:00:00 2001 From: StillHammer Date: Mon, 19 Jan 2026 01:14:28 +0700 Subject: [PATCH] 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 --- .gitignore | 16 +++ README.md | 196 ++++++++++++++++++++++++++++++ projects/WIP/WrittingFantasy.md | 30 +++++ tools/analyze.py | 207 +++++++++++++++++++++++++++++++ tools/config.json | 69 +++++++++++ tools/scan.sh | 208 ++++++++++++++++++++++++++++++++ 6 files changed, 726 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 projects/WIP/WrittingFantasy.md create mode 100644 tools/analyze.py create mode 100644 tools/config.json create mode 100644 tools/scan.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..331fc9a --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# Environment variables (NEVER commit tokens/secrets) +.env + +# Planning outputs (generated files) +.planning/state.json +.planning/scan.log +.planning/conflicts.txt +.planning/summary.md + +# System files +.DS_Store +Thumbs.db + +# Temporary files +tmpclaude-* +*.tmp diff --git a/README.md b/README.md new file mode 100644 index 0000000..bc3d2fc --- /dev/null +++ b/README.md @@ -0,0 +1,196 @@ +# ProjectTracker + +**Multi-repository state tracker and coordinator for managing multiple Git projects.** + +ProjectTracker scans all tracked repositories, detects changes, pulls updates, identifies conflicts, and generates human-readable summaries of the entire ecosystem state. + +## Purpose + +Managing 15+ active Git repositories manually is tedious and error-prone. ProjectTracker automates: + +- **State Tracking**: Which repos have new commits, uncommitted changes, conflicts +- **Activity Monitoring**: Which projects are active vs dormant +- **Conflict Detection**: Automatic merge conflict identification +- **Categorization**: Organize projects by status (WIP, CONSTANT, PAUSE, etc.) +- **Reporting**: Generate readable summaries for quick ecosystem overview + +## Structure + +``` +ProjectTracker/ +├── tools/ +│ ├── config.json # Repository configuration and tracking settings +│ ├── scan.sh # Main scanner script (bash) +│ └── analyze.py # Summary generator (Python) +├── projects/ +│ ├── META/ # Meta-projects (planning, infrastructure) +│ ├── CONSTANT/ # Ongoing/maintenance projects +│ ├── WIP/ # Active development projects +│ ├── CONCEPT/ # Early-stage ideas +│ ├── PAUSE/ # Paused projects +│ └── DONE/ # Completed projects +├── .planning/ # Generated state files (auto-created) +│ ├── state.json # Machine-readable repo state +│ ├── summary.md # Human-readable summary +│ ├── scan.log # Detailed scan logs +│ └── conflicts.txt # List of repos with conflicts +├── .env # Gitea credentials and configuration +└── README.md # This file +``` + +## Configuration + +### 1. Environment Variables (`.env`) + +```bash +GITEA_URL=https://git.etheryale.com +GITEA_TOKEN=your_token_here +``` + +**Note**: Use `wget` instead of `curl` for API calls due to proxy compatibility. + +### 2. Repository Tracking (`tools/config.json`) + +Define which repos to track, exclude, and categorize: + +```json +{ + "repos_root": "C:/Users/alexi/Documents/projects", + "tracked_repos": ["civjdr", "ChineseClass", "seogeneratorserver", ...], + "excluded_repos": ["couple-repo", "vba-mcp-demo", ...], + "project_mapping": { + "civjdr": "projects/CONSTANT/civjdr.md", + "ChineseClass": "projects/CONSTANT/ChineseClass.md", + ... + } +} +``` + +**Scan Options**: +- `auto_pull`: Automatically pull updates (default: true) +- `commits_lookback_days`: Activity window for "recent" commits (default: 21) +- `detect_uncommitted_changes`: Check for unstaged/uncommitted files (default: true) + +## Usage + +### Full Scan and Summary + +```bash +cd ProjectTracker +bash tools/scan.sh +``` + +This will: +1. Fetch updates from all tracked repos +2. Pull changes (if `auto_pull: true`) +3. Detect conflicts, uncommitted changes, ahead/behind status +4. Generate `.planning/state.json` with full repo state +5. Run `analyze.py` to generate `.planning/summary.md` + +### Generate Summary Only + +If you already have a `state.json` and want to regenerate the summary: + +```bash +python tools/analyze.py +``` + +### View Summary + +```bash +cat .planning/summary.md +``` + +## Output Files + +### state.json (Machine-Readable) + +JSON snapshot of all repos with detailed metrics: + +```json +{ + "last_scan": "2026-01-18T14:30:00+00:00", + "repos": { + "civjdr": { + "last_commit": "abc1234", + "branch": "main", + "commits_3w": 8, + "new_commits_since_last_scan": 2, + "has_uncommitted_changes": false, + "has_conflict": false, + "needs_attention": true, + ... + } + }, + "stats": { + "total_repos": 18, + "active_3w": 12, + "needs_attention": 5, + "conflicts": 1 + } +} +``` + +### summary.md (Human-Readable) + +Markdown report with: +- **Global Stats**: Total repos, active count, attention needed, conflicts +- **Needs Attention**: Prioritized list (conflicts → uncommitted → new commits → behind) +- **By Category**: Repos organized by status (META, CONSTANT, WIP, etc.) +- **Recent Activity**: Commit counts for active repos (last 3 weeks) + +Example: +```markdown +## ⚠️ Needs Attention + +- **seogeneratorserver** (develop) - ❌ CONFLICT | 📝 7 uncommitted | 🆕 5 new + Last: Update SEO templates (3 days ago) +- **ChineseClass** (main) - 📝 3 uncommitted | 🆕 2 new + Last: Add vocabulary list (2 days ago) +``` + +## Project Categorization + +Projects are organized by development status: + +- **META**: Infrastructure, planning, coordination projects +- **CONSTANT**: Ongoing/maintenance projects (no end date) +- **WIP**: Active development (work in progress) +- **CONCEPT**: Early-stage ideas, prototypes +- **PAUSE**: Temporarily paused projects +- **DONE**: Completed projects (archived) + +Each project has a corresponding `.md` file in `projects//` with: +- Description +- Current status +- Repository URL +- Next steps +- Notes + +## Workflow + +1. **Daily/Weekly**: Run `scan.sh` to check ecosystem state +2. **Review**: Check `summary.md` for repos needing attention +3. **Act**: Address conflicts, commit changes, pull updates +4. **Repeat**: Keep repos synchronized and tracked + +## Requirements + +- **Bash**: For `scan.sh` (Git Bash on Windows, native on Linux/Mac) +- **Python 3.7+**: For `analyze.py` +- **jq**: JSON processor for bash script parsing +- **Git**: Obviously :) + +## Notes + +- Scanner automatically fetches before pulling +- Conflicts are never auto-resolved (manual intervention required) +- `state.json` tracks previous scan state to detect "new" commits +- All timestamps use ISO 8601 format +- UTF-8 encoding support for emoji indicators on all platforms + +--- + +**Created**: 2026-01-18 (Cycle 2 - External Exploration) +**Purpose**: Coordinate multi-project ecosystem efficiently +**Maintained**: Active diff --git a/projects/WIP/WrittingFantasy.md b/projects/WIP/WrittingFantasy.md new file mode 100644 index 0000000..b70d779 --- /dev/null +++ b/projects/WIP/WrittingFantasy.md @@ -0,0 +1,30 @@ +# WrittingFantasy + +**Statut:** WIP (Work In Progress) +**Type:** Projet d'écriture fantasy +**Repository:** git@git.etheryale.com:StillHammer/WrittingFantasy.git + +## Description + +Projet d'écriture fantasy avec worldbuilding complet, personnages, et arcs narratifs. + +## Structure + +- `worldbuilding/` - Univers, géographie, magie, cultures +- `characters/` - Fiches personnages +- `plots/` - Intrigues et arcs narratifs +- `chapters/` - Chapitres et brouillons +- `notes/` - Notes et idées + +## Prochaines Étapes + +1. Créer le repository sur Gitea (git.etheryale.com) +2. Pusher le commit initial +3. Développer le worldbuilding de base +4. Créer les fiches des personnages principaux +5. Écrire les premiers chapitres + +## Notes + +- Repository créé localement le 2025-12-17 +- En attente de création sur Gitea (serveur API injoignable temporairement) diff --git a/tools/analyze.py b/tools/analyze.py new file mode 100644 index 0000000..4ecbe55 --- /dev/null +++ b/tools/analyze.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 +""" +ProjectTracker - Summary Generator +Reads state.json and generates human-readable summary.md +""" + +import json +import sys +import os +from pathlib import Path +from typing import Dict, List, Tuple + +# Force UTF-8 encoding for Windows console (emoji support) +os.environ['PYTHONIOENCODING'] = 'utf-8' +if hasattr(sys.stdout, 'reconfigure'): + sys.stdout.reconfigure(encoding='utf-8') + +# Constants +SCRIPT_DIR = Path(__file__).parent +PROJECT_TRACKER = SCRIPT_DIR.parent +STATE_FILE = PROJECT_TRACKER / ".planning" / "state.json" +CONFIG_FILE = SCRIPT_DIR / "config.json" +SUMMARY_FILE = PROJECT_TRACKER / ".planning" / "summary.md" + +# Categories order for display +CATEGORIES_ORDER = ["META", "CONSTANT", "WIP", "CONCEPT", "PAUSE", "DONE"] + + +def load_json(path: Path) -> dict: + """Load JSON file""" + with open(path, 'r', encoding='utf-8') as f: + return json.load(f) + + +def get_category(project_file: str) -> str: + """Extract category from project file path + + Example: projects/WIP/repo.md -> WIP + """ + if not project_file: + return "UNCATEGORIZED" + + parts = project_file.split('/') + if len(parts) >= 2 and parts[0] == "projects": + return parts[1] + + return "UNCATEGORIZED" + + +def format_repo_status(repo_name: str, data: Dict) -> str: + """Format single repo status line with indicators""" + indicators = [] + + # Priority order: conflict > uncommitted > new commits > behind + if data.get("has_conflict"): + indicators.append("❌ CONFLICT") + if data.get("has_uncommitted_changes"): + count = data.get("uncommitted_count", 0) + indicators.append(f"📝 {count} uncommitted") + if data.get("new_commits_since_last_scan", 0) > 0: + count = data["new_commits_since_last_scan"] + indicators.append(f"🆕 {count} new") + if data.get("behind", 0) > 0: + count = data["behind"] + indicators.append(f"⬆️ {count} behind") + + status = " | ".join(indicators) if indicators else "✅" + + # Format output + branch = data.get("branch", "unknown") + msg = data.get("last_commit_msg", "No commits") + date = data.get("last_commit_date", "unknown") + + return f"- **{repo_name}** ({branch}) - {status}\n Last: {msg} ({date})" + + +def categorize_repos(repos: Dict, config: Dict) -> Dict[str, List[Tuple[str, Dict]]]: + """Organize repos by category""" + categorized = {cat: [] for cat in CATEGORIES_ORDER} + categorized["UNCATEGORIZED"] = [] + + for repo_name, data in repos.items(): + category = get_category(data.get("project_file", "")) + if category in categorized: + categorized[category].append((repo_name, data)) + else: + categorized["UNCATEGORIZED"].append((repo_name, data)) + + return categorized + + +def generate_summary(): + """Main function: generate summary.md from state.json""" + + # Load data + state = load_json(STATE_FILE) + config = load_json(CONFIG_FILE) + + repos = state["repos"] + stats = state["stats"] + last_scan = state["last_scan"] + + # Categorize + categorized = categorize_repos(repos, config) + + # Start building markdown + md = [] + md.append("# ProjectTracker - Summary\n") + md.append(f"**Last Scan:** {last_scan}\n") + md.append("---\n") + + # Global Stats + md.append("## 📊 Global Stats\n") + md.append(f"- **Total Repos:** {stats['total_repos']}") + md.append(f"- **Active (3w):** {stats['active_3w']}") + md.append(f"- **Needs Attention:** {stats['needs_attention']}") + md.append(f"- **Conflicts:** {stats['conflicts']}\n") + + # Needs Attention Section + attention_repos = [ + (name, data) for name, data in repos.items() + if data.get("needs_attention") + ] + + if attention_repos: + md.append("## ⚠️ Needs Attention\n") + + # Sort by priority: conflicts > uncommitted > new commits > behind + attention_repos.sort(key=lambda x: ( + -int(x[1].get("has_conflict", False)), + -int(x[1].get("has_uncommitted_changes", False)), + -x[1].get("new_commits_since_last_scan", 0), + -x[1].get("behind", 0) + )) + + for repo_name, data in attention_repos: + md.append(format_repo_status(repo_name, data)) + md.append("") + + # By Category Section + md.append("## 📂 By Category\n") + + for category in CATEGORIES_ORDER: + repos_in_cat = categorized[category] + if repos_in_cat: + md.append(f"### {category} ({len(repos_in_cat)})\n") + for repo_name, data in sorted(repos_in_cat): + md.append(format_repo_status(repo_name, data)) + md.append("") + + # Uncategorized (if any) + if categorized["UNCATEGORIZED"]: + count = len(categorized["UNCATEGORIZED"]) + md.append(f"### UNCATEGORIZED ({count})\n") + for repo_name, data in sorted(categorized["UNCATEGORIZED"]): + md.append(format_repo_status(repo_name, data)) + md.append("") + + # Recent Activity Section + active_repos = [ + (name, data) for name, data in repos.items() + if data.get("commits_3w", 0) > 0 + ] + + if active_repos: + md.append("## 🔥 Recent Activity (3 weeks)\n") + + # Sort by commit count (descending) + active_repos.sort(key=lambda x: -x[1]["commits_3w"]) + + for repo_name, data in active_repos: + commits = data["commits_3w"] + plural = 's' if commits > 1 else '' + md.append(f"- **{repo_name}**: {commits} commit{plural}") + md.append("") + + # Footer + md.append("---\n") + md.append("*Generated by ProjectTracker/tools/analyze.py*") + + # Write summary + summary_content = "\n".join(md) + SUMMARY_FILE.parent.mkdir(parents=True, exist_ok=True) + + with open(SUMMARY_FILE, 'w', encoding='utf-8') as f: + f.write(summary_content) + + # Console output + print(f"✅ Summary generated: {SUMMARY_FILE}") + print(f"📊 {stats['total_repos']} repos scanned, {stats['needs_attention']} need attention") + + +if __name__ == "__main__": + try: + generate_summary() + except FileNotFoundError as e: + print(f"❌ Error: File not found - {e}") + print("💡 Run scan.sh first to generate state.json") + exit(1) + except json.JSONDecodeError as e: + print(f"❌ Error: Invalid JSON - {e}") + exit(1) + except Exception as e: + print(f"❌ Unexpected error: {e}") + import traceback + traceback.print_exc() + exit(1) diff --git a/tools/config.json b/tools/config.json new file mode 100644 index 0000000..10a3eec --- /dev/null +++ b/tools/config.json @@ -0,0 +1,69 @@ +{ + "version": "1.0.0", + "repos_root": "C:/Users/alexi/Documents/projects", + + "tracked_repos": [ + "aissia", + "groveengine", + "AlwaysOnRecorder", + "civjdr", + "ChineseClass", + "Class_generator", + "confluent", + "maicivy", + "mobilecommand", + "secondvoice", + "seogeneratorserver", + "vba-mcp-monorepo", + "vba-mcp-server", + "videotoMP3Transcriptor", + "warfactoryracine", + "wechat-homework-bot", + "freelance_planning", + "WrittingFantasy" + ], + + "excluded_repos": [ + "couple-repo", + "knowledgemapperrepo", + "vba-mcp-demo", + "ProjectTracker" + ], + + "project_mapping": { + "aissia": "projects/WIP/aissia.md", + "groveengine": "projects/WIP/groveengine.md", + "AlwaysOnRecorder": "projects/WIP/AlwaysOnRecorder.md", + "civjdr": "projects/CONSTANT/civjdr.md", + "ChineseClass": "projects/CONSTANT/ChineseClass.md", + "Class_generator": "projects/CONSTANT/Class_generator.md", + "confluent": "projects/WIP/confluent.md", + "maicivy": "projects/WIP/maicivy.md", + "mobilecommand": "projects/CONCEPT/mobilecommand.md", + "secondvoice": "projects/PAUSE/secondvoice.md", + "seogeneratorserver": "projects/WIP/seogeneratorserver.md", + "vba-mcp-monorepo": "projects/WIP/vba-mcp-monorepo.md", + "vba-mcp-server": "projects/WIP/vba-mcp-server.md", + "videotoMP3Transcriptor": "projects/DONE/videotoMP3Transcriptor.md", + "warfactoryracine": "projects/PAUSE/warfactoryracine.md", + "wechat-homework-bot": "projects/WIP/wechat-homework-bot.md", + "freelance_planning": "projects/META/freelance_planning.md", + "WrittingFantasy": "projects/WIP/WrittingFantasy.md" + }, + + "scan_options": { + "auto_pull": true, + "fetch_before_pull": true, + "check_ahead_behind": true, + "commits_lookback_days": 21, + "detect_uncommitted_changes": true, + "timeout_per_repo_seconds": 30 + }, + + "output_options": { + "state_file": ".planning/state.json", + "log_file": ".planning/scan.log", + "conflicts_file": ".planning/conflicts.txt", + "summary_file": ".planning/summary.md" + } +} diff --git a/tools/scan.sh b/tools/scan.sh new file mode 100644 index 0000000..6e5661c --- /dev/null +++ b/tools/scan.sh @@ -0,0 +1,208 @@ +#!/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