clutch-cli 0.2.0__tar.gz → 0.3.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.
- {clutch_cli-0.2.0 → clutch_cli-0.3.0}/PKG-INFO +10 -10
- {clutch_cli-0.2.0 → clutch_cli-0.3.0}/README.md +9 -9
- clutch_cli-0.3.0/clutch_cli/activity/__init__.py +0 -0
- {clutch_cli-0.2.0/clutch_cli → clutch_cli-0.3.0/clutch_cli/activity}/patterns.py +18 -33
- {clutch_cli-0.2.0/clutch_cli → clutch_cli-0.3.0/clutch_cli/activity}/stats.py +11 -21
- clutch_cli-0.3.0/clutch_cli/activity/streak.py +54 -0
- clutch_cli-0.3.0/clutch_cli/authentication/__init__.py +0 -0
- clutch_cli-0.3.0/clutch_cli/authentication/login.py +110 -0
- clutch_cli-0.3.0/clutch_cli/authentication/logout.py +13 -0
- clutch_cli-0.3.0/clutch_cli/authentication/whoami.py +11 -0
- clutch_cli-0.3.0/clutch_cli/insights/__init__.py +0 -0
- clutch_cli-0.2.0/clutch_cli/insight.py → clutch_cli-0.3.0/clutch_cli/insights/weekly.py +13 -21
- clutch_cli-0.3.0/clutch_cli/main.py +59 -0
- clutch_cli-0.3.0/clutch_cli/repositories/__init__.py +0 -0
- clutch_cli-0.2.0/clutch_cli/repos.py → clutch_cli-0.3.0/clutch_cli/repositories/list.py +9 -30
- clutch_cli-0.3.0/clutch_cli/system/__init__.py +0 -0
- clutch_cli-0.3.0/clutch_cli/system/status.py +45 -0
- clutch_cli-0.3.0/clutch_cli/theme.py +42 -0
- {clutch_cli-0.2.0 → clutch_cli-0.3.0}/clutch_cli.egg-info/PKG-INFO +10 -10
- clutch_cli-0.3.0/clutch_cli.egg-info/SOURCES.txt +27 -0
- {clutch_cli-0.2.0 → clutch_cli-0.3.0}/pyproject.toml +1 -1
- clutch_cli-0.2.0/clutch_cli/auth.py +0 -132
- clutch_cli-0.2.0/clutch_cli/main.py +0 -43
- clutch_cli-0.2.0/clutch_cli/status.py +0 -54
- clutch_cli-0.2.0/clutch_cli/streak.py +0 -80
- clutch_cli-0.2.0/clutch_cli.egg-info/SOURCES.txt +0 -19
- {clutch_cli-0.2.0 → clutch_cli-0.3.0}/clutch_cli/__init__.py +0 -0
- {clutch_cli-0.2.0 → clutch_cli-0.3.0}/clutch_cli/api.py +0 -0
- {clutch_cli-0.2.0 → clutch_cli-0.3.0}/clutch_cli/config.py +0 -0
- {clutch_cli-0.2.0 → clutch_cli-0.3.0}/clutch_cli.egg-info/dependency_links.txt +0 -0
- {clutch_cli-0.2.0 → clutch_cli-0.3.0}/clutch_cli.egg-info/entry_points.txt +0 -0
- {clutch_cli-0.2.0 → clutch_cli-0.3.0}/clutch_cli.egg-info/requires.txt +0 -0
- {clutch_cli-0.2.0 → clutch_cli-0.3.0}/clutch_cli.egg-info/top_level.txt +0 -0
- {clutch_cli-0.2.0 → clutch_cli-0.3.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clutch-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: GitHub tracks your work. Clutch tracks you.
|
|
5
5
|
Author-email: Lay Patel <lay.patel.1313@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -35,7 +35,7 @@ Requires-Dist: rich>=13.0.0
|
|
|
35
35
|
|
|
36
36
|
---
|
|
37
37
|
|
|
38
|
-
`clutch-cli` is the terminal companion for [Clutch](https://clutch-
|
|
38
|
+
`clutch-cli` is the terminal companion for [Clutch](https://clutch-woad.vercel.app) — an open-source AI-powered developer activity dashboard. Get your GitHub streaks, stats, coding patterns, and AI insights without leaving your terminal.
|
|
39
39
|
|
|
40
40
|
## Installation
|
|
41
41
|
|
|
@@ -47,7 +47,7 @@ pip install clutch-cli
|
|
|
47
47
|
|
|
48
48
|
```bash
|
|
49
49
|
# Login via GitHub (opens browser automatically, no copy-paste needed)
|
|
50
|
-
clutch
|
|
50
|
+
clutch login
|
|
51
51
|
|
|
52
52
|
# Check your streak
|
|
53
53
|
clutch streak
|
|
@@ -63,9 +63,9 @@ clutch insight
|
|
|
63
63
|
|
|
64
64
|
| Command | Description |
|
|
65
65
|
|---|---|
|
|
66
|
-
| `clutch
|
|
67
|
-
| `clutch
|
|
68
|
-
| `clutch
|
|
66
|
+
| `clutch login` | Login via GitHub OAuth (fully automatic) |
|
|
67
|
+
| `clutch logout` | Logout and clear saved credentials |
|
|
68
|
+
| `clutch whoami` | Show currently logged-in user |
|
|
69
69
|
| `clutch streak` | Current and longest commit streak |
|
|
70
70
|
| `clutch stats` | Activity stats — commits, PRs, issues, active days |
|
|
71
71
|
| `clutch stats --days 7` | Stats for a custom time range |
|
|
@@ -77,10 +77,10 @@ clutch insight
|
|
|
77
77
|
|
|
78
78
|
## How Login Works
|
|
79
79
|
|
|
80
|
-
`clutch
|
|
80
|
+
`clutch login` spins up a temporary local server on port `9876`, opens GitHub OAuth in your browser, and automatically captures the token when GitHub redirects back. No copy-pasting required.
|
|
81
81
|
|
|
82
82
|
```
|
|
83
|
-
$ clutch
|
|
83
|
+
$ clutch login
|
|
84
84
|
|
|
85
85
|
⚡ Clutch Login
|
|
86
86
|
Opening GitHub in your browser...
|
|
@@ -98,12 +98,12 @@ By default the CLI talks to the hosted Clutch API. To point it at a local backen
|
|
|
98
98
|
|
|
99
99
|
```bash
|
|
100
100
|
export CLUTCH_API_URL=http://localhost:8000
|
|
101
|
-
clutch
|
|
101
|
+
clutch login
|
|
102
102
|
```
|
|
103
103
|
|
|
104
104
|
## Links
|
|
105
105
|
|
|
106
|
-
- 🌐 [Live Dashboard](https://clutch-
|
|
106
|
+
- 🌐 [Live Dashboard](https://clutch-woad.vercel.app)
|
|
107
107
|
- 📖 [Full Documentation](https://github.com/laypatel13/clutch)
|
|
108
108
|
- 🐛 [Report a Bug](https://github.com/laypatel13/clutch/issues)
|
|
109
109
|
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
-
`clutch-cli` is the terminal companion for [Clutch](https://clutch-
|
|
15
|
+
`clutch-cli` is the terminal companion for [Clutch](https://clutch-woad.vercel.app) — an open-source AI-powered developer activity dashboard. Get your GitHub streaks, stats, coding patterns, and AI insights without leaving your terminal.
|
|
16
16
|
|
|
17
17
|
## Installation
|
|
18
18
|
|
|
@@ -24,7 +24,7 @@ pip install clutch-cli
|
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
26
|
# Login via GitHub (opens browser automatically, no copy-paste needed)
|
|
27
|
-
clutch
|
|
27
|
+
clutch login
|
|
28
28
|
|
|
29
29
|
# Check your streak
|
|
30
30
|
clutch streak
|
|
@@ -40,9 +40,9 @@ clutch insight
|
|
|
40
40
|
|
|
41
41
|
| Command | Description |
|
|
42
42
|
|---|---|
|
|
43
|
-
| `clutch
|
|
44
|
-
| `clutch
|
|
45
|
-
| `clutch
|
|
43
|
+
| `clutch login` | Login via GitHub OAuth (fully automatic) |
|
|
44
|
+
| `clutch logout` | Logout and clear saved credentials |
|
|
45
|
+
| `clutch whoami` | Show currently logged-in user |
|
|
46
46
|
| `clutch streak` | Current and longest commit streak |
|
|
47
47
|
| `clutch stats` | Activity stats — commits, PRs, issues, active days |
|
|
48
48
|
| `clutch stats --days 7` | Stats for a custom time range |
|
|
@@ -54,10 +54,10 @@ clutch insight
|
|
|
54
54
|
|
|
55
55
|
## How Login Works
|
|
56
56
|
|
|
57
|
-
`clutch
|
|
57
|
+
`clutch login` spins up a temporary local server on port `9876`, opens GitHub OAuth in your browser, and automatically captures the token when GitHub redirects back. No copy-pasting required.
|
|
58
58
|
|
|
59
59
|
```
|
|
60
|
-
$ clutch
|
|
60
|
+
$ clutch login
|
|
61
61
|
|
|
62
62
|
⚡ Clutch Login
|
|
63
63
|
Opening GitHub in your browser...
|
|
@@ -75,12 +75,12 @@ By default the CLI talks to the hosted Clutch API. To point it at a local backen
|
|
|
75
75
|
|
|
76
76
|
```bash
|
|
77
77
|
export CLUTCH_API_URL=http://localhost:8000
|
|
78
|
-
clutch
|
|
78
|
+
clutch login
|
|
79
79
|
```
|
|
80
80
|
|
|
81
81
|
## Links
|
|
82
82
|
|
|
83
|
-
- 🌐 [Live Dashboard](https://clutch-
|
|
83
|
+
- 🌐 [Live Dashboard](https://clutch-woad.vercel.app)
|
|
84
84
|
- 📖 [Full Documentation](https://github.com/laypatel13/clutch)
|
|
85
85
|
- 🐛 [Report a Bug](https://github.com/laypatel13/clutch/issues)
|
|
86
86
|
|
|
File without changes
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import typer
|
|
2
|
-
from rich.console import Console
|
|
3
2
|
from rich.table import Table
|
|
4
3
|
from rich import box
|
|
5
4
|
from clutch_cli.api import get_client
|
|
6
|
-
|
|
7
|
-
console = Console()
|
|
5
|
+
from clutch_cli.theme import console, ACCENT, DIM, SUCCESS, WARNING, ERROR, header, footer, bar
|
|
8
6
|
|
|
9
7
|
|
|
10
8
|
def patterns():
|
|
@@ -12,81 +10,68 @@ def patterns():
|
|
|
12
10
|
with get_client() as client:
|
|
13
11
|
try:
|
|
14
12
|
console.print()
|
|
15
|
-
console.print("[
|
|
13
|
+
console.print(f"[{DIM}]Analyzing patterns...[/{DIM}]")
|
|
16
14
|
|
|
17
15
|
response = client.get("/insights/patterns")
|
|
18
16
|
if response.status_code != 200:
|
|
19
|
-
console.print("[red]Error: Failed to fetch patterns.[/red]")
|
|
17
|
+
console.print("[bold red]Error: Failed to fetch patterns.[/bold red]")
|
|
20
18
|
raise typer.Exit(1)
|
|
21
19
|
|
|
22
20
|
data = response.json()
|
|
23
21
|
|
|
24
22
|
if "message" in data:
|
|
25
|
-
console.print(f"[
|
|
23
|
+
console.print(f"[{WARNING}]{data['message']}[/{WARNING}]")
|
|
26
24
|
raise typer.Exit()
|
|
27
25
|
|
|
28
|
-
|
|
29
|
-
console.rule("[bold blue]⚡ CLUTCH — CODING PATTERNS[/bold blue]")
|
|
30
|
-
console.print()
|
|
26
|
+
header("CODING PATTERNS")
|
|
31
27
|
|
|
32
|
-
# Summary table
|
|
33
28
|
score = data["consistency_score"]
|
|
34
|
-
|
|
29
|
+
score_style = SUCCESS if score >= 70 else WARNING if score >= 40 else ERROR
|
|
35
30
|
|
|
36
31
|
summary = Table(box=box.SIMPLE, show_header=False, pad_edge=False)
|
|
37
|
-
summary.add_column("Label", style=
|
|
32
|
+
summary.add_column("Label", style=DIM, width=22)
|
|
38
33
|
summary.add_column("Value", style="bold white")
|
|
39
34
|
|
|
40
|
-
summary.add_row("Best Day", f"[
|
|
41
|
-
summary.add_row("Worst Day", f"[
|
|
35
|
+
summary.add_row("Best Day", f"[{ACCENT}]{data['best_day']}[/{ACCENT}]")
|
|
36
|
+
summary.add_row("Worst Day", f"[{DIM}]{data['worst_day']}[/{DIM}]")
|
|
42
37
|
summary.add_row("Avg Daily Commits", str(data["avg_daily_commits"]))
|
|
43
38
|
summary.add_row("Total Active Days", str(data["total_active_days"]))
|
|
44
|
-
summary.add_row(
|
|
45
|
-
"Consistency Score",
|
|
46
|
-
f"[{score_color}]{score}%[/{score_color}]",
|
|
47
|
-
)
|
|
39
|
+
summary.add_row("Consistency Score", f"[{score_style}]{score}%[/{score_style}]")
|
|
48
40
|
|
|
49
41
|
console.print(summary)
|
|
50
42
|
console.print()
|
|
51
|
-
console.rule("[
|
|
43
|
+
console.rule(f"[{DIM}]Activity by Day[/{DIM}]", style=DIM)
|
|
52
44
|
console.print()
|
|
53
45
|
|
|
54
|
-
# Day distribution bar chart
|
|
55
46
|
day_table = Table(box=box.SIMPLE, show_header=False, pad_edge=False)
|
|
56
|
-
day_table.add_column("Day", style=
|
|
47
|
+
day_table.add_column("Day", style=DIM, width=12)
|
|
57
48
|
day_table.add_column("Bar", width=32)
|
|
58
49
|
day_table.add_column("Count", justify="right", style="bold white", width=6)
|
|
59
50
|
|
|
60
51
|
max_commits = max(data["day_distribution"].values()) or 1
|
|
61
52
|
for day, commits in data["day_distribution"].items():
|
|
62
|
-
filled = int((commits / max_commits) * 28)
|
|
63
53
|
is_best = day == data["best_day"]
|
|
64
|
-
|
|
65
|
-
bar
|
|
66
|
-
day_label = f"[blue]{day}[/blue]" if is_best else day
|
|
67
|
-
day_table.add_row(day_label, bar, str(commits))
|
|
54
|
+
day_label = f"[{ACCENT}]{day}[/{ACCENT}]" if is_best else day
|
|
55
|
+
day_table.add_row(day_label, bar(commits, max_commits), str(commits))
|
|
68
56
|
|
|
69
57
|
console.print(day_table)
|
|
70
58
|
|
|
71
|
-
# Top repos
|
|
72
59
|
if data.get("top_repos"):
|
|
73
60
|
console.print()
|
|
74
|
-
console.rule("[
|
|
61
|
+
console.rule(f"[{DIM}]Most Active Repos[/{DIM}]", style=DIM)
|
|
75
62
|
console.print()
|
|
76
63
|
|
|
77
64
|
repo_table = Table(box=box.SIMPLE, show_header=False, pad_edge=False)
|
|
78
65
|
repo_table.add_column("Repo", style="bold white", width=30)
|
|
79
|
-
repo_table.add_column("Active Days", justify="right", style=
|
|
66
|
+
repo_table.add_column("Active Days", justify="right", style=DIM, width=12)
|
|
80
67
|
|
|
81
68
|
for repo in data["top_repos"]:
|
|
82
69
|
repo_table.add_row(repo["repo"], str(repo["days_active"]))
|
|
83
70
|
|
|
84
71
|
console.print(repo_table)
|
|
85
72
|
|
|
86
|
-
|
|
87
|
-
console.rule(style="dim")
|
|
88
|
-
console.print()
|
|
73
|
+
footer()
|
|
89
74
|
|
|
90
75
|
except Exception:
|
|
91
|
-
console.print("[red]Error: Could not connect to Clutch API.[/red]")
|
|
76
|
+
console.print("[bold red]Error: Could not connect to Clutch API.[/bold red]")
|
|
92
77
|
raise typer.Exit(1)
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import typer
|
|
2
|
-
from rich.console import Console
|
|
3
2
|
from rich.table import Table
|
|
4
3
|
from rich import box
|
|
5
4
|
from clutch_cli.api import get_client
|
|
6
|
-
|
|
7
|
-
console = Console()
|
|
5
|
+
from clutch_cli.theme import console, ACCENT, DIM, SUCCESS, header, footer, bar
|
|
8
6
|
|
|
9
7
|
|
|
10
8
|
def stats(days: int = typer.Option(30, help="Number of days to look back.")):
|
|
@@ -13,7 +11,7 @@ def stats(days: int = typer.Option(30, help="Number of days to look back.")):
|
|
|
13
11
|
try:
|
|
14
12
|
response = client.get(f"/github/activity?days={days}")
|
|
15
13
|
if response.status_code != 200:
|
|
16
|
-
console.print("[red]Error: Failed to fetch activity data.[/red]")
|
|
14
|
+
console.print("[bold red]Error: Failed to fetch activity data.[/bold red]")
|
|
17
15
|
raise typer.Exit(1)
|
|
18
16
|
|
|
19
17
|
data = response.json()
|
|
@@ -22,34 +20,26 @@ def stats(days: int = typer.Option(30, help="Number of days to look back.")):
|
|
|
22
20
|
issues = data["total_issues"]
|
|
23
21
|
active = data["active_days"]
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
console.rule(f"[bold blue]⚡ CLUTCH — STATS · LAST {days} DAYS[/bold blue]")
|
|
27
|
-
console.print()
|
|
23
|
+
header(f"STATS · LAST {days} DAYS")
|
|
28
24
|
|
|
29
25
|
table = Table(box=box.SIMPLE, show_header=True, pad_edge=False)
|
|
30
|
-
table.add_column("Metric", style=
|
|
26
|
+
table.add_column("Metric", style=DIM, width=22)
|
|
31
27
|
table.add_column("Count", justify="right", style="bold white", width=10)
|
|
32
28
|
table.add_column("Bar", width=32)
|
|
33
29
|
|
|
34
30
|
max_val = max(commits, prs, issues, active, 1)
|
|
35
31
|
|
|
36
|
-
def bar(val):
|
|
37
|
-
filled = int((val / max_val) * 28)
|
|
38
|
-
return f"[blue]{'█' * filled}[/blue][dim]{'░' * (28 - filled)}[/dim]"
|
|
39
|
-
|
|
40
32
|
def color(val):
|
|
41
|
-
return
|
|
33
|
+
return SUCCESS if val > 0 else DIM
|
|
42
34
|
|
|
43
|
-
table.add_row("Commits", f"[{color(commits)}]{commits}[/{color(commits)}]", bar(commits))
|
|
44
|
-
table.add_row("Pull Requests", f"[{color(prs)}]{prs}[/{color(prs)}]", bar(prs))
|
|
45
|
-
table.add_row("Issues", f"[{color(issues)}]{issues}[/{color(issues)}]", bar(issues))
|
|
46
|
-
table.add_row("Active Days", f"[{color(active)}]{active}[/{color(active)}]", bar(active))
|
|
35
|
+
table.add_row("Commits", f"[{color(commits)}]{commits}[/{color(commits)}]", bar(commits, max_val))
|
|
36
|
+
table.add_row("Pull Requests", f"[{color(prs)}]{prs}[/{color(prs)}]", bar(prs, max_val))
|
|
37
|
+
table.add_row("Issues", f"[{color(issues)}]{issues}[/{color(issues)}]", bar(issues, max_val))
|
|
38
|
+
table.add_row("Active Days", f"[{color(active)}]{active}[/{color(active)}]", bar(active, max_val))
|
|
47
39
|
|
|
48
40
|
console.print(table)
|
|
49
|
-
|
|
50
|
-
console.rule(style="dim")
|
|
51
|
-
console.print()
|
|
41
|
+
footer()
|
|
52
42
|
|
|
53
43
|
except Exception:
|
|
54
|
-
console.print("[red]Error: Could not connect to Clutch API.[/red]")
|
|
44
|
+
console.print("[bold red]Error: Could not connect to Clutch API.[/bold red]")
|
|
55
45
|
raise typer.Exit(1)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from rich.table import Table
|
|
3
|
+
from rich import box
|
|
4
|
+
from clutch_cli.api import get_client
|
|
5
|
+
from clutch_cli.theme import console, ACCENT, DIM, SUCCESS, WARNING, header, footer, bar
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def streak():
|
|
9
|
+
"""Show your current and longest commit streak."""
|
|
10
|
+
with get_client() as client:
|
|
11
|
+
try:
|
|
12
|
+
response = client.get("/github/streak")
|
|
13
|
+
if response.status_code != 200:
|
|
14
|
+
console.print("[bold red]Error: Failed to fetch streak data.[/bold red]")
|
|
15
|
+
raise typer.Exit(1)
|
|
16
|
+
|
|
17
|
+
data = response.json()
|
|
18
|
+
current = data["current_streak"]
|
|
19
|
+
longest = data["longest_streak"]
|
|
20
|
+
total_active = data["total_active_days"]
|
|
21
|
+
|
|
22
|
+
if current >= 14:
|
|
23
|
+
status = "STRONG"
|
|
24
|
+
status_style = SUCCESS
|
|
25
|
+
elif current >= 7:
|
|
26
|
+
status = "BUILDING"
|
|
27
|
+
status_style = ACCENT
|
|
28
|
+
elif current > 0:
|
|
29
|
+
status = "ACTIVE"
|
|
30
|
+
status_style = WARNING
|
|
31
|
+
else:
|
|
32
|
+
status = "INACTIVE"
|
|
33
|
+
status_style = DIM
|
|
34
|
+
|
|
35
|
+
header("STREAK")
|
|
36
|
+
|
|
37
|
+
table = Table(box=box.SIMPLE, show_header=False, pad_edge=False)
|
|
38
|
+
table.add_column("Label", style=DIM, width=22)
|
|
39
|
+
table.add_column("Value", style="bold white")
|
|
40
|
+
table.add_column("Badge", justify="right")
|
|
41
|
+
|
|
42
|
+
table.add_row("Current Streak", f"[{ACCENT}]{current} days[/{ACCENT}]", f"[{status_style}]{status}[/{status_style}]")
|
|
43
|
+
table.add_row("Longest Streak", f"{longest} days", "")
|
|
44
|
+
table.add_row("Total Active Days", f"{total_active} days", "")
|
|
45
|
+
|
|
46
|
+
console.print(table)
|
|
47
|
+
if longest > 0:
|
|
48
|
+
console.print(f" [{DIM}]Progress to longest[/{DIM}] {bar(current, longest)} [{DIM}]{current}/{longest}[/{DIM}]")
|
|
49
|
+
|
|
50
|
+
footer()
|
|
51
|
+
|
|
52
|
+
except Exception:
|
|
53
|
+
console.print("[bold red]Error: Could not connect to Clutch API.[/bold red]")
|
|
54
|
+
raise typer.Exit(1)
|
|
File without changes
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import webbrowser
|
|
2
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
3
|
+
from urllib.parse import parse_qs, urlparse
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
from clutch_cli.config import API_BASE_URL, save_token
|
|
7
|
+
from clutch_cli.theme import console, ACCENT, ERROR, DIM, SUCCESS
|
|
8
|
+
|
|
9
|
+
CLI_CALLBACK_PORT = 9876
|
|
10
|
+
_captured_token: dict = {}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _CallbackHandler(BaseHTTPRequestHandler):
|
|
14
|
+
def do_GET(self):
|
|
15
|
+
parsed = urlparse(self.path)
|
|
16
|
+
if parsed.path == "/callback":
|
|
17
|
+
params = parse_qs(parsed.query)
|
|
18
|
+
token = params.get("token", [None])[0]
|
|
19
|
+
if token:
|
|
20
|
+
_captured_token["value"] = token
|
|
21
|
+
self._respond(200, _success_page())
|
|
22
|
+
else:
|
|
23
|
+
self._respond(400, _error_page("No token received."))
|
|
24
|
+
else:
|
|
25
|
+
self._respond(404, b"Not found")
|
|
26
|
+
|
|
27
|
+
def _respond(self, status: int, body: bytes) -> None:
|
|
28
|
+
self.send_response(status)
|
|
29
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
30
|
+
self.end_headers()
|
|
31
|
+
self.wfile.write(body)
|
|
32
|
+
|
|
33
|
+
def log_message(self, *args):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _success_page() -> bytes:
|
|
38
|
+
return """<!DOCTYPE html>
|
|
39
|
+
<html>
|
|
40
|
+
<head>
|
|
41
|
+
<title>Clutch</title>
|
|
42
|
+
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>⚙️</text></svg>" />
|
|
43
|
+
</head>
|
|
44
|
+
<body style="font-family:-apple-system,sans-serif;text-align:center;padding:80px;background:#0a0a0a;color:#ffffff">
|
|
45
|
+
<h1 style="font-weight:800;letter-spacing:1px">CLUTCH</h1>
|
|
46
|
+
<p style="color:#ffffff;font-size:1.1rem;opacity:0.9">✓ Login successful — you can close this tab.</p>
|
|
47
|
+
</body>
|
|
48
|
+
</html>""".encode()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _error_page(msg: str) -> bytes:
|
|
52
|
+
return f"""<!DOCTYPE html>
|
|
53
|
+
<html>
|
|
54
|
+
<head>
|
|
55
|
+
<title>Clutch</title>
|
|
56
|
+
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>⚙️</text></svg>" />
|
|
57
|
+
</head>
|
|
58
|
+
<body style="font-family:-apple-system,sans-serif;text-align:center;padding:80px;background:#0a0a0a;color:#ffffff">
|
|
59
|
+
<h1 style="font-weight:800;letter-spacing:1px">CLUTCH</h1>
|
|
60
|
+
<p style="opacity:0.8">Login failed: {msg}</p>
|
|
61
|
+
</body>
|
|
62
|
+
</html>""".encode()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def login():
|
|
66
|
+
"""Login to Clutch via GitHub OAuth (browser-based, fully automatic)."""
|
|
67
|
+
console.print()
|
|
68
|
+
console.rule(f"[{ACCENT}]⚡ CLUTCH — LOGIN[/{ACCENT}]")
|
|
69
|
+
console.print(f"[{DIM}]Starting local callback listener...[/{DIM}]")
|
|
70
|
+
|
|
71
|
+
_captured_token.clear()
|
|
72
|
+
server = HTTPServer(("localhost", CLI_CALLBACK_PORT), _CallbackHandler)
|
|
73
|
+
|
|
74
|
+
login_url = f"{API_BASE_URL}/auth/github?cli=true"
|
|
75
|
+
console.print(f"[{DIM}]Opening GitHub in your browser...[/{DIM}]\n")
|
|
76
|
+
webbrowser.open(login_url)
|
|
77
|
+
|
|
78
|
+
console.print(f"[{ACCENT}]Waiting for GitHub authorization...[/{ACCENT}]")
|
|
79
|
+
console.print(f"[{DIM}](If your browser didn't open, visit:)[/{DIM}]")
|
|
80
|
+
console.print(f"[{DIM}]{login_url}[/{DIM}]\n")
|
|
81
|
+
|
|
82
|
+
server.handle_request()
|
|
83
|
+
server.server_close()
|
|
84
|
+
|
|
85
|
+
token = _captured_token.get("value")
|
|
86
|
+
if not token:
|
|
87
|
+
console.print(f"[{ERROR}]Login failed — no token received.[/{ERROR}]")
|
|
88
|
+
raise SystemExit(1)
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
response = httpx.get(
|
|
92
|
+
f"{API_BASE_URL}/users/me",
|
|
93
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
94
|
+
timeout=10,
|
|
95
|
+
)
|
|
96
|
+
if response.status_code != 200:
|
|
97
|
+
console.print(f"[{ERROR}]Token validation failed.[/{ERROR}]")
|
|
98
|
+
raise SystemExit(1)
|
|
99
|
+
|
|
100
|
+
user = response.json()
|
|
101
|
+
save_token(token, user["username"])
|
|
102
|
+
console.print(f"[{SUCCESS}]Logged in as @{user['username']}[/{SUCCESS}]")
|
|
103
|
+
console.print(f"[{DIM}]Welcome to Clutch, {user.get('name') or user['username']}.[/{DIM}]")
|
|
104
|
+
console.print()
|
|
105
|
+
console.rule(style=DIM)
|
|
106
|
+
console.print()
|
|
107
|
+
|
|
108
|
+
except httpx.RequestError:
|
|
109
|
+
console.print(f"[{ERROR}]Could not connect to Clutch API.[/{ERROR}]")
|
|
110
|
+
raise SystemExit(1)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from clutch_cli.config import clear_config, get_username
|
|
2
|
+
from clutch_cli.theme import console, DIM, SUCCESS, WARNING
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def logout():
|
|
6
|
+
"""Logout from Clutch."""
|
|
7
|
+
username = get_username()
|
|
8
|
+
if not username:
|
|
9
|
+
console.print(f"[{WARNING}]You are not logged in.[/{WARNING}]")
|
|
10
|
+
raise SystemExit()
|
|
11
|
+
|
|
12
|
+
clear_config()
|
|
13
|
+
console.print(f"[{SUCCESS}]Logged out successfully.[/{SUCCESS}]")
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from clutch_cli.config import get_username
|
|
2
|
+
from clutch_cli.theme import console, ACCENT, WARNING
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def whoami():
|
|
6
|
+
"""Show the currently logged-in user."""
|
|
7
|
+
username = get_username()
|
|
8
|
+
if not username:
|
|
9
|
+
console.print(f"[{WARNING}]Not logged in. Run: clutch login[/{WARNING}]")
|
|
10
|
+
raise SystemExit()
|
|
11
|
+
console.print(f"[{ACCENT}]Logged in as @{username}[/{ACCENT}]")
|
|
File without changes
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import typer
|
|
2
|
-
from rich.console import Console
|
|
3
2
|
from rich.table import Table
|
|
4
3
|
from rich.text import Text
|
|
5
4
|
from rich import box
|
|
6
5
|
from clutch_cli.api import get_client
|
|
7
|
-
|
|
8
|
-
console = Console()
|
|
6
|
+
from clutch_cli.theme import console, DIM, SUCCESS, WARNING, header, footer
|
|
9
7
|
|
|
10
8
|
|
|
11
9
|
def insight():
|
|
@@ -13,51 +11,45 @@ def insight():
|
|
|
13
11
|
with get_client() as client:
|
|
14
12
|
try:
|
|
15
13
|
console.print()
|
|
16
|
-
console.print("[
|
|
14
|
+
console.print(f"[{DIM}]Generating insight...[/{DIM}]")
|
|
17
15
|
|
|
18
16
|
response = client.get("/insights/weekly")
|
|
19
17
|
if response.status_code != 200:
|
|
20
|
-
console.print("[red]Error: Failed to fetch insight.[/red]")
|
|
18
|
+
console.print("[bold red]Error: Failed to fetch insight.[/bold red]")
|
|
21
19
|
raise typer.Exit(1)
|
|
22
20
|
|
|
23
21
|
data = response.json()
|
|
24
22
|
|
|
25
23
|
if "message" in data:
|
|
26
|
-
console.print(f"[
|
|
24
|
+
console.print(f"[{WARNING}]{data['message']}[/{WARNING}]")
|
|
27
25
|
raise typer.Exit()
|
|
28
26
|
|
|
29
27
|
stats = data["stats"]
|
|
30
28
|
summary = data["ai_summary"]
|
|
31
29
|
|
|
32
|
-
|
|
33
|
-
console.rule("[bold blue]⚡ CLUTCH — WEEKLY INSIGHT[/bold blue]")
|
|
34
|
-
console.print()
|
|
30
|
+
header("WEEKLY INSIGHT")
|
|
35
31
|
|
|
36
|
-
|
|
37
|
-
console.print(f"[dim]Week of[/dim] [white]{data['week_start']}[/white]")
|
|
32
|
+
console.print(f"[{DIM}]Week of[/{DIM}] [bold white]{data['week_start']}[/bold white]")
|
|
38
33
|
console.print()
|
|
39
34
|
console.print(Text(summary, style="white"), soft_wrap=True)
|
|
40
35
|
console.print()
|
|
41
|
-
console.rule(style=
|
|
36
|
+
console.rule(style=DIM)
|
|
42
37
|
console.print()
|
|
43
38
|
|
|
44
|
-
# Stats row
|
|
45
39
|
table = Table(box=box.SIMPLE, show_header=False, pad_edge=False)
|
|
46
|
-
table.add_column("Label", style=
|
|
40
|
+
table.add_column("Label", style=DIM, width=22)
|
|
47
41
|
table.add_column("Value", style="bold white")
|
|
48
42
|
|
|
49
|
-
table.add_row("Commits", f"[
|
|
50
|
-
table.add_row("Active Days", f"[
|
|
43
|
+
table.add_row("Commits", f"[{SUCCESS}]{stats['total_commits']}[/{SUCCESS}]")
|
|
44
|
+
table.add_row("Active Days", f"[{SUCCESS}]{stats['active_days']}[/{SUCCESS}]")
|
|
51
45
|
table.add_row("Pull Requests", str(stats["total_prs"]))
|
|
52
46
|
table.add_row("Issues", str(stats["total_issues"]))
|
|
53
47
|
table.add_row("Best Day", str(stats["best_day"]))
|
|
54
|
-
table.add_row("Generated by", f"[
|
|
48
|
+
table.add_row("Generated by", f"[{DIM}]{data['generated_by']}[/{DIM}]")
|
|
55
49
|
|
|
56
50
|
console.print(table)
|
|
57
|
-
|
|
58
|
-
console.rule(style="dim")
|
|
59
|
-
console.print()
|
|
51
|
+
footer()
|
|
60
52
|
|
|
61
53
|
except Exception:
|
|
62
|
-
console.print("[red]Error: Could not connect to Clutch API.[/red]")
|
|
54
|
+
console.print("[bold red]Error: Could not connect to Clutch API.[/bold red]")
|
|
63
55
|
raise typer.Exit(1)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
|
|
3
|
+
from clutch_cli.authentication import login, logout, whoami
|
|
4
|
+
from clutch_cli.activity import streak, stats, patterns
|
|
5
|
+
from clutch_cli.repositories import list as repositories_list
|
|
6
|
+
from clutch_cli.insights import weekly
|
|
7
|
+
from clutch_cli.system import status
|
|
8
|
+
|
|
9
|
+
__version__ = "0.3.0"
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(
|
|
12
|
+
name="clutch",
|
|
13
|
+
help="GitHub tracks your work. Clutch tracks you.",
|
|
14
|
+
no_args_is_help=True,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _version_callback(value: bool):
|
|
19
|
+
if value:
|
|
20
|
+
typer.echo(f"clutch v{__version__}")
|
|
21
|
+
raise typer.Exit()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@app.callback()
|
|
25
|
+
def main(
|
|
26
|
+
version: bool = typer.Option(
|
|
27
|
+
None,
|
|
28
|
+
"--version",
|
|
29
|
+
"-v",
|
|
30
|
+
help="Show version and exit.",
|
|
31
|
+
callback=_version_callback,
|
|
32
|
+
is_eager=True,
|
|
33
|
+
),
|
|
34
|
+
):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Authentication
|
|
39
|
+
app.command(name="login")(login.login)
|
|
40
|
+
app.command(name="logout")(logout.logout)
|
|
41
|
+
app.command(name="whoami")(whoami.whoami)
|
|
42
|
+
|
|
43
|
+
# Activity
|
|
44
|
+
app.command(name="streak")(streak.streak)
|
|
45
|
+
app.command(name="stats")(stats.stats)
|
|
46
|
+
app.command(name="patterns")(patterns.patterns)
|
|
47
|
+
|
|
48
|
+
# Repositories
|
|
49
|
+
app.command(name="repos")(repositories_list.repos)
|
|
50
|
+
|
|
51
|
+
# Insights
|
|
52
|
+
app.command(name="insight")(weekly.insight)
|
|
53
|
+
|
|
54
|
+
# System
|
|
55
|
+
app.command(name="status")(status.status)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
if __name__ == "__main__":
|
|
59
|
+
app()
|
|
File without changes
|
|
@@ -1,24 +1,8 @@
|
|
|
1
1
|
import typer
|
|
2
|
-
from rich.console import Console
|
|
3
2
|
from rich.table import Table
|
|
4
3
|
from rich import box
|
|
5
4
|
from clutch_cli.api import get_client
|
|
6
|
-
|
|
7
|
-
console = Console()
|
|
8
|
-
|
|
9
|
-
LANG_COLORS = {
|
|
10
|
-
"Python": "blue",
|
|
11
|
-
"TypeScript": "cyan",
|
|
12
|
-
"JavaScript": "yellow",
|
|
13
|
-
"Go": "cyan",
|
|
14
|
-
"Rust": "red",
|
|
15
|
-
"Java": "red",
|
|
16
|
-
"C++": "blue",
|
|
17
|
-
"C": "blue",
|
|
18
|
-
"Shell": "green",
|
|
19
|
-
"HTML": "red",
|
|
20
|
-
"CSS": "blue",
|
|
21
|
-
}
|
|
5
|
+
from clutch_cli.theme import console, ACCENT, DIM, WARNING, header, footer
|
|
22
6
|
|
|
23
7
|
|
|
24
8
|
def repos():
|
|
@@ -27,41 +11,36 @@ def repos():
|
|
|
27
11
|
try:
|
|
28
12
|
response = client.get("/github/repos")
|
|
29
13
|
if response.status_code != 200:
|
|
30
|
-
console.print("[red]Error: Failed to fetch repositories.[/red]")
|
|
14
|
+
console.print("[bold red]Error: Failed to fetch repositories.[/bold red]")
|
|
31
15
|
raise typer.Exit(1)
|
|
32
16
|
|
|
33
17
|
repos_data = response.json()
|
|
34
18
|
|
|
35
|
-
|
|
36
|
-
console.rule("[bold blue]⚡ CLUTCH — REPOSITORIES[/bold blue]")
|
|
37
|
-
console.print()
|
|
19
|
+
header("REPOSITORIES")
|
|
38
20
|
|
|
39
21
|
table = Table(box=box.SIMPLE, show_header=True, pad_edge=False)
|
|
40
22
|
table.add_column("Repository", style="bold white", width=28)
|
|
41
23
|
table.add_column("Language", width=14)
|
|
42
24
|
table.add_column("Stars", justify="right", width=7)
|
|
43
25
|
table.add_column("Forks", justify="right", width=7)
|
|
44
|
-
table.add_column("Updated", style=
|
|
26
|
+
table.add_column("Updated", style=DIM, width=12)
|
|
45
27
|
|
|
46
28
|
for repo in repos_data[:10]:
|
|
47
29
|
lang = repo.get("language") or "—"
|
|
48
|
-
lang_color = LANG_COLORS.get(lang, "white")
|
|
49
30
|
stars = repo.get("stargazers_count", 0)
|
|
50
31
|
forks = repo.get("forks_count", 0)
|
|
51
32
|
|
|
52
33
|
table.add_row(
|
|
53
34
|
repo["name"],
|
|
54
|
-
|
|
55
|
-
f"[
|
|
56
|
-
str(forks) if forks > 0 else "[
|
|
35
|
+
lang,
|
|
36
|
+
f"[{WARNING}]{stars}[/{WARNING}]" if stars > 0 else f"[{DIM}]0[/{DIM}]",
|
|
37
|
+
str(forks) if forks > 0 else f"[{DIM}]0[/{DIM}]",
|
|
57
38
|
repo["updated_at"][:10],
|
|
58
39
|
)
|
|
59
40
|
|
|
60
41
|
console.print(table)
|
|
61
|
-
|
|
62
|
-
console.rule(style="dim")
|
|
63
|
-
console.print()
|
|
42
|
+
footer()
|
|
64
43
|
|
|
65
44
|
except Exception:
|
|
66
|
-
console.print("[red]Error: Could not connect to Clutch API.[/red]")
|
|
45
|
+
console.print("[bold red]Error: Could not connect to Clutch API.[/bold red]")
|
|
67
46
|
raise typer.Exit(1)
|
|
File without changes
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
from rich.table import Table
|
|
3
|
+
from rich import box
|
|
4
|
+
from clutch_cli.config import API_BASE_URL, get_token, get_username
|
|
5
|
+
from clutch_cli.theme import console, ACCENT, DIM, SUCCESS, ERROR, WARNING, header, footer
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def status():
|
|
9
|
+
"""Show login status and API health."""
|
|
10
|
+
username = get_username()
|
|
11
|
+
token = get_token()
|
|
12
|
+
|
|
13
|
+
header("STATUS")
|
|
14
|
+
|
|
15
|
+
table = Table(box=box.SIMPLE, show_header=False, pad_edge=False)
|
|
16
|
+
table.add_column("Check", style=DIM, width=18)
|
|
17
|
+
table.add_column("Result", style="bold white")
|
|
18
|
+
|
|
19
|
+
if not username or not token:
|
|
20
|
+
table.add_row("Auth", f"[{ERROR}]Not logged in[/{ERROR}]")
|
|
21
|
+
table.add_row("Hint", f"[{DIM}]Run: clutch login[/{DIM}]")
|
|
22
|
+
console.print(table)
|
|
23
|
+
footer()
|
|
24
|
+
raise SystemExit()
|
|
25
|
+
|
|
26
|
+
table.add_row("User", f"[{ACCENT}]@{username}[/{ACCENT}]")
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
response = httpx.get(
|
|
30
|
+
f"{API_BASE_URL}/users/me",
|
|
31
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
32
|
+
timeout=8,
|
|
33
|
+
)
|
|
34
|
+
if response.status_code == 200:
|
|
35
|
+
table.add_row("Token", f"[{SUCCESS}]Valid[/{SUCCESS}]")
|
|
36
|
+
table.add_row("API", f"[{SUCCESS}]Reachable[/{SUCCESS}] [{DIM}]{API_BASE_URL}[/{DIM}]")
|
|
37
|
+
else:
|
|
38
|
+
table.add_row("Token", f"[{WARNING}]Expired ({response.status_code})[/{WARNING}]")
|
|
39
|
+
table.add_row("Hint", f"[{DIM}]Run: clutch login[/{DIM}]")
|
|
40
|
+
except httpx.RequestError:
|
|
41
|
+
table.add_row("Token", f"[{SUCCESS}]Saved[/{SUCCESS}]")
|
|
42
|
+
table.add_row("API", f"[{ERROR}]Unreachable[/{ERROR}]")
|
|
43
|
+
|
|
44
|
+
console.print(table)
|
|
45
|
+
footer()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Shared visual theme for all Clutch CLI output.
|
|
2
|
+
|
|
3
|
+
One bold, monochrome accent system — no rainbow, no gradients.
|
|
4
|
+
Primary: bold white. Accent: bold (terminal default bright). Dim: grey.
|
|
5
|
+
Status: green (positive only), red (errors only), yellow (warnings only).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
# Text styles
|
|
13
|
+
PRIMARY = "bold white"
|
|
14
|
+
ACCENT = "bold" # bright/bold default terminal color — reads as black/white bold
|
|
15
|
+
DIM = "dim"
|
|
16
|
+
SUCCESS = "bold green"
|
|
17
|
+
ERROR = "bold red"
|
|
18
|
+
WARNING = "bold yellow"
|
|
19
|
+
|
|
20
|
+
BRAND = "⚡ CLUTCH"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def header(title: str) -> None:
|
|
24
|
+
"""Print the standard Clutch section header."""
|
|
25
|
+
console.print()
|
|
26
|
+
console.rule(f"[{ACCENT}]{BRAND} — {title.upper()}[/{ACCENT}]")
|
|
27
|
+
console.print()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def footer() -> None:
|
|
31
|
+
"""Print the standard Clutch section footer."""
|
|
32
|
+
console.print()
|
|
33
|
+
console.rule(style=DIM)
|
|
34
|
+
console.print()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def bar(value: float, max_value: float, width: int = 28) -> str:
|
|
38
|
+
"""Render a solid/empty block bar, bold white filled + dim empty."""
|
|
39
|
+
max_value = max_value or 1
|
|
40
|
+
filled = int((value / max_value) * width)
|
|
41
|
+
filled = max(0, min(width, filled))
|
|
42
|
+
return f"[{ACCENT}]{'█' * filled}[/{ACCENT}][{DIM}]{'░' * (width - filled)}[/{DIM}]"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clutch-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: GitHub tracks your work. Clutch tracks you.
|
|
5
5
|
Author-email: Lay Patel <lay.patel.1313@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -35,7 +35,7 @@ Requires-Dist: rich>=13.0.0
|
|
|
35
35
|
|
|
36
36
|
---
|
|
37
37
|
|
|
38
|
-
`clutch-cli` is the terminal companion for [Clutch](https://clutch-
|
|
38
|
+
`clutch-cli` is the terminal companion for [Clutch](https://clutch-woad.vercel.app) — an open-source AI-powered developer activity dashboard. Get your GitHub streaks, stats, coding patterns, and AI insights without leaving your terminal.
|
|
39
39
|
|
|
40
40
|
## Installation
|
|
41
41
|
|
|
@@ -47,7 +47,7 @@ pip install clutch-cli
|
|
|
47
47
|
|
|
48
48
|
```bash
|
|
49
49
|
# Login via GitHub (opens browser automatically, no copy-paste needed)
|
|
50
|
-
clutch
|
|
50
|
+
clutch login
|
|
51
51
|
|
|
52
52
|
# Check your streak
|
|
53
53
|
clutch streak
|
|
@@ -63,9 +63,9 @@ clutch insight
|
|
|
63
63
|
|
|
64
64
|
| Command | Description |
|
|
65
65
|
|---|---|
|
|
66
|
-
| `clutch
|
|
67
|
-
| `clutch
|
|
68
|
-
| `clutch
|
|
66
|
+
| `clutch login` | Login via GitHub OAuth (fully automatic) |
|
|
67
|
+
| `clutch logout` | Logout and clear saved credentials |
|
|
68
|
+
| `clutch whoami` | Show currently logged-in user |
|
|
69
69
|
| `clutch streak` | Current and longest commit streak |
|
|
70
70
|
| `clutch stats` | Activity stats — commits, PRs, issues, active days |
|
|
71
71
|
| `clutch stats --days 7` | Stats for a custom time range |
|
|
@@ -77,10 +77,10 @@ clutch insight
|
|
|
77
77
|
|
|
78
78
|
## How Login Works
|
|
79
79
|
|
|
80
|
-
`clutch
|
|
80
|
+
`clutch login` spins up a temporary local server on port `9876`, opens GitHub OAuth in your browser, and automatically captures the token when GitHub redirects back. No copy-pasting required.
|
|
81
81
|
|
|
82
82
|
```
|
|
83
|
-
$ clutch
|
|
83
|
+
$ clutch login
|
|
84
84
|
|
|
85
85
|
⚡ Clutch Login
|
|
86
86
|
Opening GitHub in your browser...
|
|
@@ -98,12 +98,12 @@ By default the CLI talks to the hosted Clutch API. To point it at a local backen
|
|
|
98
98
|
|
|
99
99
|
```bash
|
|
100
100
|
export CLUTCH_API_URL=http://localhost:8000
|
|
101
|
-
clutch
|
|
101
|
+
clutch login
|
|
102
102
|
```
|
|
103
103
|
|
|
104
104
|
## Links
|
|
105
105
|
|
|
106
|
-
- 🌐 [Live Dashboard](https://clutch-
|
|
106
|
+
- 🌐 [Live Dashboard](https://clutch-woad.vercel.app)
|
|
107
107
|
- 📖 [Full Documentation](https://github.com/laypatel13/clutch)
|
|
108
108
|
- 🐛 [Report a Bug](https://github.com/laypatel13/clutch/issues)
|
|
109
109
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
clutch_cli/__init__.py
|
|
4
|
+
clutch_cli/api.py
|
|
5
|
+
clutch_cli/config.py
|
|
6
|
+
clutch_cli/main.py
|
|
7
|
+
clutch_cli/theme.py
|
|
8
|
+
clutch_cli.egg-info/PKG-INFO
|
|
9
|
+
clutch_cli.egg-info/SOURCES.txt
|
|
10
|
+
clutch_cli.egg-info/dependency_links.txt
|
|
11
|
+
clutch_cli.egg-info/entry_points.txt
|
|
12
|
+
clutch_cli.egg-info/requires.txt
|
|
13
|
+
clutch_cli.egg-info/top_level.txt
|
|
14
|
+
clutch_cli/activity/__init__.py
|
|
15
|
+
clutch_cli/activity/patterns.py
|
|
16
|
+
clutch_cli/activity/stats.py
|
|
17
|
+
clutch_cli/activity/streak.py
|
|
18
|
+
clutch_cli/authentication/__init__.py
|
|
19
|
+
clutch_cli/authentication/login.py
|
|
20
|
+
clutch_cli/authentication/logout.py
|
|
21
|
+
clutch_cli/authentication/whoami.py
|
|
22
|
+
clutch_cli/insights/__init__.py
|
|
23
|
+
clutch_cli/insights/weekly.py
|
|
24
|
+
clutch_cli/repositories/__init__.py
|
|
25
|
+
clutch_cli/repositories/list.py
|
|
26
|
+
clutch_cli/system/__init__.py
|
|
27
|
+
clutch_cli/system/status.py
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import webbrowser
|
|
2
|
-
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
3
|
-
from urllib.parse import parse_qs, urlparse
|
|
4
|
-
|
|
5
|
-
import httpx
|
|
6
|
-
import typer
|
|
7
|
-
from rich.console import Console
|
|
8
|
-
|
|
9
|
-
from clutch_cli.config import API_BASE_URL, clear_config, get_username, save_token
|
|
10
|
-
|
|
11
|
-
app = typer.Typer(help="Authentication commands.")
|
|
12
|
-
console = Console()
|
|
13
|
-
|
|
14
|
-
CLI_CALLBACK_PORT = 9876
|
|
15
|
-
_captured_token: dict = {}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class _CallbackHandler(BaseHTTPRequestHandler):
|
|
19
|
-
"""Minimal HTTP handler that captures the JWT from the OAuth redirect."""
|
|
20
|
-
|
|
21
|
-
def do_GET(self):
|
|
22
|
-
parsed = urlparse(self.path)
|
|
23
|
-
if parsed.path == "/callback":
|
|
24
|
-
params = parse_qs(parsed.query)
|
|
25
|
-
token = params.get("token", [None])[0]
|
|
26
|
-
if token:
|
|
27
|
-
_captured_token["value"] = token
|
|
28
|
-
self._respond(200, _success_page())
|
|
29
|
-
else:
|
|
30
|
-
self._respond(400, _error_page("No token received."))
|
|
31
|
-
else:
|
|
32
|
-
self._respond(404, b"Not found")
|
|
33
|
-
|
|
34
|
-
def _respond(self, status: int, body: bytes) -> None:
|
|
35
|
-
self.send_response(status)
|
|
36
|
-
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
37
|
-
self.end_headers()
|
|
38
|
-
self.wfile.write(body)
|
|
39
|
-
|
|
40
|
-
def log_message(self, *args):
|
|
41
|
-
pass # silence default request logging
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def _success_page() -> bytes:
|
|
45
|
-
return b"""<!DOCTYPE html>
|
|
46
|
-
<html>
|
|
47
|
-
<head><title>Clutch Login</title></head>
|
|
48
|
-
<body style="font-family:sans-serif;text-align:center;padding:60px;background:#0d1117;color:#58a6ff">
|
|
49
|
-
<h1>⚡ Clutch</h1>
|
|
50
|
-
<p style="color:#3fb950;font-size:1.2rem">✓ Login successful! You can close this tab.</p>
|
|
51
|
-
</body>
|
|
52
|
-
</html>"""
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def _error_page(msg: str) -> bytes:
|
|
56
|
-
return f"""<!DOCTYPE html>
|
|
57
|
-
<html>
|
|
58
|
-
<head><title>Clutch Login Error</title></head>
|
|
59
|
-
<body style="font-family:sans-serif;text-align:center;padding:60px;background:#0d1117;color:#f85149">
|
|
60
|
-
<h1>⚡ Clutch</h1>
|
|
61
|
-
<p>Login failed: {msg}</p>
|
|
62
|
-
</body>
|
|
63
|
-
</html>""".encode()
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
@app.command()
|
|
67
|
-
def login():
|
|
68
|
-
"""Login to Clutch via GitHub OAuth (browser-based, fully automatic)."""
|
|
69
|
-
console.print("\n[bold green]⚡ Clutch Login[/bold green]")
|
|
70
|
-
console.print("[dim]Starting local callback listener...[/dim]")
|
|
71
|
-
|
|
72
|
-
_captured_token.clear()
|
|
73
|
-
server = HTTPServer(("localhost", CLI_CALLBACK_PORT), _CallbackHandler)
|
|
74
|
-
|
|
75
|
-
login_url = f"{API_BASE_URL}/auth/github?cli=true"
|
|
76
|
-
console.print("[dim]Opening GitHub in your browser...[/dim]\n")
|
|
77
|
-
webbrowser.open(login_url)
|
|
78
|
-
|
|
79
|
-
console.print("[yellow]Waiting for GitHub authorization...[/yellow]")
|
|
80
|
-
console.print("[dim](If your browser didn't open, visit:)[/dim]")
|
|
81
|
-
console.print(f"[dim]{login_url}[/dim]\n")
|
|
82
|
-
|
|
83
|
-
# Block on main thread until the OAuth callback hits localhost:9876/callback
|
|
84
|
-
server.handle_request()
|
|
85
|
-
server.server_close()
|
|
86
|
-
|
|
87
|
-
token = _captured_token.get("value")
|
|
88
|
-
if not token:
|
|
89
|
-
console.print("[red]❌ Login failed — no token received.[/red]")
|
|
90
|
-
raise typer.Exit(1)
|
|
91
|
-
|
|
92
|
-
# Verify token and fetch user info
|
|
93
|
-
try:
|
|
94
|
-
response = httpx.get(
|
|
95
|
-
f"{API_BASE_URL}/users/me",
|
|
96
|
-
headers={"Authorization": f"Bearer {token}"},
|
|
97
|
-
timeout=10,
|
|
98
|
-
)
|
|
99
|
-
if response.status_code != 200:
|
|
100
|
-
console.print("[red]❌ Token validation failed.[/red]")
|
|
101
|
-
raise typer.Exit(1)
|
|
102
|
-
|
|
103
|
-
user = response.json()
|
|
104
|
-
save_token(token, user["username"])
|
|
105
|
-
console.print(f"[bold green]✅ Logged in as @{user['username']}[/bold green]")
|
|
106
|
-
console.print(f"[dim]Welcome to Clutch, {user.get('name') or user['username']}![/dim]\n")
|
|
107
|
-
|
|
108
|
-
except httpx.RequestError:
|
|
109
|
-
console.print("[red]❌ Could not connect to Clutch API.[/red]")
|
|
110
|
-
raise typer.Exit(1)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
@app.command()
|
|
114
|
-
def logout():
|
|
115
|
-
"""Logout from Clutch."""
|
|
116
|
-
username = get_username()
|
|
117
|
-
if not username:
|
|
118
|
-
console.print("[yellow]You are not logged in.[/yellow]")
|
|
119
|
-
raise typer.Exit()
|
|
120
|
-
|
|
121
|
-
clear_config()
|
|
122
|
-
console.print("[bold green]✅ Logged out successfully.[/bold green]")
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
@app.command()
|
|
126
|
-
def whoami():
|
|
127
|
-
"""Show the currently logged-in user."""
|
|
128
|
-
username = get_username()
|
|
129
|
-
if not username:
|
|
130
|
-
console.print("[yellow]Not logged in. Run: clutch auth login[/yellow]")
|
|
131
|
-
raise typer.Exit()
|
|
132
|
-
console.print(f"[bold green]Logged in as @{username}[/bold green]")
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import typer
|
|
2
|
-
from clutch_cli import auth, streak, stats, insight, repos, patterns, status
|
|
3
|
-
|
|
4
|
-
__version__ = "0.2.0"
|
|
5
|
-
|
|
6
|
-
app = typer.Typer(
|
|
7
|
-
name="clutch",
|
|
8
|
-
help="GitHub tracks your work. Clutch tracks you.",
|
|
9
|
-
no_args_is_help=True,
|
|
10
|
-
)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def _version_callback(value: bool):
|
|
14
|
-
if value:
|
|
15
|
-
typer.echo(f"clutch v{__version__}")
|
|
16
|
-
raise typer.Exit()
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
@app.callback()
|
|
20
|
-
def main(
|
|
21
|
-
version: bool = typer.Option(
|
|
22
|
-
None,
|
|
23
|
-
"--version",
|
|
24
|
-
"-v",
|
|
25
|
-
help="Show version and exit.",
|
|
26
|
-
callback=_version_callback,
|
|
27
|
-
is_eager=True,
|
|
28
|
-
),
|
|
29
|
-
):
|
|
30
|
-
pass
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
app.add_typer(auth.app, name="auth")
|
|
34
|
-
app.command()(streak.streak)
|
|
35
|
-
app.command()(stats.stats)
|
|
36
|
-
app.command()(insight.insight)
|
|
37
|
-
app.command()(repos.repos)
|
|
38
|
-
app.command()(patterns.patterns)
|
|
39
|
-
app.command()(status.status)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if __name__ == "__main__":
|
|
43
|
-
app()
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import httpx
|
|
2
|
-
import typer
|
|
3
|
-
from rich.console import Console
|
|
4
|
-
from rich.table import Table
|
|
5
|
-
from rich import box
|
|
6
|
-
from clutch_cli.config import API_BASE_URL, get_token, get_username
|
|
7
|
-
|
|
8
|
-
console = Console()
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def status():
|
|
12
|
-
"""Show login status and API health."""
|
|
13
|
-
username = get_username()
|
|
14
|
-
token = get_token()
|
|
15
|
-
|
|
16
|
-
console.print()
|
|
17
|
-
console.rule("[bold blue]⚡ CLUTCH — STATUS[/bold blue]")
|
|
18
|
-
console.print()
|
|
19
|
-
|
|
20
|
-
table = Table(box=box.SIMPLE, show_header=False, pad_edge=False)
|
|
21
|
-
table.add_column("Check", style="dim", width=18)
|
|
22
|
-
table.add_column("Result", style="bold white")
|
|
23
|
-
|
|
24
|
-
if not username or not token:
|
|
25
|
-
table.add_row("Auth", "[red]Not logged in[/red]")
|
|
26
|
-
table.add_row("Hint", "[dim]Run: clutch auth login[/dim]")
|
|
27
|
-
console.print(table)
|
|
28
|
-
console.print()
|
|
29
|
-
console.rule(style="dim")
|
|
30
|
-
console.print()
|
|
31
|
-
raise typer.Exit()
|
|
32
|
-
|
|
33
|
-
table.add_row("User", f"[blue]@{username}[/blue]")
|
|
34
|
-
|
|
35
|
-
try:
|
|
36
|
-
response = httpx.get(
|
|
37
|
-
f"{API_BASE_URL}/users/me",
|
|
38
|
-
headers={"Authorization": f"Bearer {token}"},
|
|
39
|
-
timeout=8,
|
|
40
|
-
)
|
|
41
|
-
if response.status_code == 200:
|
|
42
|
-
table.add_row("Token", "[green]Valid[/green]")
|
|
43
|
-
table.add_row("API", f"[green]Reachable[/green] [dim]{API_BASE_URL}[/dim]")
|
|
44
|
-
else:
|
|
45
|
-
table.add_row("Token", f"[yellow]Expired ({response.status_code})[/yellow]")
|
|
46
|
-
table.add_row("Hint", "[dim]Run: clutch auth login[/dim]")
|
|
47
|
-
except httpx.RequestError:
|
|
48
|
-
table.add_row("Token", "[green]Saved[/green]")
|
|
49
|
-
table.add_row("API", "[red]Unreachable[/red]")
|
|
50
|
-
|
|
51
|
-
console.print(table)
|
|
52
|
-
console.print()
|
|
53
|
-
console.rule(style="dim")
|
|
54
|
-
console.print()
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import typer
|
|
2
|
-
from rich.console import Console
|
|
3
|
-
from rich.table import Table
|
|
4
|
-
from rich import box
|
|
5
|
-
from clutch_cli.api import get_client
|
|
6
|
-
|
|
7
|
-
console = Console()
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def streak():
|
|
11
|
-
"""Show your current and longest commit streak."""
|
|
12
|
-
with get_client() as client:
|
|
13
|
-
try:
|
|
14
|
-
response = client.get("/github/streak")
|
|
15
|
-
if response.status_code != 200:
|
|
16
|
-
console.print("[red]Error: Failed to fetch streak data.[/red]")
|
|
17
|
-
raise typer.Exit(1)
|
|
18
|
-
|
|
19
|
-
data = response.json()
|
|
20
|
-
current = data["current_streak"]
|
|
21
|
-
longest = data["longest_streak"]
|
|
22
|
-
total_active = data["total_active_days"]
|
|
23
|
-
|
|
24
|
-
if current >= 30:
|
|
25
|
-
streak_color = "bold green"
|
|
26
|
-
status = "ON FIRE"
|
|
27
|
-
elif current >= 14:
|
|
28
|
-
streak_color = "bold green"
|
|
29
|
-
status = "STRONG"
|
|
30
|
-
elif current >= 7:
|
|
31
|
-
streak_color = "bold blue"
|
|
32
|
-
status = "BUILDING"
|
|
33
|
-
elif current > 0:
|
|
34
|
-
streak_color = "bold yellow"
|
|
35
|
-
status = "ACTIVE"
|
|
36
|
-
else:
|
|
37
|
-
streak_color = "dim"
|
|
38
|
-
status = "INACTIVE"
|
|
39
|
-
|
|
40
|
-
console.print()
|
|
41
|
-
console.rule("[bold blue]⚡ CLUTCH — STREAK[/bold blue]")
|
|
42
|
-
console.print()
|
|
43
|
-
|
|
44
|
-
table = Table(box=box.SIMPLE, show_header=False, pad_edge=False)
|
|
45
|
-
table.add_column("Label", style="dim", width=22)
|
|
46
|
-
table.add_column("Value", style="bold white")
|
|
47
|
-
table.add_column("Badge", justify="right")
|
|
48
|
-
|
|
49
|
-
table.add_row(
|
|
50
|
-
"Current Streak",
|
|
51
|
-
f"[{streak_color}]{current} DAY'S[/{streak_color}]",
|
|
52
|
-
f"[blue]{status}[/blue]",
|
|
53
|
-
)
|
|
54
|
-
table.add_row(
|
|
55
|
-
"Longest Streak",
|
|
56
|
-
f"{longest} DAY'S",
|
|
57
|
-
"",
|
|
58
|
-
)
|
|
59
|
-
table.add_row(
|
|
60
|
-
"Total Active Days",
|
|
61
|
-
f"{total_active} DAY'S",
|
|
62
|
-
"",
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
# Visual bar: current vs longest
|
|
66
|
-
if longest > 0:
|
|
67
|
-
filled = int((current / longest) * 30)
|
|
68
|
-
bar = "█" * filled + "░" * (30 - filled)
|
|
69
|
-
console.print(table)
|
|
70
|
-
console.print(f" [dim]Progress to longest[/dim] [blue]{bar}[/blue] [dim]{current}/{longest}[/dim]")
|
|
71
|
-
else:
|
|
72
|
-
console.print(table)
|
|
73
|
-
|
|
74
|
-
console.print()
|
|
75
|
-
console.rule(style="dim")
|
|
76
|
-
console.print()
|
|
77
|
-
|
|
78
|
-
except Exception:
|
|
79
|
-
console.print("[red]Error: Could not connect to Clutch API.[/red]")
|
|
80
|
-
raise typer.Exit(1)
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
README.md
|
|
2
|
-
pyproject.toml
|
|
3
|
-
clutch_cli/__init__.py
|
|
4
|
-
clutch_cli/api.py
|
|
5
|
-
clutch_cli/auth.py
|
|
6
|
-
clutch_cli/config.py
|
|
7
|
-
clutch_cli/insight.py
|
|
8
|
-
clutch_cli/main.py
|
|
9
|
-
clutch_cli/patterns.py
|
|
10
|
-
clutch_cli/repos.py
|
|
11
|
-
clutch_cli/stats.py
|
|
12
|
-
clutch_cli/status.py
|
|
13
|
-
clutch_cli/streak.py
|
|
14
|
-
clutch_cli.egg-info/PKG-INFO
|
|
15
|
-
clutch_cli.egg-info/SOURCES.txt
|
|
16
|
-
clutch_cli.egg-info/dependency_links.txt
|
|
17
|
-
clutch_cli.egg-info/entry_points.txt
|
|
18
|
-
clutch_cli.egg-info/requires.txt
|
|
19
|
-
clutch_cli.egg-info/top_level.txt
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|