raj-devpulse-cli 1.0.1__py3-none-any.whl

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.
devpulse/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """
2
+ DevPulse — Terminal-based developer productivity tracker.
3
+ Track streaks, log tasks, monitor hours, and stay motivated.
4
+ """
5
+
6
+ __version__ = "1.0.0"
7
+ __author__ = "Your Name"
8
+ __email__ = "you@example.com"
devpulse/cli.py ADDED
@@ -0,0 +1,278 @@
1
+ """
2
+ cli.py — DevPulse command-line interface.
3
+
4
+ Commands
5
+ --------
6
+ devpulse start – Begin a coding session
7
+ devpulse stop – End the current session
8
+ devpulse log – Log a completed task
9
+ devpulse stats – Full productivity dashboard
10
+ devpulse streak – Show current coding streak
11
+ devpulse quote – Display a motivational quote
12
+ devpulse heatmap – Show activity heatmap
13
+ devpulse weekly – Weekly summary
14
+ devpulse export – Export report (txt or json)
15
+ """
16
+
17
+ import sys
18
+ import time
19
+ import click
20
+ from datetime import date
21
+ from rich.console import Console
22
+ from rich.progress import Progress, SpinnerColumn, TextColumn
23
+
24
+ from devpulse import db, ui, quotes, heatmap, export
25
+
26
+ console = Console()
27
+
28
+
29
+ # ── Helper: ensure DB is ready before every command ──────────────────────────
30
+
31
+ def _init():
32
+ db.init_db()
33
+
34
+
35
+ # ── CLI group ─────────────────────────────────────────────────────────────────
36
+
37
+ @click.group()
38
+ @click.version_option(version="1.0.0", prog_name="DevPulse")
39
+ def cli():
40
+ """
41
+ \b
42
+ ⚡ DevPulse — Developer Productivity Tracker
43
+ Track streaks, sessions, tasks, and stay motivated.
44
+ """
45
+ _init()
46
+
47
+
48
+ # ── devpulse start ────────────────────────────────────────────────────────────
49
+
50
+ @cli.command()
51
+ def start():
52
+ """Start a new coding session."""
53
+ # Check if a session is already open
54
+ active = db.get_active_session()
55
+ if active:
56
+ console.print(
57
+ f"[yellow]⚠ A session is already running (ID #{active['id']}, "
58
+ f"started {active['start_ts'][:19]}).[/yellow]\n"
59
+ f" Run [cyan]devpulse stop[/cyan] first."
60
+ )
61
+ sys.exit(1)
62
+
63
+ with Progress(
64
+ SpinnerColumn(spinner_name="dots", style="cyan"),
65
+ TextColumn("[cyan]Starting session…"),
66
+ transient=True,
67
+ console=console,
68
+ ) as progress:
69
+ progress.add_task("", total=None)
70
+ time.sleep(0.8)
71
+
72
+ session_id = db.start_session()
73
+ ui.print_session_started(session_id)
74
+
75
+ # Show today's quote for motivation
76
+ q, author = quotes.get_quote_of_the_day()
77
+ console.print()
78
+ ui.print_quote(q, author)
79
+
80
+
81
+ # ── devpulse stop ─────────────────────────────────────────────────────────────
82
+
83
+ @cli.command()
84
+ def stop():
85
+ """End the current coding session."""
86
+ active = db.get_active_session()
87
+ if not active:
88
+ console.print("[yellow]⚠ No active session found. Run [cyan]devpulse start[/cyan] first.[/yellow]")
89
+ sys.exit(1)
90
+
91
+ with Progress(
92
+ SpinnerColumn(spinner_name="dots", style="cyan"),
93
+ TextColumn("[cyan]Saving session…"),
94
+ transient=True,
95
+ console=console,
96
+ ) as progress:
97
+ progress.add_task("", total=None)
98
+ time.sleep(0.6)
99
+
100
+ hours = db.end_session(active["id"])
101
+ ui.print_session_stopped(hours)
102
+
103
+
104
+ # ── devpulse log ──────────────────────────────────────────────────────────────
105
+
106
+ @cli.command()
107
+ @click.argument("task", nargs=-1, required=True)
108
+ def log(task):
109
+ """Log a completed task.
110
+
111
+ \b
112
+ Example:
113
+ devpulse log Fixed login bug
114
+ devpulse log "Refactored auth module"
115
+ """
116
+ description = " ".join(task)
117
+ task_id = db.add_task(description)
118
+ console.print(
119
+ f"[green]✔ Task #{task_id} logged:[/green] [white]{description}[/white]"
120
+ )
121
+
122
+
123
+ # ── devpulse stats ────────────────────────────────────────────────────────────
124
+
125
+ @cli.command()
126
+ def stats():
127
+ """Show the full productivity dashboard."""
128
+ with Progress(
129
+ SpinnerColumn(spinner_name="dots", style="cyan"),
130
+ TextColumn("[cyan]Loading stats…"),
131
+ transient=True,
132
+ console=console,
133
+ ) as progress:
134
+ progress.add_task("", total=None)
135
+ time.sleep(0.5)
136
+
137
+ ui.print_banner()
138
+
139
+ today = date.today().isoformat()
140
+ hours_today = db.get_daily_hours(today)
141
+ hours_map = db.get_hours_by_date(90)
142
+ total_hours = sum(hours_map.values())
143
+ active_dates = db.get_all_active_dates()
144
+ current_streak, longest_streak = db.compute_streak(active_dates)
145
+ tasks_today = db.get_tasks(today)
146
+ active_session = db.get_active_session()
147
+
148
+ # Stat cards row
149
+ session_label = f"#{active_session['id']} running" if active_session else "No active session"
150
+ ui.print_stat_row([
151
+ ("Today's Hours", f"{hours_today:.1f} h", "🕐", "cyan"),
152
+ ("Current Streak", f"{current_streak} days", "🔥", "yellow"),
153
+ ("Total Hours", f"{total_hours:.1f} h", "📈", "green"),
154
+ ("Session Status", session_label, "💻", "magenta"),
155
+ ])
156
+ console.print()
157
+
158
+ # Weekly summary
159
+ ui.print_weekly_summary(hours_map)
160
+ console.print()
161
+
162
+ # Heatmap
163
+ console.print(heatmap.render_heatmap(hours_map, weeks=18))
164
+ console.print()
165
+
166
+ # Today's tasks
167
+ ui.print_tasks(tasks_today, title=f"Today's Tasks ({today})")
168
+
169
+
170
+ # ── devpulse streak ───────────────────────────────────────────────────────────
171
+
172
+ @cli.command()
173
+ def streak():
174
+ """Display your current coding streak."""
175
+ active_dates = db.get_all_active_dates()
176
+ current, longest = db.compute_streak(active_dates)
177
+ ui.print_streak(current, longest)
178
+
179
+
180
+ # ── devpulse quote ────────────────────────────────────────────────────────────
181
+
182
+ @cli.command()
183
+ @click.option("--random", "use_random", is_flag=True, default=False,
184
+ help="Show a random quote instead of the daily one.")
185
+ def quote(use_random):
186
+ """Display a motivational quote."""
187
+ if use_random:
188
+ q, author = quotes.get_random_quote()
189
+ else:
190
+ q, author = quotes.get_quote_of_the_day()
191
+ ui.print_quote(q, author)
192
+
193
+
194
+ # ── devpulse heatmap ──────────────────────────────────────────────────────────
195
+
196
+ @cli.command()
197
+ @click.option("--weeks", default=18, show_default=True, help="Number of weeks to display.")
198
+ def heatmap_cmd(weeks):
199
+ """Show the GitHub-style activity heatmap."""
200
+ hours_map = db.get_hours_by_date(weeks * 7)
201
+
202
+ with Progress(
203
+ SpinnerColumn(spinner_name="dots", style="cyan"),
204
+ TextColumn("[cyan]Rendering heatmap…"),
205
+ transient=True,
206
+ console=console,
207
+ ) as progress:
208
+ progress.add_task("", total=None)
209
+ time.sleep(0.4)
210
+
211
+ console.print(heatmap.render_heatmap(hours_map, weeks=weeks))
212
+
213
+
214
+ # Register heatmap under the name "heatmap" (not "heatmap-cmd")
215
+ cli.add_command(heatmap_cmd, name="heatmap")
216
+
217
+
218
+ # ── devpulse weekly ───────────────────────────────────────────────────────────
219
+
220
+ @cli.command()
221
+ def weekly():
222
+ """Show the weekly productivity summary."""
223
+ hours_map = db.get_hours_by_date(7)
224
+ ui.print_weekly_summary(hours_map)
225
+
226
+
227
+ # ── devpulse export ───────────────────────────────────────────────────────────
228
+
229
+ @cli.command()
230
+ @click.option("--format", "fmt", type=click.Choice(["txt", "json"]), default="txt",
231
+ show_default=True, help="Output format.")
232
+ @click.option("--output", "-o", default=None, help="Output file path (optional).")
233
+ @click.option("--days", default=30, show_default=True, help="Number of days to include.")
234
+ def export_cmd(fmt, output, days):
235
+ """Export a productivity report to a file."""
236
+ with Progress(
237
+ SpinnerColumn(spinner_name="dots", style="cyan"),
238
+ TextColumn(f"[cyan]Exporting {fmt.upper()} report…"),
239
+ transient=True,
240
+ console=console,
241
+ ) as progress:
242
+ progress.add_task("", total=None)
243
+ time.sleep(0.5)
244
+
245
+ if fmt == "json":
246
+ path = export.export_json(output, days)
247
+ else:
248
+ path = export.export_txt(output, days)
249
+
250
+ console.print(f"[green]✔ Report saved to:[/green] [cyan]{path}[/cyan]")
251
+
252
+
253
+ cli.add_command(export_cmd, name="export")
254
+
255
+
256
+ # ── devpulse tasks ────────────────────────────────────────────────────────────
257
+
258
+ @cli.command()
259
+ @click.option("--all", "show_all", is_flag=True, default=False, help="Show all tasks, not just today's.")
260
+ def tasks(show_all):
261
+ """List logged tasks."""
262
+ if show_all:
263
+ t = db.get_all_tasks()
264
+ ui.print_tasks(t, title="All Tasks")
265
+ else:
266
+ today = date.today().isoformat()
267
+ t = db.get_tasks(today)
268
+ ui.print_tasks(t, title=f"Tasks for {today}")
269
+
270
+
271
+ # ── Entry point ───────────────────────────────────────────────────────────────
272
+
273
+ def main():
274
+ cli()
275
+
276
+
277
+ if __name__ == "__main__":
278
+ main()
devpulse/db.py ADDED
@@ -0,0 +1,244 @@
1
+ """
2
+ db.py — Database layer for DevPulse.
3
+ Handles all SQLite operations: sessions, tasks, and quotes.
4
+ """
5
+
6
+ import sqlite3
7
+ import os
8
+ from pathlib import Path
9
+ from datetime import date, datetime
10
+
11
+
12
+ # Store data in user's home directory under ~/.devpulse/
13
+ DATA_DIR = Path.home() / ".devpulse"
14
+ DB_PATH = DATA_DIR / "devpulse.db"
15
+
16
+
17
+ def get_connection() -> sqlite3.Connection:
18
+ """Return a sqlite3 connection, creating the DB file if needed."""
19
+ DATA_DIR.mkdir(parents=True, exist_ok=True)
20
+ conn = sqlite3.connect(DB_PATH)
21
+ conn.row_factory = sqlite3.Row # allows dict-like access to rows
22
+ return conn
23
+
24
+
25
+ def init_db() -> None:
26
+ """Create all tables if they don't exist yet."""
27
+ conn = get_connection()
28
+ cur = conn.cursor()
29
+
30
+ # sessions: one row per coding session
31
+ cur.execute("""
32
+ CREATE TABLE IF NOT EXISTS sessions (
33
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
34
+ date TEXT NOT NULL, -- ISO date: YYYY-MM-DD
35
+ start_ts TEXT NOT NULL, -- ISO datetime
36
+ end_ts TEXT, -- NULL while session is open
37
+ hours REAL DEFAULT 0.0 -- computed when session ends
38
+ )
39
+ """)
40
+
41
+ # tasks: user-logged completed items
42
+ cur.execute("""
43
+ CREATE TABLE IF NOT EXISTS tasks (
44
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
45
+ date TEXT NOT NULL,
46
+ description TEXT NOT NULL,
47
+ created_at TEXT NOT NULL
48
+ )
49
+ """)
50
+
51
+ # quotes cache (populated from bundled list)
52
+ cur.execute("""
53
+ CREATE TABLE IF NOT EXISTS shown_quotes (
54
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
55
+ quote_text TEXT NOT NULL,
56
+ shown_at TEXT NOT NULL
57
+ )
58
+ """)
59
+
60
+ conn.commit()
61
+ conn.close()
62
+
63
+
64
+ # ── Session helpers ──────────────────────────────────────────────────────────
65
+
66
+ def start_session() -> int:
67
+ """Open a new coding session. Returns the new session id."""
68
+ conn = get_connection()
69
+ cur = conn.cursor()
70
+ now = datetime.now().isoformat()
71
+ today = date.today().isoformat()
72
+ cur.execute(
73
+ "INSERT INTO sessions (date, start_ts) VALUES (?, ?)",
74
+ (today, now),
75
+ )
76
+ session_id = cur.lastrowid
77
+ conn.commit()
78
+ conn.close()
79
+ return session_id
80
+
81
+
82
+ def end_session(session_id: int) -> float:
83
+ """Close an open session and compute hours worked. Returns hours."""
84
+ conn = get_connection()
85
+ cur = conn.cursor()
86
+ now = datetime.now()
87
+
88
+ row = cur.execute(
89
+ "SELECT start_ts FROM sessions WHERE id = ?", (session_id,)
90
+ ).fetchone()
91
+
92
+ if not row:
93
+ conn.close()
94
+ return 0.0
95
+
96
+ start = datetime.fromisoformat(row["start_ts"])
97
+ hours = (now - start).total_seconds() / 3600
98
+
99
+ cur.execute(
100
+ "UPDATE sessions SET end_ts = ?, hours = ? WHERE id = ?",
101
+ (now.isoformat(), round(hours, 4), session_id),
102
+ )
103
+ conn.commit()
104
+ conn.close()
105
+ return hours
106
+
107
+
108
+ def get_active_session() -> dict | None:
109
+ """Return the most recent open session, or None."""
110
+ conn = get_connection()
111
+ cur = conn.cursor()
112
+ row = cur.execute(
113
+ "SELECT * FROM sessions WHERE end_ts IS NULL ORDER BY id DESC LIMIT 1"
114
+ ).fetchone()
115
+ conn.close()
116
+ return dict(row) if row else None
117
+
118
+
119
+ def get_daily_hours(iso_date: str) -> float:
120
+ """Sum of hours for all completed sessions on a given date."""
121
+ conn = get_connection()
122
+ cur = conn.cursor()
123
+ row = cur.execute(
124
+ "SELECT COALESCE(SUM(hours), 0) AS total FROM sessions WHERE date = ? AND end_ts IS NOT NULL",
125
+ (iso_date,),
126
+ ).fetchone()
127
+ conn.close()
128
+ return float(row["total"])
129
+
130
+
131
+ def get_hours_by_date(days: int = 90) -> dict[str, float]:
132
+ """Return {iso_date: hours} for the last `days` days."""
133
+ conn = get_connection()
134
+ cur = conn.cursor()
135
+ rows = cur.execute(
136
+ """
137
+ SELECT date, COALESCE(SUM(hours), 0) AS total
138
+ FROM sessions
139
+ WHERE end_ts IS NOT NULL
140
+ AND date >= date('now', ?)
141
+ GROUP BY date
142
+ """,
143
+ (f"-{days} days",),
144
+ ).fetchall()
145
+ conn.close()
146
+ return {r["date"]: float(r["total"]) for r in rows}
147
+
148
+
149
+ # ── Task helpers ─────────────────────────────────────────────────────────────
150
+
151
+ def add_task(description: str) -> int:
152
+ """Insert a completed task for today. Returns new task id."""
153
+ conn = get_connection()
154
+ cur = conn.cursor()
155
+ now = datetime.now().isoformat()
156
+ today = date.today().isoformat()
157
+ cur.execute(
158
+ "INSERT INTO tasks (date, description, created_at) VALUES (?, ?, ?)",
159
+ (today, description, now),
160
+ )
161
+ task_id = cur.lastrowid
162
+ conn.commit()
163
+ conn.close()
164
+ return task_id
165
+
166
+
167
+ def get_tasks(iso_date: str) -> list[dict]:
168
+ """Return all tasks for a given date."""
169
+ conn = get_connection()
170
+ cur = conn.cursor()
171
+ rows = cur.execute(
172
+ "SELECT * FROM tasks WHERE date = ? ORDER BY created_at",
173
+ (iso_date,),
174
+ ).fetchall()
175
+ conn.close()
176
+ return [dict(r) for r in rows]
177
+
178
+
179
+ def get_all_tasks(limit: int = 200) -> list[dict]:
180
+ """Return most recent tasks across all dates."""
181
+ conn = get_connection()
182
+ cur = conn.cursor()
183
+ rows = cur.execute(
184
+ "SELECT * FROM tasks ORDER BY created_at DESC LIMIT ?", (limit,)
185
+ ).fetchall()
186
+ conn.close()
187
+ return [dict(r) for r in rows]
188
+
189
+
190
+ # ── Streak helpers ────────────────────────────────────────────────────────────
191
+
192
+ def get_all_active_dates() -> list[str]:
193
+ """Return sorted list of dates that have ≥1 completed session OR ≥1 task."""
194
+ conn = get_connection()
195
+ cur = conn.cursor()
196
+ rows = cur.execute(
197
+ """
198
+ SELECT DISTINCT date FROM sessions WHERE end_ts IS NOT NULL
199
+ UNION
200
+ SELECT DISTINCT date FROM tasks
201
+ ORDER BY date
202
+ """
203
+ ).fetchall()
204
+ conn.close()
205
+ return [r["date"] for r in rows]
206
+
207
+
208
+ def compute_streak(active_dates: list[str]) -> tuple[int, int]:
209
+ """
210
+ Given a sorted list of active ISO dates, compute:
211
+ - current streak (consecutive days ending today or yesterday)
212
+ - longest streak ever
213
+ Returns (current_streak, longest_streak).
214
+ """
215
+ if not active_dates:
216
+ return 0, 0
217
+
218
+ from datetime import date, timedelta
219
+
220
+ date_set = {date.fromisoformat(d) for d in active_dates}
221
+ today = date.today()
222
+
223
+ # ── longest streak ──
224
+ longest = 1
225
+ current_run = 1
226
+ sorted_dates = sorted(date_set)
227
+ for i in range(1, len(sorted_dates)):
228
+ if sorted_dates[i] - sorted_dates[i - 1] == timedelta(days=1):
229
+ current_run += 1
230
+ longest = max(longest, current_run)
231
+ else:
232
+ current_run = 1
233
+
234
+ # ── current streak (must include today or yesterday) ──
235
+ if today not in date_set and (today - timedelta(days=1)) not in date_set:
236
+ return 0, longest
237
+
238
+ streak = 0
239
+ check = today if today in date_set else today - timedelta(days=1)
240
+ while check in date_set:
241
+ streak += 1
242
+ check -= timedelta(days=1)
243
+
244
+ return streak, longest
devpulse/export.py ADDED
@@ -0,0 +1,86 @@
1
+ """
2
+ export.py — Export DevPulse data to TXT or JSON files.
3
+ """
4
+
5
+ import json
6
+ from datetime import date, timedelta
7
+ from pathlib import Path
8
+
9
+ from devpulse import db
10
+
11
+
12
+ def _collect_report_data(days: int = 30) -> dict:
13
+ """Gather all stats needed for a report."""
14
+ today = date.today()
15
+ hours_map = db.get_hours_by_date(days)
16
+ active_dates = db.get_all_active_dates()
17
+ current_streak, longest_streak = db.compute_streak(active_dates)
18
+
19
+ total_hours = sum(hours_map.values())
20
+ total_days = len(hours_map)
21
+ avg_hours = total_hours / total_days if total_days else 0.0
22
+
23
+ # Recent tasks
24
+ tasks = db.get_all_tasks(limit=100)
25
+
26
+ return {
27
+ "generated_at": today.isoformat(),
28
+ "period_days": days,
29
+ "total_hours": round(total_hours, 2),
30
+ "average_hours_per_day": round(avg_hours, 2),
31
+ "active_days": total_days,
32
+ "current_streak": current_streak,
33
+ "longest_streak": longest_streak,
34
+ "hours_by_date": hours_map,
35
+ "recent_tasks": tasks,
36
+ }
37
+
38
+
39
+ def export_json(output_path: str | None = None, days: int = 30) -> Path:
40
+ """Export report as JSON. Returns the path written."""
41
+ data = _collect_report_data(days)
42
+ path = Path(output_path) if output_path else Path.cwd() / f"devpulse_report_{data['generated_at']}.json"
43
+ path.write_text(json.dumps(data, indent=2, default=str), encoding="utf-8")
44
+ return path
45
+
46
+
47
+ def export_txt(output_path: str | None = None, days: int = 30) -> Path:
48
+ """Export report as plain text. Returns the path written."""
49
+ data = _collect_report_data(days)
50
+ path = Path(output_path) if output_path else Path.cwd() / f"devpulse_report_{data['generated_at']}.txt"
51
+
52
+ lines = [
53
+ "=" * 60,
54
+ " DEVPULSE — PRODUCTIVITY REPORT",
55
+ f" Generated : {data['generated_at']}",
56
+ f" Period : Last {data['period_days']} days",
57
+ "=" * 60,
58
+ "",
59
+ "SUMMARY",
60
+ "-------",
61
+ f" Total hours coded : {data['total_hours']} h",
62
+ f" Active days : {data['active_days']}",
63
+ f" Avg hours / day : {data['average_hours_per_day']} h",
64
+ f" Current streak : {data['current_streak']} days 🔥",
65
+ f" Longest streak : {data['longest_streak']} days",
66
+ "",
67
+ "DAILY HOURS",
68
+ "-----------",
69
+ ]
70
+
71
+ for iso_date, h in sorted(data["hours_by_date"].items()):
72
+ bar = "█" * int(h) + ("▌" if h % 1 >= 0.5 else "")
73
+ lines.append(f" {iso_date} {h:5.2f} h {bar}")
74
+
75
+ lines += [
76
+ "",
77
+ "RECENT TASKS",
78
+ "------------",
79
+ ]
80
+ for task in data["recent_tasks"][:30]:
81
+ lines.append(f" [{task['date']}] {task['description']}")
82
+
83
+ lines += ["", "=" * 60, " Keep shipping! 🚀", "=" * 60]
84
+
85
+ path.write_text("\n".join(lines), encoding="utf-8")
86
+ return path
devpulse/heatmap.py ADDED
@@ -0,0 +1,95 @@
1
+ """
2
+ heatmap.py — Render a GitHub-style activity heatmap in the terminal using Rich.
3
+ Columns = weeks (oldest → newest, left → right).
4
+ Rows = days of week (Mon–Sun).
5
+ """
6
+
7
+ from datetime import date, timedelta
8
+ from rich.console import Console
9
+ from rich.text import Text
10
+ from rich.panel import Panel
11
+
12
+
13
+ # ── Colour palette (hours → colour) ──────────────────────────────────────────
14
+ # Each level maps to a Rich colour string.
15
+ LEVELS = [
16
+ (0, "grey23"), # no activity
17
+ (0.5, "dark_green"), # tiny bit
18
+ (1.5, "green3"), # ~1-1.5 h
19
+ (3.0, "green1"), # ~1.5-3 h
20
+ (5.0, "chartreuse1"), # ~3-5 h
21
+ (999, "bright_white"), # 5+ h — beast mode
22
+ ]
23
+
24
+ BLOCK = "■ " # each "cell" in the heatmap
25
+ EMPTY = "· " # placeholder for future dates
26
+
27
+
28
+ def _colour_for_hours(h: float) -> str:
29
+ """Map a float hours value to a Rich colour name."""
30
+ for threshold, colour in LEVELS:
31
+ if h <= threshold:
32
+ return colour
33
+ return LEVELS[-1][1]
34
+
35
+
36
+ def render_heatmap(hours_by_date: dict[str, float], weeks: int = 18) -> Panel:
37
+ """
38
+ Build and return a Rich Panel containing the heatmap.
39
+
40
+ Args:
41
+ hours_by_date: mapping of ISO date string → hours coded that day.
42
+ weeks: how many weeks to show (default 18 ≈ ~4 months).
43
+ """
44
+ today = date.today()
45
+
46
+ # Align start to the Monday of the week `weeks` ago
47
+ start = today - timedelta(days=today.weekday() + 7 * (weeks - 1))
48
+
49
+ # Day-of-week labels
50
+ day_labels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
51
+
52
+ # Build a 7-row grid: rows = weekdays, cols = weeks
53
+ grid: list[list[tuple[str, float]]] = [[] for _ in range(7)]
54
+
55
+ for week in range(weeks):
56
+ for dow in range(7): # 0=Mon … 6=Sun
57
+ d = start + timedelta(days=week * 7 + dow)
58
+ iso = d.isoformat()
59
+ h = hours_by_date.get(iso, 0.0)
60
+ # Mark future dates differently
61
+ is_future = d > today
62
+ grid[dow].append(("future" if is_future else iso, h))
63
+
64
+ # ── Assemble Rich Text row by row ────────────────────────────────────────
65
+ lines: list[Text] = []
66
+ for dow in range(7):
67
+ line = Text()
68
+ line.append(f"{day_labels[dow]} ", style="bold cyan")
69
+ for iso_or_tag, h in grid[dow]:
70
+ if iso_or_tag == "future":
71
+ line.append(EMPTY, style="grey19")
72
+ else:
73
+ colour = _colour_for_hours(h)
74
+ line.append(BLOCK, style=colour)
75
+ lines.append(line)
76
+
77
+ # ── Legend ────────────────────────────────────────────────────────────────
78
+ legend = Text("\n Less ", style="dim")
79
+ for _, colour in LEVELS:
80
+ legend.append(BLOCK, style=colour)
81
+ legend.append(" More", style="dim")
82
+
83
+ # Combine rows + legend
84
+ combined = Text()
85
+ for line in lines:
86
+ combined.append_text(line)
87
+ combined.append("\n")
88
+ combined.append_text(legend)
89
+
90
+ return Panel(
91
+ combined,
92
+ title="[bold cyan]⬛ Activity Heatmap[/bold cyan]",
93
+ border_style="cyan",
94
+ padding=(1, 2),
95
+ )
devpulse/quotes.py ADDED
@@ -0,0 +1,53 @@
1
+ """
2
+ quotes.py — Curated motivational quotes for developers.
3
+ """
4
+
5
+ import random
6
+
7
+ QUOTES: list[tuple[str, str]] = [
8
+ ("First, solve the problem. Then, write the code.", "John Johnson"),
9
+ ("Code is like humor. When you have to explain it, it's bad.", "Cory House"),
10
+ ("Any fool can write code that a computer can understand. Good programmers write code that humans can understand.", "Martin Fowler"),
11
+ ("The best error message is the one that never shows up.", "Thomas Fuchs"),
12
+ ("Fix the cause, not the symptom.", "Steve Maguire"),
13
+ ("Simplicity is the soul of efficiency.", "Austin Freeman"),
14
+ ("Make it work, make it right, make it fast.", "Kent Beck"),
15
+ ("Talk is cheap. Show me the code.", "Linus Torvalds"),
16
+ ("Programs must be written for people to read, and only incidentally for machines to execute.", "Harold Abelson"),
17
+ ("Every great developer you know got there by solving problems they were unqualified to solve until they did it.", "Patrick McKenzie"),
18
+ ("It's not a bug — it's an undocumented feature.", "Anonymous"),
19
+ ("The only way to go fast is to go well.", "Robert C. Martin"),
20
+ ("Debugging is twice as hard as writing the code in the first place.", "Brian Kernighan"),
21
+ ("One bad programmer can easily create two new jobs a year.", "David Parnas"),
22
+ ("In order to be irreplaceable, one must always be different.", "Coco Chanel"),
23
+ ("The function of good software is to make the complex appear simple.", "Grady Booch"),
24
+ ("Measuring programming progress by lines of code is like measuring aircraft building progress by weight.", "Bill Gates"),
25
+ ("Don't comment bad code — rewrite it.", "Brian Kernighan"),
26
+ ("Perfection is achieved not when there is nothing more to add, but when there is nothing left to take away.", "Antoine de Saint-Exupéry"),
27
+ ("The most disastrous thing that you can ever learn is your first programming language.", "Alan Kay"),
28
+ ("Software is eating the world.", "Marc Andreessen"),
29
+ ("The best way to predict the future is to invent it.", "Alan Kay"),
30
+ ("Weeks of coding can save you hours of planning.", "Anonymous"),
31
+ ("A ship in harbor is safe, but that is not what ships are for.", "John A. Shedd"),
32
+ ("Consistency is the key to mastery.", "Robin Sharma"),
33
+ ("Small steps every day lead to big leaps over time.", "Anonymous"),
34
+ ("Your limitation — it's only your imagination.", "Anonymous"),
35
+ ("Push yourself, because no one else is going to do it for you.", "Anonymous"),
36
+ ("Dream it. Wish it. Do it.", "Anonymous"),
37
+ ("Great things never come from comfort zones.", "Anonymous"),
38
+ ]
39
+
40
+
41
+ def get_random_quote() -> tuple[str, str]:
42
+ """Return a random (quote, author) tuple."""
43
+ return random.choice(QUOTES)
44
+
45
+
46
+ def get_quote_of_the_day() -> tuple[str, str]:
47
+ """
48
+ Return a deterministic daily quote based on today's date.
49
+ Same quote all day, different each day.
50
+ """
51
+ from datetime import date
52
+ idx = date.today().toordinal() % len(QUOTES)
53
+ return QUOTES[idx]
devpulse/ui.py ADDED
@@ -0,0 +1,201 @@
1
+ """
2
+ ui.py — Reusable Rich UI components for DevPulse.
3
+ All terminal rendering lives here to keep cli.py clean.
4
+ """
5
+
6
+ from datetime import date
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+ from rich.table import Table
10
+ from rich.text import Text
11
+ from rich.align import Align
12
+ from rich.columns import Columns
13
+ from rich import box
14
+
15
+ console = Console()
16
+
17
+ # ── Brand colours (used as Rich style strings) ────────────────────────────
18
+ PRIMARY = "bold cyan"
19
+ ACCENT = "bold green"
20
+ DIM = "dim white"
21
+ WARN = "bold yellow"
22
+ ERROR = "bold red"
23
+ MUTED = "grey62"
24
+
25
+
26
+ # ── Banner ────────────────────────────────────────────────────────────────────
27
+
28
+ BANNER = r"""
29
+ ██████╗ ███████╗██╗ ██╗██████╗ ██╗ ██╗██╗ ███████╗███████╗
30
+ ██╔══██╗██╔════╝██║ ██║██╔══██╗██║ ██║██║ ██╔════╝██╔════╝
31
+ ██║ ██║█████╗ ██║ ██║██████╔╝██║ ██║██║ ███████╗█████╗
32
+ ██║ ██║██╔══╝ ╚██╗ ██╔╝██╔═══╝ ██║ ██║██║ ╚════██║██╔══╝
33
+ ██████╔╝███████╗ ╚████╔╝ ██║ ╚██████╔╝███████╗███████║███████╗
34
+ ╚═════╝ ╚══════╝ ╚═══╝ ╚═╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝
35
+ """
36
+
37
+
38
+ def print_banner() -> None:
39
+ """Print the DevPulse ASCII banner with a tagline."""
40
+ console.print(BANNER, style="bold cyan", highlight=False)
41
+ console.print(
42
+ Align.center("[dim cyan]⚡ Developer Productivity Tracker • v1.0.0[/dim cyan]")
43
+ )
44
+ console.print()
45
+
46
+
47
+ # ── Stat cards ────────────────────────────────────────────────────────────────
48
+
49
+ def stat_card(label: str, value: str, icon: str = "", colour: str = "cyan") -> Panel:
50
+ """Return a small Rich Panel acting as a stat card."""
51
+ body = Text(justify="center")
52
+ body.append(f"{icon} " if icon else "", style=f"bold {colour}")
53
+ body.append(value, style=f"bold {colour}")
54
+ return Panel(body, title=f"[dim]{label}[/dim]", border_style=colour, padding=(0, 2))
55
+
56
+
57
+ def print_stat_row(stats: list[tuple[str, str, str, str]]) -> None:
58
+ """
59
+ Print a row of stat cards.
60
+ Each stat is (label, value, icon, colour).
61
+ """
62
+ cards = [stat_card(label, value, icon, colour) for label, value, icon, colour in stats]
63
+ console.print(Columns(cards, equal=True, expand=True))
64
+
65
+
66
+ # ── Quote display ─────────────────────────────────────────────────────────────
67
+
68
+ def print_quote(quote: str, author: str) -> None:
69
+ """Render a motivational quote in a styled panel."""
70
+ text = Text(justify="center")
71
+ text.append(f'"{quote}"\n\n', style="italic white")
72
+ text.append(f"— {author}", style="bold cyan")
73
+
74
+ console.print(
75
+ Panel(
76
+ Align.center(text),
77
+ title="[bold cyan]✨ Quote of the Day[/bold cyan]",
78
+ border_style="cyan",
79
+ padding=(1, 4),
80
+ )
81
+ )
82
+
83
+
84
+ # ── Streak display ────────────────────────────────────────────────────────────
85
+
86
+ def print_streak(current: int, longest: int) -> None:
87
+ """Render streak info with a flame visual."""
88
+ flame_colours = ["red", "dark_orange", "orange1", "yellow1", "bright_yellow"]
89
+
90
+ # build a little flame bar proportional to the streak
91
+ width = min(current, 30)
92
+ flame_bar = Text()
93
+ for i in range(width):
94
+ colour = flame_colours[i % len(flame_colours)]
95
+ flame_bar.append("🔥", style=colour)
96
+
97
+ body = Text(justify="center")
98
+ body.append(f"\n{current}", style="bold bright_yellow")
99
+ body.append(" day streak\n\n", style="bold white")
100
+ body.append_text(flame_bar)
101
+ body.append(f"\n\nAll-time best: ", style="dim white")
102
+ body.append(f"{longest} days", style="bold cyan")
103
+
104
+ console.print(
105
+ Panel(
106
+ Align.center(body),
107
+ title="[bold yellow]🔥 Coding Streak[/bold yellow]",
108
+ border_style="yellow",
109
+ padding=(1, 4),
110
+ )
111
+ )
112
+
113
+
114
+ # ── Task list ─────────────────────────────────────────────────────────────────
115
+
116
+ def print_tasks(tasks: list[dict], title: str = "Today's Tasks") -> None:
117
+ """Render a list of tasks in a Rich table."""
118
+ table = Table(
119
+ title=title,
120
+ box=box.SIMPLE_HEAD,
121
+ border_style="cyan",
122
+ header_style="bold cyan",
123
+ show_lines=False,
124
+ )
125
+ table.add_column("#", style="dim", width=4)
126
+ table.add_column("Date", style="cyan", width=12)
127
+ table.add_column("Task", style="white")
128
+
129
+ for i, task in enumerate(tasks, start=1):
130
+ table.add_row(str(i), task["date"], task["description"])
131
+
132
+ if not tasks:
133
+ console.print(Panel("[dim]No tasks logged yet.[/dim]", border_style="dim"))
134
+ else:
135
+ console.print(table)
136
+
137
+
138
+ # ── Weekly summary table ──────────────────────────────────────────────────────
139
+
140
+ def print_weekly_summary(hours_by_date: dict[str, float]) -> None:
141
+ """Print last 7 days as a table with a simple bar chart."""
142
+ from datetime import timedelta
143
+
144
+ today = date.today()
145
+ table = Table(
146
+ title="[bold cyan]📅 Last 7 Days[/bold cyan]",
147
+ box=box.SIMPLE_HEAD,
148
+ border_style="cyan",
149
+ header_style="bold cyan",
150
+ show_lines=False,
151
+ )
152
+ table.add_column("Date", style="cyan", width=12)
153
+ table.add_column("Day", width=5)
154
+ table.add_column("Hours", justify="right", width=8)
155
+ table.add_column("Progress", min_width=20)
156
+
157
+ day_names = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
158
+ total = 0.0
159
+
160
+ for offset in range(6, -1, -1):
161
+ d = today - timedelta(days=offset)
162
+ iso = d.isoformat()
163
+ h = hours_by_date.get(iso, 0.0)
164
+ total += h
165
+ bar_len = int(h * 4) # 4 blocks per hour
166
+ bar = "█" * bar_len
167
+ colour = "green" if h >= 2 else ("yellow" if h > 0 else "grey30")
168
+ table.add_row(
169
+ iso,
170
+ day_names[d.weekday()],
171
+ f"{h:.1f} h",
172
+ f"[{colour}]{bar}[/{colour}]",
173
+ )
174
+
175
+ console.print(table)
176
+ console.print(f" [bold]Weekly total:[/bold] [cyan]{total:.1f} h[/cyan] | "
177
+ f"[bold]Average:[/bold] [cyan]{total/7:.1f} h/day[/cyan]")
178
+
179
+
180
+ # ── Session status ────────────────────────────────────────────────────────────
181
+
182
+ def print_session_started(session_id: int) -> None:
183
+ console.print(
184
+ Panel(
185
+ f"[bold green]▶ Session #{session_id} started[/bold green]\n"
186
+ f"[dim]Run [cyan]devpulse stop[/cyan] when you're done.[/dim]",
187
+ border_style="green",
188
+ padding=(1, 2),
189
+ )
190
+ )
191
+
192
+
193
+ def print_session_stopped(hours: float) -> None:
194
+ console.print(
195
+ Panel(
196
+ f"[bold cyan]⏹ Session ended[/bold cyan]\n"
197
+ f"[green]Logged [bold]{hours:.2f} hours[/bold] — great work! 💪[/green]",
198
+ border_style="cyan",
199
+ padding=(1, 2),
200
+ )
201
+ )
@@ -0,0 +1,221 @@
1
+ Metadata-Version: 2.4
2
+ Name: raj-devpulse-cli
3
+ Version: 1.0.1
4
+ Summary: ⚡ Terminal-based developer productivity tracker — streaks, sessions, heatmaps & more.
5
+ Author-email: Your Name <you@example.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/yourname/devpulse
8
+ Project-URL: Repository, https://github.com/yourname/devpulse
9
+ Project-URL: Bug Tracker, https://github.com/yourname/devpulse/issues
10
+ Keywords: productivity,cli,developer,streak,terminal,coding
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Utilities
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: click>=8.1
25
+ Requires-Dist: rich>=13.0
26
+ Dynamic: license-file
27
+
28
+ # ⚡ DevPulse
29
+
30
+ <div align="center">
31
+
32
+ ![PyPI version](https://img.shields.io/pypi/v/devpulse-cli?color=00d4ff&style=for-the-badge)
33
+ ![Python](https://img.shields.io/pypi/pyversions/devpulse-cli?color=00d4ff&style=for-the-badge)
34
+ ![License](https://img.shields.io/github/license/yourname/devpulse?color=00d4ff&style=for-the-badge)
35
+ ![Stars](https://img.shields.io/github/stars/yourname/devpulse?color=00d4ff&style=for-the-badge)
36
+
37
+ **A futuristic terminal productivity tracker for developers.**
38
+ Track coding sessions, streaks, tasks, and stay motivated — all from your terminal.
39
+
40
+ </div>
41
+
42
+ ---
43
+
44
+ ## ✨ Features
45
+
46
+ | Feature | Description |
47
+ |---|---|
48
+ | 🔥 **Coding Streaks** | Daily streak tracker with all-time best |
49
+ | 🕐 **Session Timer** | Start/stop coding sessions, auto-track hours |
50
+ | ✅ **Task Logger** | Log completed tasks with timestamps |
51
+ | 📈 **Activity Heatmap** | GitHub-style heatmap in your terminal |
52
+ | 📅 **Weekly Summary** | Hours per day with progress bars |
53
+ | ✨ **Daily Quote** | Deterministic motivational quote each day |
54
+ | 📤 **Export Reports** | TXT or JSON productivity reports |
55
+ | 💾 **Local SQLite** | All data stored privately on your machine |
56
+
57
+ ---
58
+
59
+ ## 🚀 Installation
60
+
61
+ ### From PyPI (recommended)
62
+
63
+ ```bash
64
+ pip install devpulse-cli
65
+ ```
66
+
67
+ ### From source
68
+
69
+ ```bash
70
+ git clone https://github.com/yourname/devpulse.git
71
+ cd devpulse
72
+ pip install -e .
73
+ ```
74
+
75
+ ---
76
+
77
+ ## 📖 Usage
78
+
79
+ ### Start a coding session
80
+ ```bash
81
+ devpulse start
82
+ ```
83
+
84
+ ### Stop and save session
85
+ ```bash
86
+ devpulse stop
87
+ ```
88
+
89
+ ### Log a completed task
90
+ ```bash
91
+ devpulse log Fixed the authentication bug
92
+ devpulse log "Wrote unit tests for the payment module"
93
+ ```
94
+
95
+ ### Full productivity dashboard
96
+ ```bash
97
+ devpulse stats
98
+ ```
99
+
100
+ ### View your streak
101
+ ```bash
102
+ devpulse streak
103
+ ```
104
+
105
+ ### Get a motivational quote
106
+ ```bash
107
+ devpulse quote # today's quote (same all day)
108
+ devpulse quote --random # random quote
109
+ ```
110
+
111
+ ### Activity heatmap
112
+ ```bash
113
+ devpulse heatmap # last 18 weeks
114
+ devpulse heatmap --weeks 8 # custom range
115
+ ```
116
+
117
+ ### Weekly summary
118
+ ```bash
119
+ devpulse weekly
120
+ ```
121
+
122
+ ### View tasks
123
+ ```bash
124
+ devpulse tasks # today's tasks
125
+ devpulse tasks --all # all tasks
126
+ ```
127
+
128
+ ### Export a report
129
+ ```bash
130
+ devpulse export # TXT report (last 30 days)
131
+ devpulse export --format json # JSON report
132
+ devpulse export --days 90 -o report.txt
133
+ ```
134
+
135
+ ---
136
+
137
+ ## 🗂 Project Structure
138
+
139
+ ```
140
+ devpulse/
141
+ ├── devpulse/
142
+ │ ├── __init__.py # Version info
143
+ │ ├── cli.py # All CLI commands (Click)
144
+ │ ├── db.py # SQLite data layer
145
+ │ ├── ui.py # Rich terminal UI components
146
+ │ ├── heatmap.py # GitHub-style heatmap renderer
147
+ │ ├── quotes.py # Motivational quotes
148
+ │ └── export.py # TXT / JSON export
149
+ ├── tests/
150
+ │ └── test_core.py # Unit tests
151
+ ├── pyproject.toml # Package config & metadata
152
+ ├── requirements.txt # Dependencies
153
+ ├── LICENSE # MIT
154
+ └── README.md
155
+ ```
156
+
157
+ ---
158
+
159
+ ## 🗄 Data Storage
160
+
161
+ All data is stored locally in `~/.devpulse/devpulse.db` (SQLite).
162
+ No network requests, no accounts, no telemetry.
163
+
164
+ ---
165
+
166
+ ## 🧪 Running Tests
167
+
168
+ ```bash
169
+ pip install pytest pytest-cov
170
+ pytest -v
171
+ pytest --cov=devpulse --cov-report=term-missing
172
+ ```
173
+
174
+ ---
175
+
176
+ ## 📦 Publishing to PyPI
177
+
178
+ ### 1. Set up accounts
179
+ - Create account at https://pypi.org
180
+ - Install tools: `pip install build twine`
181
+
182
+ ### 2. Update metadata
183
+ Edit `pyproject.toml` — update `name`, `authors`, `urls`.
184
+
185
+ ### 3. Build the package
186
+ ```bash
187
+ python -m build
188
+ ```
189
+ This creates `dist/devpulse_cli-1.0.0.tar.gz` and `dist/devpulse_cli-1.0.0-py3-none-any.whl`.
190
+
191
+ ### 4. Test on TestPyPI first (optional but recommended)
192
+ ```bash
193
+ twine upload --repository testpypi dist/*
194
+ pip install --index-url https://test.pypi.org/simple/ devpulse-cli
195
+ ```
196
+
197
+ ### 5. Publish to PyPI
198
+ ```bash
199
+ twine upload dist/*
200
+ ```
201
+
202
+ ---
203
+
204
+ ## 🤝 Contributing
205
+
206
+ 1. Fork the repo
207
+ 2. Create a feature branch: `git checkout -b feat/your-feature`
208
+ 3. Commit your changes: `git commit -m "feat: add your feature"`
209
+ 4. Push and open a Pull Request
210
+
211
+ ---
212
+
213
+ ## 📄 License
214
+
215
+ MIT © [Your Name](https://github.com/yourname)
216
+
217
+ ---
218
+
219
+ <div align="center">
220
+ Made with ❤️ and ☕ — <em>Keep shipping!</em> 🚀
221
+ </div>
@@ -0,0 +1,13 @@
1
+ devpulse/__init__.py,sha256=yKkppoybJkwiJoXyKxkNde-oh_KBjieVXQpo8MbmUpk,207
2
+ devpulse/cli.py,sha256=TKGg2rNNuOyugjfYAhP-WQGsl7oWEKNqKO2sTTNzQYM,9783
3
+ devpulse/db.py,sha256=lM-UIqES-Q4qbC4cLMLWhx9FyTt8i7amOq2oj_JrJaU,7428
4
+ devpulse/export.py,sha256=-NdOCmFc-W_MIAMQcYIxKS2XUO86rGu4a5sJTrtcdj8,2839
5
+ devpulse/heatmap.py,sha256=uWnbXB5hAQxQSY1FT2HMsT7V43HUQeB5gd9kWVVMc8Q,3403
6
+ devpulse/quotes.py,sha256=pLfsrfk_yQ8GEREnMzIR5JOM5s2ty2TseJ63V-ag-pQ,3027
7
+ devpulse/ui.py,sha256=LcTG8otogdorIqEgnC0TE1jJGVc__hZ7gSTqSjeaSc0,8223
8
+ raj_devpulse_cli-1.0.1.dist-info/licenses/LICENSE,sha256=OphKV48tcMv6ep-7j-8T6nycykPT0g8ZlMJ9zbGvdPs,1066
9
+ raj_devpulse_cli-1.0.1.dist-info/METADATA,sha256=5Jt1R7Y5CRqa5FrT_Aa5ISNoaNpAUKxXTMY-19xi6sU,5344
10
+ raj_devpulse_cli-1.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
11
+ raj_devpulse_cli-1.0.1.dist-info/entry_points.txt,sha256=HUztpqE7DJJPJsO2zf2b6q-WtauEKQ4c_nThB0sXngc,47
12
+ raj_devpulse_cli-1.0.1.dist-info/top_level.txt,sha256=XpqTfIq2CyIy--wH624_mgbzG1Q1rDdqK1BbH9ScHkY,9
13
+ raj_devpulse_cli-1.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ devpulse = devpulse.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Your Name
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ devpulse