get2work 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.
- get2work-0.1.0/.gitignore +7 -0
- get2work-0.1.0/PKG-INFO +118 -0
- get2work-0.1.0/README.md +102 -0
- get2work-0.1.0/get2work/__init__.py +1 -0
- get2work-0.1.0/get2work/celebrate.py +114 -0
- get2work-0.1.0/get2work/cli.py +227 -0
- get2work-0.1.0/get2work/distraction.py +74 -0
- get2work-0.1.0/get2work/levels.py +37 -0
- get2work-0.1.0/get2work/peer_pressure.py +123 -0
- get2work-0.1.0/get2work/roast.py +181 -0
- get2work-0.1.0/get2work/storage.py +38 -0
- get2work-0.1.0/get2work/watcher.py +172 -0
- get2work-0.1.0/pyproject.toml +27 -0
- get2work-0.1.0/sounds/celebrate.mp3 +0 -0
- get2work-0.1.0/sounds/error.mp3 +0 -0
- get2work-0.1.0/sounds/funeral.mp3 +0 -0
- get2work-0.1.0/sounds/levelup.mp3 +0 -0
- get2work-0.1.0/sounds/shame.mp3 +0 -0
- get2work-0.1.0/sounds/success.mp3 +0 -0
get2work-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: get2work
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The Python library that makes you actually work
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Requires-Dist: anthropic>=0.20.0
|
|
7
|
+
Requires-Dist: apscheduler>=3.10.0
|
|
8
|
+
Requires-Dist: gitpython>=3.1.0
|
|
9
|
+
Requires-Dist: psutil>=5.9.0
|
|
10
|
+
Requires-Dist: pygame>=2.5.0
|
|
11
|
+
Requires-Dist: requests>=2.31.0
|
|
12
|
+
Requires-Dist: rich>=13.0.0
|
|
13
|
+
Requires-Dist: typer>=0.9.0
|
|
14
|
+
Requires-Dist: watchdog>=3.0.0
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# get2work
|
|
18
|
+
|
|
19
|
+
> The Python library that makes you actually work.
|
|
20
|
+
|
|
21
|
+
[](https://badge.fury.io/py/get2work)
|
|
22
|
+
[](https://www.python.org/downloads/)
|
|
23
|
+
|
|
24
|
+
get2work is a CLI tool that gamifies your coding productivity. It celebrates your commits, roasts your git history, shames you when you're slacking, and makes you actually ship things.
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
|
|
28
|
+
- **Commit celebrations** — animations and sounds every time you commit
|
|
29
|
+
- **Level system** — 8 levels from "Hello World Survivor" to "Linus Would Be Proud"
|
|
30
|
+
- **Streak tracking** — daily commit streaks
|
|
31
|
+
- **Shame notifications** — get roasted if you haven't committed today
|
|
32
|
+
- **AI roasts** — your commit history analyzed and destroyed by AI
|
|
33
|
+
- **Vibe check** — AI mental health diagnosis based on your commits
|
|
34
|
+
- **Peer pressure** — see what other devs are shipping and feel bad
|
|
35
|
+
- **Pomodoro timer** — focus sessions that count toward your level
|
|
36
|
+
- **Accountability receipt** — daily summary of what you actually did
|
|
37
|
+
- **Code funeral** — deleting 100+ lines triggers a funeral
|
|
38
|
+
- **Git blame but personal** — AI roasts each commit individually
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install get2work
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Quick Start
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Install the git hook in your repo (do this once per project)
|
|
50
|
+
get2work install
|
|
51
|
+
|
|
52
|
+
# Check your level and stats
|
|
53
|
+
get2work status
|
|
54
|
+
|
|
55
|
+
# Now just commit normally and watch the magic happen
|
|
56
|
+
git commit -m "feat: add something cool"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Commands
|
|
60
|
+
|
|
61
|
+
| Command | Description |
|
|
62
|
+
|---|---|
|
|
63
|
+
| `get2work install` | Install git hook in current repo |
|
|
64
|
+
| `get2work status` | Your level, streak, and stats |
|
|
65
|
+
| `get2work roast` | Get your commits roasted by AI |
|
|
66
|
+
| `get2work vibe` | AI mental health diagnosis |
|
|
67
|
+
| `get2work blame` | Git blame but make it personal |
|
|
68
|
+
| `get2work peer <username>` | Stalk a dev on GitHub |
|
|
69
|
+
| `get2work randompeer` | Random dev from the internet |
|
|
70
|
+
| `get2work pomodoro` | Start a pomodoro timer |
|
|
71
|
+
| `get2work shame` | Check if you deserve to be shamed |
|
|
72
|
+
| `get2work receipt` | Your accountability receipt |
|
|
73
|
+
| `get2work start` | Start background watcher |
|
|
74
|
+
|
|
75
|
+
## AI Features (Optional)
|
|
76
|
+
|
|
77
|
+
The `roast`, `vibe`, and `blame` commands use AI for better results. Without an API key they fall back to hardcoded funny responses.
|
|
78
|
+
|
|
79
|
+
To enable AI roasts set your OpenAI API key:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
export OPENAI_API_KEY="sk-..."
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Custom Sounds
|
|
86
|
+
|
|
87
|
+
Replace default sounds with your own by setting environment variables:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
export GET2WORK_SOUND_CELEBRATE="/path/to/your/sound.mp3"
|
|
91
|
+
export GET2WORK_SOUND_FUNERAL="/path/to/funeral.mp3"
|
|
92
|
+
export GET2WORK_SOUND_SHAME="/path/to/shame.mp3"
|
|
93
|
+
export GET2WORK_SOUND_LEVELUP="/path/to/levelup.mp3"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Level System
|
|
97
|
+
|
|
98
|
+
| Level | Name | Commits needed |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| 1 | Hello World Survivor | 0 |
|
|
101
|
+
| 2 | Tutorial Finisher | 10 |
|
|
102
|
+
| 3 | Functional but Confused | 30 |
|
|
103
|
+
| 4 | It Works Don't Touch It | 60 |
|
|
104
|
+
| 5 | Commits with Confidence (wrongly) | 100 |
|
|
105
|
+
| 6 | git push --force and it worked | 200 |
|
|
106
|
+
| 7 | The Last Line of Defense | 400 |
|
|
107
|
+
| 8 | Linus Would Be Proud | 700 |
|
|
108
|
+
|
|
109
|
+
## Tutorial
|
|
110
|
+
|
|
111
|
+
[](COLAB_LINK_HERE)
|
|
112
|
+
|
|
113
|
+
## Docker
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
docker build -t get2work .
|
|
117
|
+
docker run get2work
|
|
118
|
+
```
|
get2work-0.1.0/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# get2work
|
|
2
|
+
|
|
3
|
+
> The Python library that makes you actually work.
|
|
4
|
+
|
|
5
|
+
[](https://badge.fury.io/py/get2work)
|
|
6
|
+
[](https://www.python.org/downloads/)
|
|
7
|
+
|
|
8
|
+
get2work is a CLI tool that gamifies your coding productivity. It celebrates your commits, roasts your git history, shames you when you're slacking, and makes you actually ship things.
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- **Commit celebrations** — animations and sounds every time you commit
|
|
13
|
+
- **Level system** — 8 levels from "Hello World Survivor" to "Linus Would Be Proud"
|
|
14
|
+
- **Streak tracking** — daily commit streaks
|
|
15
|
+
- **Shame notifications** — get roasted if you haven't committed today
|
|
16
|
+
- **AI roasts** — your commit history analyzed and destroyed by AI
|
|
17
|
+
- **Vibe check** — AI mental health diagnosis based on your commits
|
|
18
|
+
- **Peer pressure** — see what other devs are shipping and feel bad
|
|
19
|
+
- **Pomodoro timer** — focus sessions that count toward your level
|
|
20
|
+
- **Accountability receipt** — daily summary of what you actually did
|
|
21
|
+
- **Code funeral** — deleting 100+ lines triggers a funeral
|
|
22
|
+
- **Git blame but personal** — AI roasts each commit individually
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install get2work
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Install the git hook in your repo (do this once per project)
|
|
34
|
+
get2work install
|
|
35
|
+
|
|
36
|
+
# Check your level and stats
|
|
37
|
+
get2work status
|
|
38
|
+
|
|
39
|
+
# Now just commit normally and watch the magic happen
|
|
40
|
+
git commit -m "feat: add something cool"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Commands
|
|
44
|
+
|
|
45
|
+
| Command | Description |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `get2work install` | Install git hook in current repo |
|
|
48
|
+
| `get2work status` | Your level, streak, and stats |
|
|
49
|
+
| `get2work roast` | Get your commits roasted by AI |
|
|
50
|
+
| `get2work vibe` | AI mental health diagnosis |
|
|
51
|
+
| `get2work blame` | Git blame but make it personal |
|
|
52
|
+
| `get2work peer <username>` | Stalk a dev on GitHub |
|
|
53
|
+
| `get2work randompeer` | Random dev from the internet |
|
|
54
|
+
| `get2work pomodoro` | Start a pomodoro timer |
|
|
55
|
+
| `get2work shame` | Check if you deserve to be shamed |
|
|
56
|
+
| `get2work receipt` | Your accountability receipt |
|
|
57
|
+
| `get2work start` | Start background watcher |
|
|
58
|
+
|
|
59
|
+
## AI Features (Optional)
|
|
60
|
+
|
|
61
|
+
The `roast`, `vibe`, and `blame` commands use AI for better results. Without an API key they fall back to hardcoded funny responses.
|
|
62
|
+
|
|
63
|
+
To enable AI roasts set your OpenAI API key:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
export OPENAI_API_KEY="sk-..."
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Custom Sounds
|
|
70
|
+
|
|
71
|
+
Replace default sounds with your own by setting environment variables:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
export GET2WORK_SOUND_CELEBRATE="/path/to/your/sound.mp3"
|
|
75
|
+
export GET2WORK_SOUND_FUNERAL="/path/to/funeral.mp3"
|
|
76
|
+
export GET2WORK_SOUND_SHAME="/path/to/shame.mp3"
|
|
77
|
+
export GET2WORK_SOUND_LEVELUP="/path/to/levelup.mp3"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Level System
|
|
81
|
+
|
|
82
|
+
| Level | Name | Commits needed |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| 1 | Hello World Survivor | 0 |
|
|
85
|
+
| 2 | Tutorial Finisher | 10 |
|
|
86
|
+
| 3 | Functional but Confused | 30 |
|
|
87
|
+
| 4 | It Works Don't Touch It | 60 |
|
|
88
|
+
| 5 | Commits with Confidence (wrongly) | 100 |
|
|
89
|
+
| 6 | git push --force and it worked | 200 |
|
|
90
|
+
| 7 | The Last Line of Defense | 400 |
|
|
91
|
+
| 8 | Linus Would Be Proud | 700 |
|
|
92
|
+
|
|
93
|
+
## Tutorial
|
|
94
|
+
|
|
95
|
+
[](COLAB_LINK_HERE)
|
|
96
|
+
|
|
97
|
+
## Docker
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
docker build -t get2work .
|
|
101
|
+
docker run get2work
|
|
102
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from get2work import cli
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import random
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
|
|
7
|
+
console = Console()
|
|
8
|
+
|
|
9
|
+
SOUNDS_DIR = Path(__file__).parent.parent / "sounds"
|
|
10
|
+
|
|
11
|
+
CUSTOM_SOUNDS = {
|
|
12
|
+
"celebrate": os.environ.get("GET2WORK_SOUND_CELEBRATE"),
|
|
13
|
+
"funeral": os.environ.get("GET2WORK_SOUND_FUNERAL"),
|
|
14
|
+
"shame": os.environ.get("GET2WORK_SOUND_SHAME"),
|
|
15
|
+
"levelup": os.environ.get("GET2WORK_SOUND_LEVELUP"),
|
|
16
|
+
"success": os.environ.get("GET2WORK_SOUND_SUCCESS"),
|
|
17
|
+
"error": os.environ.get("GET2WORK_SOUND_ERROR"),
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
DEFAULT_SOUNDS = {
|
|
21
|
+
"celebrate": SOUNDS_DIR / "celebrate.mp3",
|
|
22
|
+
"funeral": SOUNDS_DIR / "funeral.mp3",
|
|
23
|
+
"shame": SOUNDS_DIR / "shame.mp3",
|
|
24
|
+
"levelup": SOUNDS_DIR / "levelup.mp3",
|
|
25
|
+
"success": SOUNDS_DIR / "success.mp3",
|
|
26
|
+
"error": SOUNDS_DIR / "error.mp3",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
COMMIT_MESSAGES = [
|
|
30
|
+
"YOOOO YOU ACTUALLY COMMITTED 🔥",
|
|
31
|
+
"look at you being productive omg",
|
|
32
|
+
"a commit?? in THIS economy??",
|
|
33
|
+
"slay bestie, that's a commit",
|
|
34
|
+
"the git history thanks you",
|
|
35
|
+
"ok ok ok we're doing this 💪",
|
|
36
|
+
"committed and not quitting, respect",
|
|
37
|
+
"your future self says thanks... maybe",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
LEVELUP_MESSAGES = [
|
|
41
|
+
"YOU LEVELED UP ARE YOU KIDDING ME",
|
|
42
|
+
"bro is actually getting good at this",
|
|
43
|
+
"the grindset is REAL",
|
|
44
|
+
"level up achieved, touch grass later",
|
|
45
|
+
"ok we are NOT the same anymore 👑",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
FUNERAL_MESSAGES = [
|
|
49
|
+
"rip to those lines of code 💀",
|
|
50
|
+
"they didn't make it...",
|
|
51
|
+
"gone but not forgotten (they were probably bad anyway)",
|
|
52
|
+
"a moment of silence for your deleted code",
|
|
53
|
+
"the codebase is lighter now. emotionally too.",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
def play_sound(name: str):
|
|
57
|
+
try:
|
|
58
|
+
import pygame
|
|
59
|
+
custom = CUSTOM_SOUNDS.get(name)
|
|
60
|
+
path = Path(custom) if custom else DEFAULT_SOUNDS.get(name)
|
|
61
|
+
|
|
62
|
+
if not path or not path.exists():
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
pygame.mixer.init()
|
|
66
|
+
pygame.mixer.music.load(str(path))
|
|
67
|
+
pygame.mixer.music.play()
|
|
68
|
+
time.sleep(1.5)
|
|
69
|
+
pygame.mixer.quit()
|
|
70
|
+
except Exception:
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
def celebrate_commit():
|
|
74
|
+
colors = ["red", "yellow", "green", "cyan", "magenta", "blue"]
|
|
75
|
+
msg = random.choice(COMMIT_MESSAGES)
|
|
76
|
+
|
|
77
|
+
play_sound("celebrate")
|
|
78
|
+
console.print()
|
|
79
|
+
for i in range(3):
|
|
80
|
+
color = random.choice(colors)
|
|
81
|
+
console.print(f" {'🎉' * (i+2)} {msg} {'🎉' * (i+2)}", style=f"bold {color}")
|
|
82
|
+
time.sleep(0.15)
|
|
83
|
+
console.print()
|
|
84
|
+
|
|
85
|
+
def celebrate_levelup(level_name: str):
|
|
86
|
+
msg = random.choice(LEVELUP_MESSAGES)
|
|
87
|
+
|
|
88
|
+
play_sound("levelup")
|
|
89
|
+
console.print()
|
|
90
|
+
console.rule("[bold yellow]LEVEL UP[/bold yellow]")
|
|
91
|
+
console.print(f"\n ⚡ [bold yellow]{msg}[/bold yellow]")
|
|
92
|
+
console.print(f" 👑 [bold cyan]You are now: {level_name}[/bold cyan]\n")
|
|
93
|
+
console.rule("[bold yellow]LEVEL UP[/bold yellow]")
|
|
94
|
+
console.print()
|
|
95
|
+
|
|
96
|
+
def code_funeral():
|
|
97
|
+
msg = random.choice(FUNERAL_MESSAGES)
|
|
98
|
+
|
|
99
|
+
play_sound("funeral")
|
|
100
|
+
console.print()
|
|
101
|
+
console.rule("[bold red]💀 CODE FUNERAL 💀[/bold red]")
|
|
102
|
+
console.print(f"\n [bold red]{msg}[/bold red]")
|
|
103
|
+
console.print(" [dim]deleted: yes. missed: debatable.[/dim]\n")
|
|
104
|
+
console.rule("[bold red]💀 💀 💀[/bold red]")
|
|
105
|
+
console.print()
|
|
106
|
+
|
|
107
|
+
def play_success():
|
|
108
|
+
play_sound("success")
|
|
109
|
+
|
|
110
|
+
def play_error():
|
|
111
|
+
play_sound("error")
|
|
112
|
+
|
|
113
|
+
def play_shame():
|
|
114
|
+
play_sound("shame")
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from datetime import date
|
|
4
|
+
|
|
5
|
+
app = typer.Typer(help="get2work — stop scrolling, start coding")
|
|
6
|
+
console = Console()
|
|
7
|
+
|
|
8
|
+
@app.command()
|
|
9
|
+
def install():
|
|
10
|
+
"""Install get2work hook in your repo 🔧"""
|
|
11
|
+
import os
|
|
12
|
+
import stat
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
import git
|
|
17
|
+
repo = git.Repo(".", search_parent_directories=True)
|
|
18
|
+
hooks_dir = Path(repo.git_dir) / "hooks"
|
|
19
|
+
hook_path = hooks_dir / "post-commit"
|
|
20
|
+
|
|
21
|
+
hook_script = """#!/bin/sh
|
|
22
|
+
get2work celebrate-commit
|
|
23
|
+
"""
|
|
24
|
+
with open(hook_path, "w") as f:
|
|
25
|
+
f.write(hook_script)
|
|
26
|
+
|
|
27
|
+
st = os.stat(hook_path)
|
|
28
|
+
os.chmod(hook_path, st.st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
|
|
29
|
+
|
|
30
|
+
console.print("\n [bold green]✅ get2work installed![/bold green]")
|
|
31
|
+
console.print(" [dim]now every commit will be celebrated 🎉[/dim]\n")
|
|
32
|
+
|
|
33
|
+
except Exception as e:
|
|
34
|
+
console.print(f"\n [bold red]❌ error: {e}[/bold red]\n")
|
|
35
|
+
|
|
36
|
+
@app.command()
|
|
37
|
+
def celebrate_commit():
|
|
38
|
+
"""Triggered automatically on commit 🎉"""
|
|
39
|
+
from get2work.storage import load, increment
|
|
40
|
+
from get2work.levels import check_levelup
|
|
41
|
+
from get2work.celebrate import celebrate_commit as do_celebrate, celebrate_levelup, code_funeral
|
|
42
|
+
import git
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
repo = git.Repo(".", search_parent_directories=True)
|
|
46
|
+
commits = list(repo.iter_commits(max_count=1))
|
|
47
|
+
if commits:
|
|
48
|
+
commit = commits[0]
|
|
49
|
+
try:
|
|
50
|
+
stats = commit.stats.total
|
|
51
|
+
lines_added = stats.get("insertions", 0)
|
|
52
|
+
lines_deleted = stats.get("deletions", 0)
|
|
53
|
+
except Exception:
|
|
54
|
+
lines_added = 0
|
|
55
|
+
lines_deleted = 0
|
|
56
|
+
|
|
57
|
+
increment("commits")
|
|
58
|
+
increment("total_lines_added", lines_added)
|
|
59
|
+
increment("total_lines_deleted", lines_deleted)
|
|
60
|
+
|
|
61
|
+
from get2work.watcher import update_streak
|
|
62
|
+
update_streak()
|
|
63
|
+
|
|
64
|
+
do_celebrate()
|
|
65
|
+
|
|
66
|
+
if lines_deleted > 100:
|
|
67
|
+
code_funeral()
|
|
68
|
+
|
|
69
|
+
leveled_up, level_name = check_levelup()
|
|
70
|
+
if leveled_up:
|
|
71
|
+
celebrate_levelup(level_name)
|
|
72
|
+
|
|
73
|
+
except Exception as e:
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
@app.command()
|
|
77
|
+
def status():
|
|
78
|
+
"""Check your level, streak and stats"""
|
|
79
|
+
from get2work.storage import load
|
|
80
|
+
from get2work.levels import get_level, get_next_level
|
|
81
|
+
|
|
82
|
+
data = load()
|
|
83
|
+
commits = data["commits"]
|
|
84
|
+
streak = data["streak"]
|
|
85
|
+
level_num, level_name = get_level(commits)
|
|
86
|
+
next_level = get_next_level(commits)
|
|
87
|
+
|
|
88
|
+
console.print()
|
|
89
|
+
console.rule("[bold cyan]get2work[/bold cyan]")
|
|
90
|
+
console.print(f"\n 👾 [bold cyan]Level {level_num}[/bold cyan] — [yellow]{level_name}[/yellow]")
|
|
91
|
+
console.print(f" 🔥 [bold]Streak:[/bold] {streak} days")
|
|
92
|
+
console.print(f" 📦 [bold]Total commits:[/bold] {commits}")
|
|
93
|
+
console.print(f" 🍅 [bold]Pomodoros:[/bold] {data['pomodoros_completed']}")
|
|
94
|
+
console.print(f" 👀 [bold]Distractions caught:[/bold] {data['distractions_caught']}")
|
|
95
|
+
|
|
96
|
+
if next_level:
|
|
97
|
+
needed = next_level[0] - commits
|
|
98
|
+
console.print(f"\n ⚡ [dim]{needed} commits until [bold]{next_level[1]}[/bold][/dim]")
|
|
99
|
+
|
|
100
|
+
console.print()
|
|
101
|
+
|
|
102
|
+
@app.command()
|
|
103
|
+
def roast():
|
|
104
|
+
"""Get your commits roasted 🔥"""
|
|
105
|
+
from get2work.roast import roast_my_commits
|
|
106
|
+
roast_my_commits()
|
|
107
|
+
|
|
108
|
+
@app.command()
|
|
109
|
+
def blame():
|
|
110
|
+
"""Git blame but make it personal 🔍"""
|
|
111
|
+
from get2work.roast import git_blame_personal
|
|
112
|
+
git_blame_personal()
|
|
113
|
+
|
|
114
|
+
@app.command()
|
|
115
|
+
def vibe():
|
|
116
|
+
"""Get a mental health diagnosis based on your commits 🧠"""
|
|
117
|
+
from get2work.roast import vibe_check
|
|
118
|
+
vibe_check()
|
|
119
|
+
|
|
120
|
+
@app.command()
|
|
121
|
+
def peer(username: str = typer.Argument(..., help="GitHub username to stalk 👀")):
|
|
122
|
+
"""See what others are committing and feel bad about yourself 👀"""
|
|
123
|
+
from get2work.peer_pressure import peer_pressure
|
|
124
|
+
peer_pressure(username)
|
|
125
|
+
|
|
126
|
+
@app.command()
|
|
127
|
+
def randompeer():
|
|
128
|
+
"""Get shamed by a random dev from the internet 🎲"""
|
|
129
|
+
from get2work.peer_pressure import random_peer
|
|
130
|
+
random_peer()
|
|
131
|
+
|
|
132
|
+
@app.command()
|
|
133
|
+
def distraction():
|
|
134
|
+
"""Check if you're slacking right now 👀"""
|
|
135
|
+
from get2work.distraction import distraction_check
|
|
136
|
+
caught = distraction_check()
|
|
137
|
+
if not caught:
|
|
138
|
+
console.print("\n [bold green]✅ no distractions detected[/bold green]")
|
|
139
|
+
console.print(" [dim]surprisingly productive. respect.[/dim]\n")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@app.command()
|
|
143
|
+
def shame():
|
|
144
|
+
"""Check if you deserve to be shamed right now 😤"""
|
|
145
|
+
from get2work.watcher import CommitWatcher
|
|
146
|
+
watcher = CommitWatcher()
|
|
147
|
+
watcher.check_shame()
|
|
148
|
+
|
|
149
|
+
@app.command()
|
|
150
|
+
def start():
|
|
151
|
+
"""Start get2work in background — watches commits and distractions 👀"""
|
|
152
|
+
import threading
|
|
153
|
+
import time
|
|
154
|
+
from get2work.watcher import CommitWatcher
|
|
155
|
+
from get2work.distraction import start_distraction_watcher
|
|
156
|
+
|
|
157
|
+
console.print("\n [bold green]get2work is watching you 👀[/bold green]")
|
|
158
|
+
console.print(" [dim]commit something or face the consequences[/dim]\n")
|
|
159
|
+
|
|
160
|
+
watcher = CommitWatcher()
|
|
161
|
+
|
|
162
|
+
def shame_loop():
|
|
163
|
+
while True:
|
|
164
|
+
time.sleep(60 * 30)
|
|
165
|
+
watcher.check_shame()
|
|
166
|
+
|
|
167
|
+
def distraction_loop():
|
|
168
|
+
start_distraction_watcher(interval_seconds=120)
|
|
169
|
+
|
|
170
|
+
t1 = threading.Thread(target=shame_loop, daemon=True)
|
|
171
|
+
t2 = threading.Thread(target=distraction_loop, daemon=True)
|
|
172
|
+
t1.start()
|
|
173
|
+
t2.start()
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
while True:
|
|
177
|
+
time.sleep(1)
|
|
178
|
+
except KeyboardInterrupt:
|
|
179
|
+
console.print("\n [dim]get2work stopped. go touch grass.[/dim]\n")
|
|
180
|
+
|
|
181
|
+
@app.command()
|
|
182
|
+
def pomodoro(minutes: int = typer.Option(25, help="Pomodoro duration in minutes")):
|
|
183
|
+
"""Start a pomodoro timer 🍅"""
|
|
184
|
+
import time
|
|
185
|
+
from get2work.storage import increment
|
|
186
|
+
|
|
187
|
+
console.print(f"\n 🍅 [bold green]Pomodoro started — {minutes} minutes[/bold green]")
|
|
188
|
+
console.print(" [dim]focus mode activated, no excuses[/dim]\n")
|
|
189
|
+
|
|
190
|
+
total_seconds = minutes * 60
|
|
191
|
+
with console.status("[green]working...[/green]") as status:
|
|
192
|
+
for remaining in range(total_seconds, 0, -1):
|
|
193
|
+
mins, secs = divmod(remaining, 60)
|
|
194
|
+
status.update(f"[green]⏱ {mins:02d}:{secs:02d} remaining — stay focused[/green]")
|
|
195
|
+
time.sleep(1)
|
|
196
|
+
|
|
197
|
+
increment("pomodoros_completed")
|
|
198
|
+
console.print("\n ✅ [bold green]POMODORO DONE![/bold green]")
|
|
199
|
+
console.print(" [dim]ok now you can touch grass for 5 minutes[/dim]\n")
|
|
200
|
+
|
|
201
|
+
@app.command()
|
|
202
|
+
def receipt():
|
|
203
|
+
"""Get your accountability receipt for today 🧾"""
|
|
204
|
+
from get2work.storage import load
|
|
205
|
+
from get2work.levels import get_level
|
|
206
|
+
import datetime
|
|
207
|
+
|
|
208
|
+
data = load()
|
|
209
|
+
now = datetime.datetime.now()
|
|
210
|
+
level_num, level_name = get_level(data["commits"])
|
|
211
|
+
|
|
212
|
+
console.print()
|
|
213
|
+
console.rule("[bold white]🧾 ACCOUNTABILITY RECEIPT[/bold white]")
|
|
214
|
+
console.print(f" [dim]{now.strftime('%Y-%m-%d %H:%M')}[/dim]\n")
|
|
215
|
+
console.print(f" Level: {level_name}")
|
|
216
|
+
console.print(f" Commits: {data['commits']}")
|
|
217
|
+
console.print(f" Streak: {data['streak']} days")
|
|
218
|
+
console.print(f" Pomodoros: {data['pomodoros_completed']}")
|
|
219
|
+
console.print(f" Distractions: {data['distractions_caught']}")
|
|
220
|
+
console.print(f"\n [dim]Lines added: {data['total_lines_added']}[/dim]")
|
|
221
|
+
console.print(f" [dim]Lines deleted: {data['total_lines_deleted']}[/dim]")
|
|
222
|
+
console.print()
|
|
223
|
+
console.rule("[bold white]thanks for coming to get2work[/bold white]")
|
|
224
|
+
console.print()
|
|
225
|
+
|
|
226
|
+
if __name__ == "__main__":
|
|
227
|
+
app()
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import psutil
|
|
2
|
+
import random
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from get2work.storage import increment
|
|
5
|
+
from get2work.celebrate import play_shame
|
|
6
|
+
|
|
7
|
+
console = Console()
|
|
8
|
+
|
|
9
|
+
DISTRACTING_APPS = [
|
|
10
|
+
"netflix", "spotify", "discord", "telegram",
|
|
11
|
+
"whatsapp", "instagram", "tiktok", "youtube",
|
|
12
|
+
"twitch", "steam", "epicgames", "roblox",
|
|
13
|
+
"minecraft", "fortnite", "chess", "solitaire",
|
|
14
|
+
"slack", "zoom", "teams",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
DISTRACTING_URLS_KEYWORDS = [
|
|
18
|
+
"youtube", "netflix", "twitter", "instagram",
|
|
19
|
+
"tiktok", "reddit", "facebook", "twitch",
|
|
20
|
+
"memes", "buzzfeed", "9gag",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
CAUGHT_MESSAGES = [
|
|
24
|
+
"bro really said 'just 5 minutes' 💀",
|
|
25
|
+
"the audacity. the AUDACITY.",
|
|
26
|
+
"we saw that. get back to work.",
|
|
27
|
+
"not you watching {app} instead of coding",
|
|
28
|
+
"your commits are not going to write themselves",
|
|
29
|
+
"caught in 4k. close it.",
|
|
30
|
+
"{app}?? really?? RIGHT NOW??",
|
|
31
|
+
"the repo is crying. {app} can wait.",
|
|
32
|
+
"ok so we're doing THIS instead of shipping features",
|
|
33
|
+
"distraction detected. shame incoming. 🚨",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
def get_running_processes() -> list:
|
|
37
|
+
processes = []
|
|
38
|
+
try:
|
|
39
|
+
for proc in psutil.process_iter(['name']):
|
|
40
|
+
try:
|
|
41
|
+
name = proc.info['name'].lower()
|
|
42
|
+
processes.append(name)
|
|
43
|
+
except Exception:
|
|
44
|
+
pass
|
|
45
|
+
except Exception:
|
|
46
|
+
pass
|
|
47
|
+
return processes
|
|
48
|
+
|
|
49
|
+
def check_distractions() -> str | None:
|
|
50
|
+
processes = get_running_processes()
|
|
51
|
+
for proc in processes:
|
|
52
|
+
for app in DISTRACTING_APPS:
|
|
53
|
+
if app in proc:
|
|
54
|
+
return app
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
def distraction_check():
|
|
58
|
+
caught = check_distractions()
|
|
59
|
+
if caught:
|
|
60
|
+
increment("distractions_caught")
|
|
61
|
+
play_shame()
|
|
62
|
+
|
|
63
|
+
msg = random.choice(CAUGHT_MESSAGES).replace("{app}", caught)
|
|
64
|
+
console.print(f"\n [bold red]🚨 DISTRACTION DETECTED[/bold red]")
|
|
65
|
+
console.print(f" [red]{msg}[/red]\n")
|
|
66
|
+
return True
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
def start_distraction_watcher(interval_seconds: int = 120):
|
|
70
|
+
import time
|
|
71
|
+
console.print(" [dim]distraction detector active 👀[/dim]")
|
|
72
|
+
while True:
|
|
73
|
+
distraction_check()
|
|
74
|
+
time.sleep(interval_seconds)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from get2work.storage import load, save
|
|
2
|
+
|
|
3
|
+
LEVELS = [
|
|
4
|
+
(0, "Hello World Survivor"),
|
|
5
|
+
(10, "Tutorial Finisher"),
|
|
6
|
+
(30, "Functional but Confused"),
|
|
7
|
+
(60, "It Works Don't Touch It"),
|
|
8
|
+
(100, "Commits with Confidence (wrongly)"),
|
|
9
|
+
(200, "git push --force and it worked"),
|
|
10
|
+
(400, "The Last Line of Defense"),
|
|
11
|
+
(700, "Linus Would Be Proud"),
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
def get_level(commits: int) -> tuple:
|
|
15
|
+
current = (1, LEVELS[0][1])
|
|
16
|
+
for i, (threshold, name) in enumerate(LEVELS):
|
|
17
|
+
if commits >= threshold:
|
|
18
|
+
current = (i + 1, name)
|
|
19
|
+
return current
|
|
20
|
+
|
|
21
|
+
def get_next_level(commits: int) -> tuple | None:
|
|
22
|
+
for threshold, name in LEVELS:
|
|
23
|
+
if commits < threshold:
|
|
24
|
+
return (threshold, name)
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
def check_levelup() -> tuple:
|
|
28
|
+
data = load()
|
|
29
|
+
commits = data["commits"]
|
|
30
|
+
level_num, level_name = get_level(commits)
|
|
31
|
+
old_level = data.get("level", 1)
|
|
32
|
+
|
|
33
|
+
if level_num > old_level:
|
|
34
|
+
data["level"] = level_num
|
|
35
|
+
save(data)
|
|
36
|
+
return True, level_name
|
|
37
|
+
return False, level_name
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import random
|
|
2
|
+
import requests
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
|
|
6
|
+
console = Console()
|
|
7
|
+
|
|
8
|
+
SARCASTIC_COMMENTS = [
|
|
9
|
+
"and what are YOU doing with your life?",
|
|
10
|
+
"must be nice to actually commit things 👀",
|
|
11
|
+
"imagine being this productive. couldn't be you huh",
|
|
12
|
+
"they said 'i'll do it later' and then did it NOW",
|
|
13
|
+
"not you watching others work while you're here",
|
|
14
|
+
"the dedication... the consistency... the shame you should feel",
|
|
15
|
+
"ok but why are they built different tho",
|
|
16
|
+
"they woke up and chose violence (against procrastination)",
|
|
17
|
+
"this person has never heard of 'i'll do it tomorrow'",
|
|
18
|
+
"ratio'd by someone's commit history 💀",
|
|
19
|
+
"they're just like you but... productive",
|
|
20
|
+
"bro said 'ship it' and actually shipped it",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
NO_COMMITS_COMMENTS = [
|
|
24
|
+
"even THEY took a day off. you have no excuse though.",
|
|
25
|
+
"ok they're resting. you should be coding.",
|
|
26
|
+
"slow day for them. not an excuse for you.",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
RANDOM_USERNAMES = [
|
|
30
|
+
"antfu", "patak-dev", "sxzz",
|
|
31
|
+
"privatenumber", "jantimon", "nzakas",
|
|
32
|
+
"ljharb", "nicolo-ribaudo", "sheremet-va",
|
|
33
|
+
"yyx990803", "egoist", "pi0",
|
|
34
|
+
"unjs", "Rich-Harris", "sveltejs",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
def get_recent_commits(username: str) -> list:
|
|
38
|
+
try:
|
|
39
|
+
url = f"https://api.github.com/users/{username}/events/public"
|
|
40
|
+
response = requests.get(url, timeout=3)
|
|
41
|
+
if response.status_code != 200:
|
|
42
|
+
return []
|
|
43
|
+
|
|
44
|
+
events = response.json()
|
|
45
|
+
commits = []
|
|
46
|
+
for event in events:
|
|
47
|
+
if event.get("type") == "PushEvent":
|
|
48
|
+
payload = event.get("payload", {})
|
|
49
|
+
repo = event.get("repo", {}).get("name", "unknown")
|
|
50
|
+
event_commits = payload.get("commits", [])
|
|
51
|
+
if event_commits:
|
|
52
|
+
for commit in event_commits:
|
|
53
|
+
msg = commit.get("message", "").split("\n")[0]
|
|
54
|
+
if msg:
|
|
55
|
+
commits.append({
|
|
56
|
+
"message": msg,
|
|
57
|
+
"repo": repo,
|
|
58
|
+
"date": event.get("created_at", ""),
|
|
59
|
+
})
|
|
60
|
+
else:
|
|
61
|
+
commits.append({
|
|
62
|
+
"message": "pushed some code 👀",
|
|
63
|
+
"repo": repo,
|
|
64
|
+
"date": event.get("created_at", ""),
|
|
65
|
+
})
|
|
66
|
+
if len(commits) >= 5:
|
|
67
|
+
break
|
|
68
|
+
return commits
|
|
69
|
+
|
|
70
|
+
except Exception:
|
|
71
|
+
return []
|
|
72
|
+
|
|
73
|
+
def format_time_ago(date_str: str) -> str:
|
|
74
|
+
try:
|
|
75
|
+
dt = datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
|
|
76
|
+
now = datetime.now(timezone.utc)
|
|
77
|
+
diff = now - dt
|
|
78
|
+
minutes = int(diff.total_seconds() / 60)
|
|
79
|
+
|
|
80
|
+
if minutes < 60:
|
|
81
|
+
return f"{minutes}m ago"
|
|
82
|
+
elif minutes < 1440:
|
|
83
|
+
return f"{minutes // 60}h ago"
|
|
84
|
+
else:
|
|
85
|
+
return f"{minutes // 1440}d ago"
|
|
86
|
+
except Exception:
|
|
87
|
+
return "recently"
|
|
88
|
+
|
|
89
|
+
def _display_commits(username: str, commits: list):
|
|
90
|
+
if not commits:
|
|
91
|
+
comment = random.choice(NO_COMMITS_COMMENTS)
|
|
92
|
+
console.print(f" [dim]no recent commits from {username}[/dim]")
|
|
93
|
+
console.print(f" [yellow]👀 {comment}[/yellow]\n")
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
comment = random.choice(SARCASTIC_COMMENTS)
|
|
97
|
+
console.print(f" [bold]recent commits by [cyan]{username}[/cyan]:[/bold]\n")
|
|
98
|
+
|
|
99
|
+
for c in commits:
|
|
100
|
+
time_ago = format_time_ago(c["date"])
|
|
101
|
+
console.print(f" [green]▸[/green] [white]{c['message']}[/white]")
|
|
102
|
+
console.print(f" [dim]{c['repo']} · {time_ago}[/dim]")
|
|
103
|
+
|
|
104
|
+
console.print(f"\n [bold yellow]👀 {comment}[/bold yellow]\n")
|
|
105
|
+
|
|
106
|
+
def peer_pressure(username: str):
|
|
107
|
+
console.print(f"\n [bold cyan]checking what {username} is up to...[/bold cyan]\n")
|
|
108
|
+
commits = get_recent_commits(username)
|
|
109
|
+
_display_commits(username, commits)
|
|
110
|
+
|
|
111
|
+
def random_peer():
|
|
112
|
+
console.print(f"\n [bold magenta]pulling a random dev from the void...[/bold magenta]\n")
|
|
113
|
+
|
|
114
|
+
usernames = random.sample(RANDOM_USERNAMES, len(RANDOM_USERNAMES))
|
|
115
|
+
|
|
116
|
+
for username in usernames:
|
|
117
|
+
commits = get_recent_commits(username)
|
|
118
|
+
if commits:
|
|
119
|
+
_display_commits(username, commits)
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
console.print(" [dim]all devs are slacking too. rare.[/dim]")
|
|
123
|
+
console.print(" [yellow]👀 even the internet is taking a break. no excuses for you though.[/yellow]\n")
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import random
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
|
|
4
|
+
console = Console()
|
|
5
|
+
|
|
6
|
+
ROAST_FALLBACK = [
|
|
7
|
+
"your commits look like you're coding with your eyes closed ngl",
|
|
8
|
+
"bro really said 'fix' 5 times in a row. fix WHAT??",
|
|
9
|
+
"committing at 3am again? the grindset is NOT it",
|
|
10
|
+
"your commit history reads like a cry for help",
|
|
11
|
+
"one commit today? one? really? ok.",
|
|
12
|
+
"the way you commit... your tech lead is losing sleep",
|
|
13
|
+
"'update' is not a commit message. try again bestie",
|
|
14
|
+
"your streak died and you didn't even notice 💀",
|
|
15
|
+
"bro codes like they're defusing a bomb at all times",
|
|
16
|
+
"the git log called. it's concerned about you.",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
VIBE_FALLBACK = [
|
|
20
|
+
"diagnosis: you're cooked but in a productive way",
|
|
21
|
+
"your commits say 'i am fine' but the timestamps say otherwise",
|
|
22
|
+
"clinical assessment: chaotic good developer with trust issues",
|
|
23
|
+
"you commit like someone who has seen things. dark things.",
|
|
24
|
+
"prognosis: will survive, but the codebase might not",
|
|
25
|
+
"your vibe is 'senior dev in a junior's body' and it shows",
|
|
26
|
+
"mental state: 404 work life balance not found",
|
|
27
|
+
"you're not burnout, you're just... extra crispy",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
def get_commits_summary(repo_path: str = ".") -> str:
|
|
31
|
+
try:
|
|
32
|
+
import git
|
|
33
|
+
repo = git.Repo(repo_path, search_parent_directories=True)
|
|
34
|
+
commits = list(repo.iter_commits(max_count=20))
|
|
35
|
+
if not commits:
|
|
36
|
+
return "No commits found"
|
|
37
|
+
summary = []
|
|
38
|
+
for c in commits:
|
|
39
|
+
summary.append(f"- {c.message.strip()} ({c.authored_datetime.strftime('%A %H:%M')})")
|
|
40
|
+
return "\n".join(summary)
|
|
41
|
+
except Exception as e:
|
|
42
|
+
return f"Error reading commits: {e}"
|
|
43
|
+
|
|
44
|
+
def _try_ai_roast(commits_summary: str, mode: str = "roast") -> str | None:
|
|
45
|
+
try:
|
|
46
|
+
from openai import OpenAI
|
|
47
|
+
import os
|
|
48
|
+
|
|
49
|
+
api_key = os.environ.get("OPENAI_API_KEY")
|
|
50
|
+
if not api_key:
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
client = OpenAI(api_key=api_key)
|
|
54
|
+
|
|
55
|
+
if mode == "roast":
|
|
56
|
+
system = """You are a savage but funny code roaster.
|
|
57
|
+
Roast the developer based on their git commit history.
|
|
58
|
+
Be sarcastic, funny and gen z. Max 4 lines.
|
|
59
|
+
Respond in the same language the commits are written in."""
|
|
60
|
+
else:
|
|
61
|
+
system = """You are a therapist for developers but make it sarcastic.
|
|
62
|
+
Analyze the git commit history and give a mental health diagnosis.
|
|
63
|
+
Be funny, gen z, and dramatic. Max 5 lines.
|
|
64
|
+
Respond in the same language the commits are written in."""
|
|
65
|
+
|
|
66
|
+
response = client.chat.completions.create(
|
|
67
|
+
model="gpt-4o-mini",
|
|
68
|
+
max_tokens=300,
|
|
69
|
+
messages=[
|
|
70
|
+
{"role": "system", "content": system},
|
|
71
|
+
{"role": "user", "content": f"Commits:\n{commits_summary}"}
|
|
72
|
+
]
|
|
73
|
+
)
|
|
74
|
+
return response.choices[0].message.content
|
|
75
|
+
|
|
76
|
+
except Exception:
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
def roast_my_commits(repo_path: str = "."):
|
|
80
|
+
console.print("\n [bold magenta]loading your L's...[/bold magenta]\n")
|
|
81
|
+
commits = get_commits_summary(repo_path)
|
|
82
|
+
|
|
83
|
+
result = _try_ai_roast(commits, mode="roast")
|
|
84
|
+
source = "🤖 AI ROAST" if result else "🔥 ROAST"
|
|
85
|
+
if not result:
|
|
86
|
+
result = random.choice(ROAST_FALLBACK)
|
|
87
|
+
|
|
88
|
+
console.print(f" [bold red]{source}:[/bold red]\n")
|
|
89
|
+
console.print(f" [red]{result}[/red]\n")
|
|
90
|
+
|
|
91
|
+
def vibe_check(repo_path: str = "."):
|
|
92
|
+
console.print("\n [bold cyan]analyzing your vibe...[/bold cyan]\n")
|
|
93
|
+
commits = get_commits_summary(repo_path)
|
|
94
|
+
|
|
95
|
+
result = _try_ai_roast(commits, mode="vibe")
|
|
96
|
+
source = "🤖 AI VIBE CHECK" if result else "🧠 VIBE CHECK"
|
|
97
|
+
if not result:
|
|
98
|
+
result = random.choice(VIBE_FALLBACK)
|
|
99
|
+
|
|
100
|
+
console.print(f" [bold cyan]{source}:[/bold cyan]\n")
|
|
101
|
+
console.print(f" [cyan]{result}[/cyan]\n")
|
|
102
|
+
|
|
103
|
+
BLAME_FALLBACK = [
|
|
104
|
+
"this line was written by someone who had given up on life",
|
|
105
|
+
"whoever wrote this was clearly having a bad day. we don't judge. we do judge.",
|
|
106
|
+
"this code works and nobody knows why. not even the author.",
|
|
107
|
+
"written at 2am. you can tell.",
|
|
108
|
+
"the author of this line has since left the company. smart move.",
|
|
109
|
+
"this is either genius or a cry for help. probably both.",
|
|
110
|
+
"no comment. literally, they left no comment. typical.",
|
|
111
|
+
"the person who wrote this knew exactly what they were doing. unfortunately.",
|
|
112
|
+
"this line has survived 3 refactors. it's unkillable. it's a cockroach.",
|
|
113
|
+
"written with the confidence of someone who doesn't have to maintain this.",
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
def git_blame_personal(repo_path: str = "."):
|
|
117
|
+
import git
|
|
118
|
+
|
|
119
|
+
console.print("\n [bold yellow]analyzing who to blame...[/bold yellow]\n")
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
repo = git.Repo(repo_path, search_parent_directories=True)
|
|
123
|
+
commits = list(repo.iter_commits(max_count=10))
|
|
124
|
+
|
|
125
|
+
if not commits:
|
|
126
|
+
console.print(" [dim]no commits to blame. you're safe. for now.[/dim]\n")
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
blame_data = []
|
|
130
|
+
for commit in commits[:5]:
|
|
131
|
+
blame_data.append({
|
|
132
|
+
"author": commit.author.name,
|
|
133
|
+
"message": commit.message.strip().split("\n")[0],
|
|
134
|
+
"date": commit.authored_datetime.strftime("%A %H:%M"),
|
|
135
|
+
"files": list(commit.stats.files.keys())[:2],
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
result = _try_ai_blame(blame_data)
|
|
139
|
+
if not result:
|
|
140
|
+
import random
|
|
141
|
+
console.print(f" [bold yellow]🔍 GIT BLAME BUT MAKE IT PERSONAL:[/bold yellow]\n")
|
|
142
|
+
for item in blame_data:
|
|
143
|
+
comment = random.choice(BLAME_FALLBACK)
|
|
144
|
+
console.print(f" [white]commit:[/white] [dim]{item['message']}[/dim]")
|
|
145
|
+
console.print(f" [yellow]→ {comment}[/yellow]\n")
|
|
146
|
+
else:
|
|
147
|
+
console.print(f" [bold yellow]🔍 GIT BLAME BUT MAKE IT PERSONAL:[/bold yellow]\n")
|
|
148
|
+
console.print(f" [yellow]{result}[/yellow]\n")
|
|
149
|
+
|
|
150
|
+
except Exception as e:
|
|
151
|
+
console.print(f" [red]error: {e}[/red]\n")
|
|
152
|
+
|
|
153
|
+
def _try_ai_blame(blame_data: list) -> str | None:
|
|
154
|
+
try:
|
|
155
|
+
from openai import OpenAI
|
|
156
|
+
import os
|
|
157
|
+
|
|
158
|
+
api_key = os.environ.get("OPENAI_API_KEY")
|
|
159
|
+
if not api_key:
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
client = OpenAI(api_key=api_key)
|
|
163
|
+
|
|
164
|
+
system = """You are a savage code historian.
|
|
165
|
+
You analyze git commits and roast each one personally.
|
|
166
|
+
For each commit, make a personal comment about the author's life choices.
|
|
167
|
+
Be funny, gen z, sarcastic. One line per commit max.
|
|
168
|
+
Respond in the same language the commits are written in."""
|
|
169
|
+
|
|
170
|
+
response = client.chat.completions.create(
|
|
171
|
+
model="gpt-4o-mini",
|
|
172
|
+
max_tokens=400,
|
|
173
|
+
messages=[
|
|
174
|
+
{"role": "system", "content": system},
|
|
175
|
+
{"role": "user", "content": f"Blame these commits:\n{str(blame_data)}"}
|
|
176
|
+
]
|
|
177
|
+
)
|
|
178
|
+
return response.choices[0].message.content
|
|
179
|
+
|
|
180
|
+
except Exception:
|
|
181
|
+
return None
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
STORAGE_PATH = Path.home() / ".get2work" / "data.json"
|
|
5
|
+
|
|
6
|
+
DEFAULT_DATA = {
|
|
7
|
+
"commits": 0,
|
|
8
|
+
"streak": 0,
|
|
9
|
+
"last_commit_date": None,
|
|
10
|
+
"level": 1,
|
|
11
|
+
"total_lines_added": 0,
|
|
12
|
+
"total_lines_deleted": 0,
|
|
13
|
+
"pomodoros_completed": 0,
|
|
14
|
+
"distractions_caught": 0,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
def load() -> dict:
|
|
18
|
+
if not STORAGE_PATH.exists():
|
|
19
|
+
STORAGE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
20
|
+
save(DEFAULT_DATA)
|
|
21
|
+
return DEFAULT_DATA.copy()
|
|
22
|
+
with open(STORAGE_PATH) as f:
|
|
23
|
+
return json.load(f)
|
|
24
|
+
|
|
25
|
+
def save(data: dict):
|
|
26
|
+
STORAGE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
with open(STORAGE_PATH, "w") as f:
|
|
28
|
+
json.dump(data, f, indent=2)
|
|
29
|
+
|
|
30
|
+
def increment(key: str, amount: int = 1):
|
|
31
|
+
data = load()
|
|
32
|
+
data[key] = data.get(key, 0) + amount
|
|
33
|
+
save(data)
|
|
34
|
+
|
|
35
|
+
def update(key: str, value):
|
|
36
|
+
data = load()
|
|
37
|
+
data[key] = value
|
|
38
|
+
save(data)
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import random
|
|
3
|
+
import git
|
|
4
|
+
from datetime import date, datetime
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from get2work.storage import load, save, increment, update
|
|
7
|
+
from get2work.levels import check_levelup
|
|
8
|
+
from get2work.celebrate import celebrate_commit, celebrate_levelup, code_funeral, play_shame
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
SHAME_MESSAGES_LOW = [
|
|
13
|
+
"hey... everything ok? no commits today 👀",
|
|
14
|
+
"sooo are we coding or just vibing?",
|
|
15
|
+
"the repo is lonely. just saying.",
|
|
16
|
+
"no pressure but... commit something maybe?",
|
|
17
|
+
"gentle reminder that you exist and so does git",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
SHAME_MESSAGES_MED = [
|
|
21
|
+
"your streak is in danger bro 😬",
|
|
22
|
+
"bro really said 'i'll commit later' huh",
|
|
23
|
+
"the codebase misses you. or maybe it doesn't. hard to tell.",
|
|
24
|
+
"ok so we're just not committing today? ok.",
|
|
25
|
+
"your github is looking a little dry rn 💀",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
SHAME_MESSAGES_HIGH = [
|
|
29
|
+
"I'M NOT MAD I'M JUST DISAPPOINTED",
|
|
30
|
+
"THE AUDACITY OF NOT COMMITTING",
|
|
31
|
+
"at this point are you even a developer",
|
|
32
|
+
"your github is a desert. a wasteland. nothing.",
|
|
33
|
+
"bro said 'i'm a developer' and then didn't develop 💀",
|
|
34
|
+
"the commit history called. it's filing a missing persons report.",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
def get_shame_message(hours_since_commit: int) -> str:
|
|
38
|
+
if hours_since_commit < 4:
|
|
39
|
+
return random.choice(SHAME_MESSAGES_LOW)
|
|
40
|
+
elif hours_since_commit < 8:
|
|
41
|
+
return random.choice(SHAME_MESSAGES_MED)
|
|
42
|
+
else:
|
|
43
|
+
return random.choice(SHAME_MESSAGES_HIGH)
|
|
44
|
+
|
|
45
|
+
class CommitWatcher:
|
|
46
|
+
def __init__(self, repo_path: str = "."):
|
|
47
|
+
self.repo_path = repo_path
|
|
48
|
+
self.last_commit = None
|
|
49
|
+
self._get_last_commit()
|
|
50
|
+
|
|
51
|
+
def _get_last_commit(self):
|
|
52
|
+
try:
|
|
53
|
+
repo = git.Repo(self.repo_path, search_parent_directories=True)
|
|
54
|
+
commits = list(repo.iter_commits(max_count=1))
|
|
55
|
+
if commits:
|
|
56
|
+
self.last_commit = commits[0].hexsha
|
|
57
|
+
except Exception:
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
def check_new_commit(self):
|
|
61
|
+
try:
|
|
62
|
+
repo = git.Repo(self.repo_path, search_parent_directories=True)
|
|
63
|
+
commits = list(repo.iter_commits(max_count=1))
|
|
64
|
+
if not commits:
|
|
65
|
+
return
|
|
66
|
+
latest = commits[0]
|
|
67
|
+
if latest.hexsha == self.last_commit:
|
|
68
|
+
return
|
|
69
|
+
self.last_commit = latest.hexsha
|
|
70
|
+
self._handle_new_commit(repo, latest)
|
|
71
|
+
except Exception:
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
def _handle_new_commit(self, repo, commit):
|
|
75
|
+
try:
|
|
76
|
+
stats = commit.stats.total
|
|
77
|
+
lines_added = stats.get("insertions", 0)
|
|
78
|
+
lines_deleted = stats.get("deletions", 0)
|
|
79
|
+
except Exception:
|
|
80
|
+
lines_added = 0
|
|
81
|
+
lines_deleted = 0
|
|
82
|
+
|
|
83
|
+
increment("commits")
|
|
84
|
+
increment("total_lines_added", lines_added)
|
|
85
|
+
increment("total_lines_deleted", lines_deleted)
|
|
86
|
+
|
|
87
|
+
self._update_streak()
|
|
88
|
+
celebrate_commit()
|
|
89
|
+
|
|
90
|
+
if lines_deleted > 100:
|
|
91
|
+
code_funeral()
|
|
92
|
+
|
|
93
|
+
leveled_up, level_name = check_levelup()
|
|
94
|
+
if leveled_up:
|
|
95
|
+
celebrate_levelup(level_name)
|
|
96
|
+
|
|
97
|
+
def _update_streak(self):
|
|
98
|
+
data = load()
|
|
99
|
+
today = str(date.today())
|
|
100
|
+
last = data.get("last_commit_date")
|
|
101
|
+
|
|
102
|
+
if last == today:
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
if last is None:
|
|
106
|
+
data["streak"] = 1
|
|
107
|
+
else:
|
|
108
|
+
last_date = datetime.strptime(last, "%Y-%m-%d").date()
|
|
109
|
+
diff = (date.today() - last_date).days
|
|
110
|
+
if diff == 1:
|
|
111
|
+
data["streak"] = data.get("streak", 0) + 1
|
|
112
|
+
else:
|
|
113
|
+
data["streak"] = 1
|
|
114
|
+
|
|
115
|
+
data["last_commit_date"] = today
|
|
116
|
+
save(data)
|
|
117
|
+
|
|
118
|
+
def check_shame(self):
|
|
119
|
+
data = load()
|
|
120
|
+
last = data.get("last_commit_date")
|
|
121
|
+
today = str(date.today())
|
|
122
|
+
|
|
123
|
+
if last == today:
|
|
124
|
+
console.print("\n [bold green]✅ you committed today. we're proud.[/bold green]")
|
|
125
|
+
console.print(" [dim]don't ruin it.[/dim]\n")
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
if last is None:
|
|
129
|
+
hours = 24
|
|
130
|
+
else:
|
|
131
|
+
last_date = datetime.strptime(last, "%Y-%m-%d")
|
|
132
|
+
hours = int((datetime.now() - last_date).total_seconds() / 3600)
|
|
133
|
+
|
|
134
|
+
msg = get_shame_message(hours)
|
|
135
|
+
play_shame()
|
|
136
|
+
console.print(f"\n [bold red]⚠️ {msg}[/bold red]\n")
|
|
137
|
+
|
|
138
|
+
def update_streak():
|
|
139
|
+
data = load()
|
|
140
|
+
today = str(date.today())
|
|
141
|
+
last = data.get("last_commit_date")
|
|
142
|
+
|
|
143
|
+
if last == today:
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
if last is None:
|
|
147
|
+
data["streak"] = 1
|
|
148
|
+
else:
|
|
149
|
+
last_date = datetime.strptime(last, "%Y-%m-%d").date()
|
|
150
|
+
diff = (date.today() - last_date).days
|
|
151
|
+
if diff == 1:
|
|
152
|
+
data["streak"] = data.get("streak", 0) + 1
|
|
153
|
+
else:
|
|
154
|
+
data["streak"] = 1
|
|
155
|
+
|
|
156
|
+
data["last_commit_date"] = today
|
|
157
|
+
save(data)
|
|
158
|
+
|
|
159
|
+
def start_watching(repo_path: str = "."):
|
|
160
|
+
watcher = CommitWatcher(repo_path)
|
|
161
|
+
console.print("\n [bold green]get2work is watching you 👀[/bold green]")
|
|
162
|
+
console.print(" [dim]commit something or face the consequences[/dim]\n")
|
|
163
|
+
|
|
164
|
+
check_interval = 0
|
|
165
|
+
while True:
|
|
166
|
+
watcher.check_new_commit()
|
|
167
|
+
check_interval += 1
|
|
168
|
+
|
|
169
|
+
if check_interval % 180 == 0:
|
|
170
|
+
watcher.check_shame()
|
|
171
|
+
|
|
172
|
+
time.sleep(10)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "get2work"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "The Python library that makes you actually work"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"typer>=0.9.0",
|
|
13
|
+
"rich>=13.0.0",
|
|
14
|
+
"gitpython>=3.1.0",
|
|
15
|
+
"watchdog>=3.0.0",
|
|
16
|
+
"psutil>=5.9.0",
|
|
17
|
+
"anthropic>=0.20.0",
|
|
18
|
+
"requests>=2.31.0",
|
|
19
|
+
"apscheduler>=3.10.0",
|
|
20
|
+
"pygame>=2.5.0",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
get2work = "get2work.cli:app"
|
|
25
|
+
|
|
26
|
+
[tool.hatch.build.targets.wheel]
|
|
27
|
+
packages = ["get2work"]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|