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>
This commit is contained in:
commit
13234b0e8b
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal file
@ -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
|
||||||
196
README.md
Normal file
196
README.md
Normal file
@ -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/<CATEGORY>/` 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
|
||||||
30
projects/WIP/WrittingFantasy.md
Normal file
30
projects/WIP/WrittingFantasy.md
Normal file
@ -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)
|
||||||
207
tools/analyze.py
Normal file
207
tools/analyze.py
Normal file
@ -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)
|
||||||
69
tools/config.json
Normal file
69
tools/config.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
208
tools/scan.sh
Normal file
208
tools/scan.sh
Normal file
@ -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
|
||||||
Loading…
Reference in New Issue
Block a user