commitpulse 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,126 @@
1
+ Metadata-Version: 2.1
2
+ Name: commitpulse
3
+ Version: 0.1.0
4
+ Summary: Premium Git repository analytics and dashboard generator
5
+ Author: Antigravity AI
6
+ Requires-Python: >=3.7
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: requests>=2.31.0
9
+
10
+ # Commit Pulse 🚀
11
+
12
+ Commit Pulse is a powerful, locally-first CLI tool that generates high-fidelity engineering dashboards for your Git repositories. It analyzes your development ecosystem, visualizes commit intensity heatmaps, and identifies top contributors using real GitHub profiles.
13
+
14
+ ## ✨ Key Features
15
+ - **Zero-PAT Analysis**: Analyzes local `.git` metadata directly. No Personal Access Tokens or cloud APIs required for core stats.
16
+ - **Accurate Project Timelines**: Dynamic contribution grids that span from the repository's inception to the **last official commit**.
17
+ - **GitHub Profile Integration**: Automatically fetches actual contributor avatars via the public GitHub Search API.
18
+ - **Master Scan**: A recursive drive scanner that aggregates stats from every project on your machine into a single interactive dashboard.
19
+ - **Premium Aesthetics**: Glassmorphic UI with dark mode, smooth animations, and official GitHub iconography.
20
+
21
+ ## 📦 Installation
22
+
23
+ ### 1. Local (Developer Mode)
24
+ If you have the source code locally:
25
+ ```bash
26
+ pip install -e .
27
+ ```
28
+
29
+ ### 2. Direct from GitHub
30
+ Anyone can install your tool directly if the repo is public:
31
+ ```bash
32
+ pip install git+https://github.com/YOUR_USERNAME/commitpulse.git
33
+ ```
34
+
35
+ ### 3. Global Command
36
+ Once installed, the `commitpulse` command is available everywhere in your terminal.
37
+
38
+ ## 🚀 Usage
39
+
40
+ ### Analyze the current repository
41
+ ```bash
42
+ commitpulse
43
+ ```
44
+
45
+ ### Analyze all repositories on your computer (Master Scan)
46
+ ```bash
47
+ commitpulse --scan
48
+ ```
49
+
50
+ ### Specify a target directory
51
+ ```bash
52
+ commitpulse /path/to/projects
53
+ ```
54
+
55
+ ## 🛠️ Options
56
+ - `--scan`: Crawls subdirectories to find and aggregate all Git repositories.
57
+ - `--no-open`: Generates the dashboard without automatically opening it in the browser.
58
+
59
+ ## 🚀 Deployment & Sharing
60
+
61
+ Commit Pulse offers two ways to view and share your results:
62
+
63
+ ### 1. Local-First (Default)
64
+ Run the command to generate a self-contained, interactive HTML dashboard on your machine. Perfect for private analysis.
65
+ ```bash
66
+ commitpulse --scan
67
+ ```
68
+ *Output: `stats_dashboard.html`*
69
+
70
+ ### 2. Commit Pulse Cloud (Global Sharing)
71
+ Ready to show the world? Use the `--publish` flag to host your dashboard on the cloud and get a unique, shareable URL.
72
+ ```bash
73
+ commitpulse --publish
74
+ ```
75
+ *Output: `https://commitpulse.app/v/your-unique-id`*
76
+
77
+ ---
78
+
79
+ ## 🛠️ Setting up your own Cloud Instance (Optional)
80
+
81
+ If you are hosting your own version of **Commit Pulse Cloud**:
82
+
83
+ 1. **Neon DB**:
84
+ - Go to [Neon.tech](https://neon.tech) and create a free project.
85
+ - Copy your **Connection String** (e.g., `postgresql://user:pass@ep-cool-beach-123.us-east-2.aws.neon.tech/neondb?sslmode=require`).
86
+ 2. **Environment Setup**:
87
+ - In the `commitpulse-cloud` directory, rename `.env.example` to `.env`.
88
+ - Paste your connection string into `DATABASE_URL`.
89
+ 3. **Vercel Deployment**:
90
+ - Deploy the `commitpulse-cloud` folder to Vercel.
91
+ - Add the `DATABASE_URL` to your Vercel Environment Variables.
92
+
93
+ ## 🌍 Distribution & PyPI Hosting
94
+
95
+ To share **Commit Pulse** with the global developer community via PyPI:
96
+
97
+ ### 1. Prepare your Account
98
+ - Register at [pypi.org](https://pypi.org/).
99
+ - Generate an API Token in your account settings.
100
+
101
+ ### 2. Build the Distribution
102
+ Ensure you have the latest build tools:
103
+ ```bash
104
+ python -m pip install --upgrade build twine
105
+ ```
106
+ Then build the package:
107
+ ```bash
108
+ python -m build
109
+ ```
110
+ This creates a `dist/` folder with your `.whl` and `.tar.gz` files.
111
+
112
+ ### 3. Upload to PyPI
113
+ Use `twine` to securely upload your package:
114
+ ```bash
115
+ python -m twine upload dist/*
116
+ ```
117
+ - **Username**: `__token__`
118
+ - **Password**: `[Your API Token]`
119
+
120
+ Once uploaded, anyone can install it via:
121
+ ```bash
122
+ pip install commitpulse
123
+ ```
124
+
125
+ ---
126
+ *Built with ❤️ for the development ecosystem.*
@@ -0,0 +1,117 @@
1
+ # Commit Pulse 🚀
2
+
3
+ Commit Pulse is a powerful, locally-first CLI tool that generates high-fidelity engineering dashboards for your Git repositories. It analyzes your development ecosystem, visualizes commit intensity heatmaps, and identifies top contributors using real GitHub profiles.
4
+
5
+ ## ✨ Key Features
6
+ - **Zero-PAT Analysis**: Analyzes local `.git` metadata directly. No Personal Access Tokens or cloud APIs required for core stats.
7
+ - **Accurate Project Timelines**: Dynamic contribution grids that span from the repository's inception to the **last official commit**.
8
+ - **GitHub Profile Integration**: Automatically fetches actual contributor avatars via the public GitHub Search API.
9
+ - **Master Scan**: A recursive drive scanner that aggregates stats from every project on your machine into a single interactive dashboard.
10
+ - **Premium Aesthetics**: Glassmorphic UI with dark mode, smooth animations, and official GitHub iconography.
11
+
12
+ ## 📦 Installation
13
+
14
+ ### 1. Local (Developer Mode)
15
+ If you have the source code locally:
16
+ ```bash
17
+ pip install -e .
18
+ ```
19
+
20
+ ### 2. Direct from GitHub
21
+ Anyone can install your tool directly if the repo is public:
22
+ ```bash
23
+ pip install git+https://github.com/YOUR_USERNAME/commitpulse.git
24
+ ```
25
+
26
+ ### 3. Global Command
27
+ Once installed, the `commitpulse` command is available everywhere in your terminal.
28
+
29
+ ## 🚀 Usage
30
+
31
+ ### Analyze the current repository
32
+ ```bash
33
+ commitpulse
34
+ ```
35
+
36
+ ### Analyze all repositories on your computer (Master Scan)
37
+ ```bash
38
+ commitpulse --scan
39
+ ```
40
+
41
+ ### Specify a target directory
42
+ ```bash
43
+ commitpulse /path/to/projects
44
+ ```
45
+
46
+ ## 🛠️ Options
47
+ - `--scan`: Crawls subdirectories to find and aggregate all Git repositories.
48
+ - `--no-open`: Generates the dashboard without automatically opening it in the browser.
49
+
50
+ ## 🚀 Deployment & Sharing
51
+
52
+ Commit Pulse offers two ways to view and share your results:
53
+
54
+ ### 1. Local-First (Default)
55
+ Run the command to generate a self-contained, interactive HTML dashboard on your machine. Perfect for private analysis.
56
+ ```bash
57
+ commitpulse --scan
58
+ ```
59
+ *Output: `stats_dashboard.html`*
60
+
61
+ ### 2. Commit Pulse Cloud (Global Sharing)
62
+ Ready to show the world? Use the `--publish` flag to host your dashboard on the cloud and get a unique, shareable URL.
63
+ ```bash
64
+ commitpulse --publish
65
+ ```
66
+ *Output: `https://commitpulse.app/v/your-unique-id`*
67
+
68
+ ---
69
+
70
+ ## 🛠️ Setting up your own Cloud Instance (Optional)
71
+
72
+ If you are hosting your own version of **Commit Pulse Cloud**:
73
+
74
+ 1. **Neon DB**:
75
+ - Go to [Neon.tech](https://neon.tech) and create a free project.
76
+ - Copy your **Connection String** (e.g., `postgresql://user:pass@ep-cool-beach-123.us-east-2.aws.neon.tech/neondb?sslmode=require`).
77
+ 2. **Environment Setup**:
78
+ - In the `commitpulse-cloud` directory, rename `.env.example` to `.env`.
79
+ - Paste your connection string into `DATABASE_URL`.
80
+ 3. **Vercel Deployment**:
81
+ - Deploy the `commitpulse-cloud` folder to Vercel.
82
+ - Add the `DATABASE_URL` to your Vercel Environment Variables.
83
+
84
+ ## 🌍 Distribution & PyPI Hosting
85
+
86
+ To share **Commit Pulse** with the global developer community via PyPI:
87
+
88
+ ### 1. Prepare your Account
89
+ - Register at [pypi.org](https://pypi.org/).
90
+ - Generate an API Token in your account settings.
91
+
92
+ ### 2. Build the Distribution
93
+ Ensure you have the latest build tools:
94
+ ```bash
95
+ python -m pip install --upgrade build twine
96
+ ```
97
+ Then build the package:
98
+ ```bash
99
+ python -m build
100
+ ```
101
+ This creates a `dist/` folder with your `.whl` and `.tar.gz` files.
102
+
103
+ ### 3. Upload to PyPI
104
+ Use `twine` to securely upload your package:
105
+ ```bash
106
+ python -m twine upload dist/*
107
+ ```
108
+ - **Username**: `__token__`
109
+ - **Password**: `[Your API Token]`
110
+
111
+ Once uploaded, anyone can install it via:
112
+ ```bash
113
+ pip install commitpulse
114
+ ```
115
+
116
+ ---
117
+ *Built with ❤️ for the development ecosystem.*
@@ -0,0 +1 @@
1
+ # NovaStats Package
@@ -0,0 +1,162 @@
1
+ import subprocess
2
+ import os
3
+ import hashlib
4
+ import json
5
+ import urllib.request
6
+ import urllib.parse
7
+ from collections import Counter
8
+ from datetime import datetime
9
+
10
+ class GitAnalyzer:
11
+ def __init__(self, repo_path):
12
+ self.repo_path = os.path.abspath(repo_path)
13
+ self.repo_name = os.path.basename(self.repo_path)
14
+ self.github_avatar_cache = {}
15
+
16
+ def _run_git(self, args):
17
+ try:
18
+ result = subprocess.run(
19
+ ['git'] + args,
20
+ cwd=self.repo_path,
21
+ capture_output=True,
22
+ text=True,
23
+ check=True,
24
+ shell=True
25
+ )
26
+ return result.stdout.strip()
27
+ except (subprocess.CalledProcessError, FileNotFoundError):
28
+ return None
29
+
30
+ def is_git_repo(self):
31
+ return os.path.exists(os.path.join(self.repo_path, '.git'))
32
+
33
+ def _get_github_avatar(self, email):
34
+ if email in self.github_avatar_cache:
35
+ return self.github_avatar_cache[email]
36
+
37
+ # Default to Gravatar
38
+ email_hash = hashlib.md5(email.lower().encode('utf-8')).hexdigest()
39
+ avatar_url = f"https://www.gravatar.com/avatar/{email_hash}?d=identicon&s=150"
40
+
41
+ try:
42
+ # Try to search for user by email using GitHub's public API
43
+ search_url = f"https://api.github.com/search/users?q={urllib.parse.quote(email)}"
44
+ request = urllib.request.Request(search_url)
45
+ request.add_header('User-Agent', 'CommitPulse-CLI-v0.1')
46
+
47
+ with urllib.request.urlopen(request, timeout=5) as response:
48
+ data = json.loads(response.read().decode())
49
+ if data.get('total_count', 0) > 0:
50
+ avatar_url = data['items'][0]['avatar_url']
51
+ except Exception:
52
+ # Silently fall back to Gravatar on any error (rate limit, network, etc)
53
+ pass
54
+
55
+ self.github_avatar_cache[email] = avatar_url
56
+ return avatar_url
57
+
58
+ def get_stats(self):
59
+ if not self.is_git_repo():
60
+ return None
61
+
62
+ print(f"Analyzing {self.repo_name}... (High Precision)")
63
+
64
+ # Get all commit dates and times for heatmap and productivity
65
+ # Use --all to include all branches and --no-merges to match GitHub's default contribution view
66
+ # or remove --no-merges if we want every single commit. GitHub counts merges if they occurred on GitHub.
67
+ # We'll stick to --all for full visibility.
68
+ commit_dates_raw = self._run_git(['log', '--all', '--format=%aI'])
69
+
70
+ heatmap_data = Counter()
71
+ hourly_distribution = Counter()
72
+
73
+ if commit_dates_raw:
74
+ dates = commit_dates_raw.split('\n')
75
+ for d in dates:
76
+ if not d: continue
77
+ try:
78
+ dt = datetime.fromisoformat(d)
79
+ date_str = dt.date().isoformat()
80
+ hour = dt.hour
81
+ heatmap_data[date_str] += 1
82
+ hourly_distribution[hour] += 1
83
+ except ValueError:
84
+ continue
85
+
86
+ total_commits = sum(heatmap_data.values())
87
+
88
+ # Get first and last commit dates
89
+ first_date = "N/A"
90
+ last_date = "N/A"
91
+ if heatmap_data:
92
+ sorted_dates = sorted(heatmap_data.keys())
93
+ first_date = sorted_dates[0]
94
+ last_date = sorted_dates[-1]
95
+
96
+ # Get contributors with high precision
97
+ # Format: count <tab> name <email>
98
+ shortlog = self._run_git(['shortlog', '-sne', '--all'])
99
+ contributors = []
100
+ if shortlog:
101
+ for line in shortlog.split('\n'):
102
+ line = line.strip()
103
+ if not line: continue
104
+ parts = line.split('\t')
105
+ if len(parts) == 2:
106
+ count = int(parts[0].strip())
107
+ name_email = parts[1].strip()
108
+ name = name_email
109
+ email = ""
110
+ if '<' in name_email:
111
+ name = name_email.split('<')[0].strip()
112
+ email = name_email.split('<')[1].split('>')[0].strip()
113
+
114
+ avatar = self._get_github_avatar(email)
115
+
116
+ contributors.append({
117
+ "name": name,
118
+ "email": email,
119
+ "commits": count,
120
+ "avatar": avatar
121
+ })
122
+
123
+ # Calculate most productive hour
124
+ peak_hour = "N/A"
125
+ if hourly_distribution:
126
+ peak_hour = f"{hourly_distribution.most_common(1)[0][0]}:00"
127
+
128
+ return {
129
+ "name": self.repo_name,
130
+ "path": self.repo_path,
131
+ "total_commits": total_commits,
132
+ "first_commit": first_date,
133
+ "last_commit": last_date,
134
+ "peak_hour": peak_hour,
135
+ "heatmap": dict(heatmap_data),
136
+ "hourly_distribution": dict(hourly_distribution),
137
+ "contributors": sorted(contributors, key=lambda x: x['commits'], reverse=True)
138
+ }
139
+
140
+ @staticmethod
141
+ def scan_for_repos(root_dir, max_depth=3):
142
+ repos = []
143
+ root_dir = os.path.abspath(root_dir)
144
+
145
+ for dirpath, dirnames, filenames in os.walk(root_dir):
146
+ if '.git' in dirnames:
147
+ repos.append(dirpath)
148
+ dirnames.remove('.git')
149
+
150
+ depth = dirpath.replace(root_dir, '').count(os.sep)
151
+ if depth >= max_depth:
152
+ del dirnames[:]
153
+
154
+ return repos
155
+ @staticmethod
156
+ def get_git_config_user():
157
+ try:
158
+ import subprocess
159
+ name = subprocess.check_output(["git", "config", "user.name"]).decode().strip()
160
+ return name
161
+ except:
162
+ return None
@@ -0,0 +1,118 @@
1
+ import argparse
2
+ import os
3
+ import sys
4
+ import webbrowser
5
+ from .analyzer import GitAnalyzer
6
+ from .renderer import DashboardRenderer
7
+
8
+ def main():
9
+ parser = argparse.ArgumentParser(
10
+ description="Commit Pulse - Premium Git Repository Analytics Dashboard"
11
+ )
12
+
13
+ parser.add_argument(
14
+ 'path',
15
+ nargs='?',
16
+ default='.',
17
+ help="Path to the repository (default is current directory)"
18
+ )
19
+
20
+ parser.add_argument(
21
+ '--scan',
22
+ action='store_true',
23
+ help="Scan the current directory (and subdirectories) for all git repositories"
24
+ )
25
+
26
+ parser.add_argument(
27
+ '--no-open',
28
+ action='store_true',
29
+ help="Do not automatically open the dashboard in the browser"
30
+ )
31
+
32
+ parser.add_argument(
33
+ '--publish',
34
+ action='store_true',
35
+ help="Publish your project's pulse to the cloud for sharing"
36
+ )
37
+
38
+ args = parser.parse_args()
39
+
40
+ all_stats = []
41
+
42
+ if args.scan:
43
+ print(f"Scanning for git repositories in {os.path.abspath(args.path)}...")
44
+ repo_paths = GitAnalyzer.scan_for_repos(args.path)
45
+ print(f"Found {len(repo_paths)} repositories.")
46
+
47
+ for p in repo_paths:
48
+ analyzer = GitAnalyzer(p)
49
+ stats = analyzer.get_stats()
50
+ if stats:
51
+ all_stats.append(stats)
52
+ else:
53
+ analyzer = GitAnalyzer(args.path)
54
+ if not analyzer.is_git_repo():
55
+ print(f"Error: {os.path.abspath(args.path)} is not a git repository.")
56
+ sys.exit(1)
57
+
58
+ stats = analyzer.get_stats()
59
+ if stats:
60
+ all_stats.append(stats)
61
+
62
+ if not all_stats:
63
+ print("Note: No statistics could be gathered.")
64
+ sys.exit(0)
65
+
66
+ # Render dashboard
67
+ renderer = DashboardRenderer(all_stats)
68
+ output_path = renderer.render()
69
+
70
+ print(f"\nDashboard generated successfully!")
71
+ print(f"Location: {output_path}")
72
+
73
+ # Decide whether to open local HTML
74
+ # If publishing, we might want to prioritize the cloud link instead of popping up the local file
75
+ should_open_local = not args.no_open
76
+ if args.publish and not args.no_open:
77
+ # If they are publishing, maybe they only want the web link?
78
+ # Let's still open local unless it's a server environment, but clarify.
79
+ pass
80
+
81
+ if should_open_local:
82
+ webbrowser.open(f"file://{output_path}")
83
+
84
+ # Cloud Publishing
85
+ if args.publish:
86
+ import requests
87
+ # Get username from git config
88
+ username = GitAnalyzer.get_git_config_user() or os.getenv("USER") or "Anonymous"
89
+
90
+ print(f"\n🚀 Publishing your Pulse to the cloud as '{username}'...")
91
+
92
+ # In a real scenario, this would be your production URL
93
+ # For now, we point to the local or configured cloud instance
94
+ cloud_url = os.getenv("COMMITPULSE_CLOUD_URL", "http://localhost:3000")
95
+ api_endpoint = f"{cloud_url}/api/publish"
96
+
97
+ try:
98
+ # We publish the first repo in scanning mode or the target repo
99
+ # For simplicity, we publish the main one if multiple found
100
+ repo_to_publish = all_stats[0]
101
+
102
+ payload = {
103
+ "username": username,
104
+ "repoName": repo_to_publish["name"],
105
+ "stats": all_stats # Send all scanned repos
106
+ }
107
+
108
+ response = requests.post(api_endpoint, json=payload, timeout=10)
109
+ if response.status_code == 200:
110
+ result = response.json()
111
+ print(f"✨ Successfully published! Share your Pulse at: {result['url']}")
112
+ else:
113
+ print(f"❌ Failed to publish: {response.text}")
114
+ except Exception as e:
115
+ print(f"⚠️ Cloud sync failed: {str(e)}")
116
+
117
+ if __name__ == "__main__":
118
+ main()
@@ -0,0 +1,537 @@
1
+ import json
2
+ import os
3
+ from datetime import datetime, timedelta
4
+
5
+ class DashboardRenderer:
6
+ def __init__(self, stats_data):
7
+ self.stats_data = stats_data
8
+
9
+ def render(self, output_path="stats_dashboard.html"):
10
+ html_template = """
11
+ <!DOCTYPE html>
12
+ <html lang="en">
13
+ <head>
14
+ <meta charset="UTF-8">
15
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
16
+ <title>Commit Pulse | Repository Ecosystem</title>
17
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
18
+ <style>
19
+ :root {
20
+ --bg-dark: #0d1117;
21
+ --card-bg: rgba(22, 27, 34, 0.7);
22
+ --border: rgba(48, 54, 61, 0.8);
23
+ --text-main: #f0f6fc;
24
+ --text-dim: #8b949e;
25
+ --accent: #238636;
26
+ --accent-glow: rgba(35, 134, 54, 0.3);
27
+ --github-blue: #58a6ff;
28
+ --glass-blur: blur(12px);
29
+
30
+ /* Heatmap Colors */
31
+ --h-0: #161b22;
32
+ --h-1: #0e4429;
33
+ --h-2: #006d32;
34
+ --h-3: #26a641;
35
+ --h-4: #39d353;
36
+ }
37
+
38
+ * {
39
+ box-sizing: border-box;
40
+ transition: all 0.2s ease-in-out;
41
+ }
42
+
43
+ body {
44
+ font-family: 'Outfit', -apple-system, BlinkMacSystemFont, sans-serif;
45
+ background-color: var(--bg-dark);
46
+ background-image: radial-gradient(circle at 50% 10%, rgba(35, 134, 54, 0.05) 0%, transparent 50%);
47
+ color: var(--text-main);
48
+ margin: 0;
49
+ padding: 0;
50
+ overflow-x: hidden;
51
+ }
52
+
53
+ header {
54
+ padding: 30px 60px;
55
+ display: flex;
56
+ align-items: center;
57
+ justify-content: space-between;
58
+ position: sticky;
59
+ top: 0;
60
+ z-index: 100;
61
+ background: rgba(13, 17, 23, 0.8);
62
+ backdrop-filter: var(--glass-blur);
63
+ border-bottom: 1px solid var(--border);
64
+ }
65
+
66
+ .header-left {
67
+ display: flex;
68
+ align-items: center;
69
+ gap: 15px;
70
+ }
71
+
72
+ .logo-icon {
73
+ fill: var(--text-main);
74
+ filter: drop-shadow(0 0 8px rgba(255,255,255,0.2));
75
+ }
76
+
77
+ h1 {
78
+ font-size: 24px;
79
+ font-weight: 700;
80
+ margin: 0;
81
+ letter-spacing: -0.5px;
82
+ background: linear-gradient(90deg, #fff, var(--text-dim));
83
+ -webkit-background-clip: text;
84
+ -webkit-text-fill-color: transparent;
85
+ }
86
+
87
+ .container {
88
+ max-width: 1400px;
89
+ margin: 0 auto;
90
+ padding: 40px 60px;
91
+ }
92
+
93
+ .stats-grid {
94
+ display: grid;
95
+ grid-template-columns: 1fr;
96
+ gap: 40px;
97
+ }
98
+
99
+ .repo-card {
100
+ background: var(--card-bg);
101
+ border: 1px solid var(--border);
102
+ border-radius: 16px;
103
+ padding: 32px;
104
+ backdrop-filter: var(--glass-blur);
105
+ position: relative;
106
+ overflow: hidden;
107
+ }
108
+
109
+ .repo-card::before {
110
+ content: '';
111
+ position: absolute;
112
+ top: 0; left: 0; width: 4px; height: 100%;
113
+ background: var(--accent);
114
+ opacity: 0;
115
+ transition: 0.3s;
116
+ }
117
+
118
+ .repo-card:hover {
119
+ border-color: var(--github-blue);
120
+ transform: translateY(-4px);
121
+ box-shadow: 0 12px 24px rgba(0,0,0,0.3);
122
+ }
123
+
124
+ .repo-card:hover::before {
125
+ opacity: 1;
126
+ }
127
+
128
+ .repo-title-row {
129
+ display: flex;
130
+ justify-content: space-between;
131
+ align-items: flex-start;
132
+ margin-bottom: 24px;
133
+ }
134
+
135
+ .repo-name-group {
136
+ display: flex;
137
+ align-items: center;
138
+ gap: 12px;
139
+ }
140
+
141
+ .repo-name {
142
+ font-size: 28px;
143
+ font-weight: 700;
144
+ color: var(--text-main);
145
+ text-decoration: none;
146
+ }
147
+
148
+ .repo-meta-badges {
149
+ display: flex;
150
+ gap: 12px;
151
+ margin-bottom: 32px;
152
+ }
153
+
154
+ .badge {
155
+ background: rgba(48, 54, 61, 0.4);
156
+ border: 1px solid var(--border);
157
+ color: var(--text-dim);
158
+ padding: 4px 12px;
159
+ border-radius: 20px;
160
+ font-size: 13px;
161
+ font-weight: 500;
162
+ display: flex;
163
+ align-items: center;
164
+ gap: 6px;
165
+ }
166
+
167
+ .badge.highlight {
168
+ border-color: var(--accent);
169
+ color: var(--accent);
170
+ background: rgba(35, 134, 54, 0.1);
171
+ }
172
+
173
+ .dashboard-row {
174
+ display: grid;
175
+ grid-template-columns: 1fr;
176
+ gap: 32px;
177
+ }
178
+
179
+ /* HEATMAP STYLES */
180
+ .heatmap-container {
181
+ background: rgba(0,0,0,0.2);
182
+ padding: 32px;
183
+ border-radius: 12px;
184
+ border: 1px solid var(--border);
185
+ width: 100%;
186
+ overflow: hidden;
187
+ }
188
+
189
+ .heatmap-wrapper {
190
+ overflow-x: auto;
191
+ display: flex;
192
+ gap: 16px;
193
+ padding-bottom: 12px;
194
+ scrollbar-width: thin;
195
+ scrollbar-color: var(--border) transparent;
196
+ }
197
+
198
+ .heatmap-wrapper::-webkit-scrollbar {
199
+ height: 6px;
200
+ }
201
+
202
+ .heatmap-wrapper::-webkit-scrollbar-track {
203
+ background: transparent;
204
+ }
205
+
206
+ .heatmap-wrapper::-webkit-scrollbar-thumb {
207
+ background: var(--border);
208
+ border-radius: 10px;
209
+ }
210
+
211
+ .heatmap-wrapper::-webkit-scrollbar-thumb:hover {
212
+ background: var(--text-dim);
213
+ }
214
+
215
+ .section-label {
216
+ font-size: 14px;
217
+ font-weight: 600;
218
+ text-transform: uppercase;
219
+ color: var(--text-dim);
220
+ letter-spacing: 1px;
221
+ margin-bottom: 20px;
222
+ display: flex;
223
+ justify-content: space-between;
224
+ align-items: center;
225
+ }
226
+
227
+ .heatmap-month-group {
228
+ display: flex;
229
+ flex-direction: column;
230
+ gap: 8px;
231
+ flex-shrink: 0; /* Don't squish month groups */
232
+ }
233
+
234
+ .month-name {
235
+ font-size: 11px;
236
+ font-weight: 700;
237
+ color: var(--github-blue); /* Subtle blue for headers */
238
+ text-transform: uppercase;
239
+ letter-spacing: 0.5px;
240
+ }
241
+
242
+ .heatmap-grid {
243
+ display: flex;
244
+ flex-flow: column wrap;
245
+ height: 122px; /* (14px * 7) + (4px * 6) = 122px */
246
+ gap: 4px;
247
+ align-content: flex-start;
248
+ }
249
+
250
+ .heatmap-cell {
251
+ width: 14px;
252
+ height: 14px;
253
+ border-radius: 2px;
254
+ background-color: var(--h-0);
255
+ cursor: pointer;
256
+ flex-shrink: 0;
257
+ }
258
+
259
+ .heatmap-cell.empty {
260
+ background-color: transparent;
261
+ cursor: default;
262
+ pointer-events: none;
263
+ }
264
+
265
+ .heatmap-cell:hover {
266
+ outline: 2px solid var(--text-main);
267
+ z-index: 10;
268
+ }
269
+
270
+ .intensity-1 { background-color: var(--h-1); }
271
+ .intensity-2 { background-color: var(--h-2); }
272
+ .intensity-3 { background-color: var(--h-3); }
273
+ .intensity-4 { background-color: var(--h-4); }
274
+
275
+ .legend {
276
+ display: flex;
277
+ align-items: center;
278
+ gap: 6px;
279
+ font-size: 12px;
280
+ color: var(--text-dim);
281
+ }
282
+
283
+ .legend-box {
284
+ width: 10px;
285
+ height: 10px;
286
+ border-radius: 2px;
287
+ }
288
+
289
+ /* CONTRIBUTOR STYLES */
290
+ .contributor-panel {
291
+ background: rgba(0,0,0,0.2);
292
+ padding: 24px;
293
+ border-radius: 12px;
294
+ border: 1px solid var(--border);
295
+ margin-top: 24px;
296
+ }
297
+
298
+ .contributor-list {
299
+ display: flex;
300
+ flex-wrap: wrap;
301
+ gap: 24px;
302
+ }
303
+
304
+ .contributor-item {
305
+ display: flex;
306
+ align-items: center;
307
+ gap: 12px;
308
+ padding: 8px;
309
+ border-radius: 8px;
310
+ background: rgba(255,255,255,0.02);
311
+ min-width: 200px;
312
+ }
313
+
314
+ .contributor-info {
315
+ display: flex;
316
+ align-items: center;
317
+ gap: 12px;
318
+ }
319
+
320
+ .avatar {
321
+ width: 40px;
322
+ height: 40px;
323
+ border-radius: 50%;
324
+ border: 2px solid var(--border);
325
+ padding: 2px;
326
+ background: #0d1117;
327
+ object-fit: cover;
328
+ }
329
+
330
+ .contributor-name {
331
+ font-weight: 600;
332
+ font-size: 15px;
333
+ color: var(--text-main);
334
+ }
335
+
336
+ .commit-pill {
337
+ background: var(--accent);
338
+ color: #fff;
339
+ padding: 4px 12px;
340
+ border-radius: 6px;
341
+ font-weight: 600;
342
+ font-size: 12px;
343
+ }
344
+
345
+ footer {
346
+ text-align: center;
347
+ padding: 60px;
348
+ color: var(--text-dim);
349
+ font-size: 14px;
350
+ opacity: 0.6;
351
+ }
352
+
353
+ @keyframes fadeIn {
354
+ from { opacity: 0; transform: translateY(20px); }
355
+ to { opacity: 1; transform: translateY(0); }
356
+ }
357
+
358
+ .repo-card {
359
+ animation: fadeIn 0.8s ease-out forwards;
360
+ }
361
+ </style>
362
+ </head>
363
+ <body>
364
+ <header>
365
+ <div class="header-left">
366
+ <svg height="32" viewBox="0 0 16 16" width="32" class="logo-icon"><path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.27-.82 2.15 0 3.07 1.87 3.75 3.65 3.95-.29.25-.54.73-.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 1 0 8c0-4.42 3.58-8 8-8Z"></path></svg>
367
+ <h1>Commit Pulse</h1>
368
+ </div>
369
+ <div class="badge highlight">
370
+ <span>Repository Ecosystem</span>
371
+ </div>
372
+ </header>
373
+
374
+ <div class="stats-grid" id="stats-grid"></div>
375
+
376
+ <footer>
377
+ &copy; 2026 Commit Pulse &bull; High-Fidelity Ecosystem Analytics
378
+ </footer>
379
+
380
+ <script>
381
+ const data = __STATS_DATA__;
382
+
383
+ function getIntensity(count) {
384
+ if (count === 0) return '';
385
+ if (count < 3) return 'intensity-1';
386
+ if (count < 6) return 'intensity-2';
387
+ if (count < 10) return 'intensity-3';
388
+ return 'intensity-4';
389
+ }
390
+
391
+ function createHeatmap(repo) {
392
+ const wrapper = document.createElement('div');
393
+ wrapper.className = 'heatmap-wrapper';
394
+
395
+ const start = new Date(repo.first_commit);
396
+ const end = new Date(repo.last_commit);
397
+
398
+ // Start from the first day of the first commit month
399
+ const current = new Date(start.getFullYear(), start.getMonth(), 1);
400
+ // End at the last day of the last commit month
401
+ const lastDay = new Date(end.getFullYear(), end.getMonth() + 1, 0);
402
+
403
+ const months = {};
404
+
405
+ while (current <= lastDay) {
406
+ const monthKey = `${current.getFullYear()}-${String(current.getMonth() + 1).padStart(2, '0')}`;
407
+ if (!months[monthKey]) {
408
+ months[monthKey] = {
409
+ name: current.toLocaleString('default', { month: 'short' }),
410
+ year: current.getFullYear(),
411
+ days: []
412
+ };
413
+ }
414
+
415
+ const dateKey = current.toISOString().split('T')[0];
416
+ const count = repo.heatmap[dateKey] || 0;
417
+
418
+ months[monthKey].days.push({
419
+ date: dateKey,
420
+ readable: current.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }),
421
+ count: count
422
+ });
423
+
424
+ current.setDate(current.getDate() + 1);
425
+ }
426
+
427
+ Object.values(months).forEach(m => {
428
+ const monthGroup = document.createElement('div');
429
+ monthGroup.className = 'heatmap-month-group';
430
+
431
+ const monthLabel = document.createElement('div');
432
+ monthLabel.className = 'month-name';
433
+ monthLabel.innerText = `${m.name} ${m.year}`;
434
+ monthGroup.appendChild(monthLabel);
435
+
436
+ const grid = document.createElement('div');
437
+ grid.className = 'heatmap-grid';
438
+
439
+ m.days.forEach(day => {
440
+ const cell = document.createElement('div');
441
+ cell.className = `heatmap-cell ${getIntensity(day.count)}`;
442
+ cell.title = `${day.count} commits on ${day.readable}`;
443
+ grid.appendChild(cell);
444
+ });
445
+
446
+ // Pad the end of the month to complete the column (total cells must be multiple of 7)
447
+ const totalCells = m.days.length;
448
+ const remaining = (7 - (totalCells % 7)) % 7;
449
+ for (let i = 0; i < remaining; i++) {
450
+ const empty = document.createElement('div');
451
+ empty.className = 'heatmap-cell empty';
452
+ grid.appendChild(empty);
453
+ }
454
+
455
+ monthGroup.appendChild(grid);
456
+ wrapper.appendChild(monthGroup);
457
+ });
458
+
459
+ return wrapper;
460
+ }
461
+
462
+ function render() {
463
+ const grid = document.getElementById('stats-grid');
464
+ data.forEach((repo, index) => {
465
+ const card = document.createElement('div');
466
+ card.className = 'repo-card';
467
+ card.style.animationDelay = `${index * 0.15}s`;
468
+
469
+ card.innerHTML = `
470
+ <div class="repo-title-row">
471
+ <div class="repo-name-group">
472
+ <svg height="24" viewBox="0 0 16 16" width="24" fill="var(--text-dim)"><path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.27-.82 2.15 0 3.07 1.87 3.75 3.65 3.95-.29.25-.54.73-.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 1 0 8c0-4.42 3.58-8 8-8Z"></path></svg>
473
+ <span class="repo-name">${repo.name}</span>
474
+ </div>
475
+ <div class="commit-pill">${repo.total_commits} Total Commits</div>
476
+ </div>
477
+
478
+ <div class="repo-meta-badges">
479
+ <div class="badge">
480
+ <span style="opacity: 0.5">Timeline:</span>
481
+ ${repo.first_commit} to ${repo.last_commit}
482
+ </div>
483
+ <div class="badge highlight">
484
+ <span style="opacity: 0.5">Peak Hour:</span>
485
+ ${repo.peak_hour}
486
+ </div>
487
+ </div>
488
+
489
+ <div class="dashboard-row">
490
+ <div class="heatmap-container">
491
+ <div class="section-label">
492
+ <span>Commit Contributions (Full History)</span>
493
+ <div class="legend">
494
+ <span>Less</span>
495
+ <div class="legend-box" style="background: var(--h-0)"></div>
496
+ <div class="legend-box intensity-1"></div>
497
+ <div class="legend-box intensity-2"></div>
498
+ <div class="legend-box intensity-3"></div>
499
+ <div class="legend-box intensity-4"></div>
500
+ <span>More</span>
501
+ </div>
502
+ </div>
503
+ <div class="heatmap-wrapper" id="heatmap-${index}"></div>
504
+ </div>
505
+ </div>
506
+
507
+ <div class="contributor-panel">
508
+ <span class="section-label">Top Engineering Impact</span>
509
+ <div class="contributor-list">
510
+ ${repo.contributors.map(c => `
511
+ <div class="contributor-item">
512
+ <div class="contributor-info">
513
+ <img src="${c.avatar}" class="avatar" onerror="this.src='https://www.gravatar.com/avatar/0?d=mp'">
514
+ <span class="contributor-name">${c.name}</span>
515
+ </div>
516
+ <div class="commit-pill" style="background: rgba(255,255,255,0.05); color: var(--text-dim); border: 1px solid var(--border)">
517
+ ${c.commits}
518
+ </div>
519
+ </div>
520
+ `).join('')}
521
+ </div>
522
+ </div>
523
+ `;
524
+ grid.appendChild(card);
525
+ document.getElementById(`heatmap-${index}`).appendChild(createHeatmap(repo));
526
+ });
527
+ }
528
+ render();
529
+ </script>
530
+ </body>
531
+ </html>
532
+ """.replace("__STATS_DATA__", json.dumps(self.stats_data))
533
+
534
+ with open(output_path, "w", encoding="utf-8") as f:
535
+ f.write(html_template)
536
+
537
+ return os.path.abspath(output_path)
@@ -0,0 +1,126 @@
1
+ Metadata-Version: 2.1
2
+ Name: commitpulse
3
+ Version: 0.1.0
4
+ Summary: Premium Git repository analytics and dashboard generator
5
+ Author: Antigravity AI
6
+ Requires-Python: >=3.7
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: requests>=2.31.0
9
+
10
+ # Commit Pulse 🚀
11
+
12
+ Commit Pulse is a powerful, locally-first CLI tool that generates high-fidelity engineering dashboards for your Git repositories. It analyzes your development ecosystem, visualizes commit intensity heatmaps, and identifies top contributors using real GitHub profiles.
13
+
14
+ ## ✨ Key Features
15
+ - **Zero-PAT Analysis**: Analyzes local `.git` metadata directly. No Personal Access Tokens or cloud APIs required for core stats.
16
+ - **Accurate Project Timelines**: Dynamic contribution grids that span from the repository's inception to the **last official commit**.
17
+ - **GitHub Profile Integration**: Automatically fetches actual contributor avatars via the public GitHub Search API.
18
+ - **Master Scan**: A recursive drive scanner that aggregates stats from every project on your machine into a single interactive dashboard.
19
+ - **Premium Aesthetics**: Glassmorphic UI with dark mode, smooth animations, and official GitHub iconography.
20
+
21
+ ## 📦 Installation
22
+
23
+ ### 1. Local (Developer Mode)
24
+ If you have the source code locally:
25
+ ```bash
26
+ pip install -e .
27
+ ```
28
+
29
+ ### 2. Direct from GitHub
30
+ Anyone can install your tool directly if the repo is public:
31
+ ```bash
32
+ pip install git+https://github.com/YOUR_USERNAME/commitpulse.git
33
+ ```
34
+
35
+ ### 3. Global Command
36
+ Once installed, the `commitpulse` command is available everywhere in your terminal.
37
+
38
+ ## 🚀 Usage
39
+
40
+ ### Analyze the current repository
41
+ ```bash
42
+ commitpulse
43
+ ```
44
+
45
+ ### Analyze all repositories on your computer (Master Scan)
46
+ ```bash
47
+ commitpulse --scan
48
+ ```
49
+
50
+ ### Specify a target directory
51
+ ```bash
52
+ commitpulse /path/to/projects
53
+ ```
54
+
55
+ ## 🛠️ Options
56
+ - `--scan`: Crawls subdirectories to find and aggregate all Git repositories.
57
+ - `--no-open`: Generates the dashboard without automatically opening it in the browser.
58
+
59
+ ## 🚀 Deployment & Sharing
60
+
61
+ Commit Pulse offers two ways to view and share your results:
62
+
63
+ ### 1. Local-First (Default)
64
+ Run the command to generate a self-contained, interactive HTML dashboard on your machine. Perfect for private analysis.
65
+ ```bash
66
+ commitpulse --scan
67
+ ```
68
+ *Output: `stats_dashboard.html`*
69
+
70
+ ### 2. Commit Pulse Cloud (Global Sharing)
71
+ Ready to show the world? Use the `--publish` flag to host your dashboard on the cloud and get a unique, shareable URL.
72
+ ```bash
73
+ commitpulse --publish
74
+ ```
75
+ *Output: `https://commitpulse.app/v/your-unique-id`*
76
+
77
+ ---
78
+
79
+ ## 🛠️ Setting up your own Cloud Instance (Optional)
80
+
81
+ If you are hosting your own version of **Commit Pulse Cloud**:
82
+
83
+ 1. **Neon DB**:
84
+ - Go to [Neon.tech](https://neon.tech) and create a free project.
85
+ - Copy your **Connection String** (e.g., `postgresql://user:pass@ep-cool-beach-123.us-east-2.aws.neon.tech/neondb?sslmode=require`).
86
+ 2. **Environment Setup**:
87
+ - In the `commitpulse-cloud` directory, rename `.env.example` to `.env`.
88
+ - Paste your connection string into `DATABASE_URL`.
89
+ 3. **Vercel Deployment**:
90
+ - Deploy the `commitpulse-cloud` folder to Vercel.
91
+ - Add the `DATABASE_URL` to your Vercel Environment Variables.
92
+
93
+ ## 🌍 Distribution & PyPI Hosting
94
+
95
+ To share **Commit Pulse** with the global developer community via PyPI:
96
+
97
+ ### 1. Prepare your Account
98
+ - Register at [pypi.org](https://pypi.org/).
99
+ - Generate an API Token in your account settings.
100
+
101
+ ### 2. Build the Distribution
102
+ Ensure you have the latest build tools:
103
+ ```bash
104
+ python -m pip install --upgrade build twine
105
+ ```
106
+ Then build the package:
107
+ ```bash
108
+ python -m build
109
+ ```
110
+ This creates a `dist/` folder with your `.whl` and `.tar.gz` files.
111
+
112
+ ### 3. Upload to PyPI
113
+ Use `twine` to securely upload your package:
114
+ ```bash
115
+ python -m twine upload dist/*
116
+ ```
117
+ - **Username**: `__token__`
118
+ - **Password**: `[Your API Token]`
119
+
120
+ Once uploaded, anyone can install it via:
121
+ ```bash
122
+ pip install commitpulse
123
+ ```
124
+
125
+ ---
126
+ *Built with ❤️ for the development ecosystem.*
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ commitpulse/__init__.py
4
+ commitpulse/analyzer.py
5
+ commitpulse/main.py
6
+ commitpulse/renderer.py
7
+ commitpulse.egg-info/PKG-INFO
8
+ commitpulse.egg-info/SOURCES.txt
9
+ commitpulse.egg-info/dependency_links.txt
10
+ commitpulse.egg-info/entry_points.txt
11
+ commitpulse.egg-info/requires.txt
12
+ commitpulse.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ commitpulse = commitpulse.main:main
@@ -0,0 +1 @@
1
+ requests>=2.31.0
@@ -0,0 +1 @@
1
+ commitpulse
@@ -0,0 +1,23 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "commitpulse"
7
+ version = "0.1.0"
8
+ description = "Premium Git repository analytics and dashboard generator"
9
+ authors = [
10
+ { name = "Antigravity AI" }
11
+ ]
12
+ readme = "README.md"
13
+ requires-python = ">=3.7"
14
+ dependencies = [
15
+ "requests>=2.31.0"
16
+ ]
17
+
18
+ [project.scripts]
19
+ commitpulse = "commitpulse.main:main"
20
+
21
+ [tool.setuptools.packages.find]
22
+ where = ["."]
23
+ include = ["commitpulse*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+