standup-cli-tool 0.1.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: standup-cli-tool
3
- Version: 0.1.0
3
+ Version: 0.3.0
4
4
  Summary: ⚡ Generate your daily standup from git commits — right in your terminal
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/muhtalhakhan/standup-cli
@@ -21,10 +21,12 @@ Description-Content-Type: text/markdown
21
21
 
22
22
  Never manually write a standup again. `standup-cli` scans your git commits from the last 24 hours, asks what you're working on today and if you have blockers, then formats a clean standup message ready to paste anywhere.
23
23
 
24
- ```
24
+ ![standup-cli demo](https://github.com/muhtalhakhan/standup-cli/raw/main/standup-cli.gif)
25
+
26
+ ```bash
25
27
  $ standup
26
28
 
27
- ⚡ standup-cli
29
+ ⚡ standup-cli-tool
28
30
  Generate your daily standup in seconds
29
31
 
30
32
  🔍 Scanning git commits from last 24hrs...
@@ -53,20 +55,22 @@ $ standup
53
55
 
54
56
  ## Install
55
57
 
56
- **via npm:**
58
+ **via npm**:
59
+
57
60
  ```bash
58
- npm install -g standup-cli
61
+ npm install -g standup-cli-tool
59
62
  ```
60
63
 
61
- **via pip:**
64
+ **via pip**:
65
+
62
66
  ```bash
63
- pip install standup-cli
67
+ pip install standup-cli-tool
64
68
  ```
65
69
 
66
70
  ## Usage
67
71
 
68
72
  ```bash
69
- # Default (plain text output)
73
+ # Default (plain output, current repo, clipboard on)
70
74
  standup
71
75
 
72
76
  # Slack-ready output
@@ -74,14 +78,71 @@ standup --format slack
74
78
 
75
79
  # Markdown output
76
80
  standup --format markdown
81
+
82
+ # Team label
83
+ standup --team "Platform"
84
+
85
+ # Disable auto-copy
86
+ standup --no-copy
87
+
88
+ # Scan multiple repositories
89
+ standup --repo . --repo ../another-repo
90
+
91
+ # Weekly summary mode
92
+ standup --weekly
77
93
  ```
78
94
 
79
- ## Output Formats
95
+ ## What It Includes
96
+
97
+ - Conventional Commit parsing (`feat`, `fix`, `docs`, etc.) into grouped sections
98
+ - Files changed count per repository (last 24h or weekly window)
99
+ - Output grouped by repository
100
+ - Clipboard auto-copy by default
101
+ - `.standuprc` support for defaults
102
+ - Weekly summaries with `--weekly`
103
+
104
+ ## .standuprc
80
105
 
81
- **Plain** (default) paste anywhere:
106
+ Place `.standuprc` in the current project or your home directory.
107
+
108
+ JSON format:
109
+
110
+ ```json
111
+ {
112
+ "format": "slack",
113
+ "team": "Platform",
114
+ "copy": true,
115
+ "weekly": false,
116
+ "repos": [".", "../service-api"]
117
+ }
118
+ ```
119
+
120
+ Key-value format is also supported:
121
+
122
+ ```ini
123
+ format=plain
124
+ team=Platform
125
+ copy=true
126
+ weekly=false
127
+ repos=.,../service-api
82
128
  ```
83
- Yesterday: Fixed auth bug, updated docs
84
- Today: Stripe integration
129
+
130
+ ## Output Example (plain)
131
+
132
+ ```text
133
+ Team: Platform
134
+ Yesterday:
135
+ standup-cli (3 commits, 9 files changed):
136
+ Features:
137
+ - Add repo grouping support
138
+ Fixes:
139
+ - Handle empty commit logs
140
+
141
+ service-api (2 commits, 4 files changed):
142
+ Docs:
143
+ - Update API usage notes
144
+
145
+ Today: Finish release checks
85
146
  Blockers: None
86
147
  ```
87
148
 
@@ -112,14 +173,16 @@ None
112
173
  2. Prompts you for today's focus and any blockers
113
174
  3. Formats and prints your standup
114
175
 
176
+ Use `--weekly` to scan commits from the last 7 days and label the summary as `Last week` / `Next`.
177
+
115
178
  > **Tip:** Run it from your project root for best results. Works with any git repo.
116
179
 
117
180
  ## Roadmap (v1 ideas)
118
181
 
119
- - [ ] Copy to clipboard automatically
120
- - [ ] Support multiple repos
121
- - [ ] `.standuprc` config file for team name, format preference
122
- - [ ] Weekly summary mode
182
+ - [x] Copy to clipboard automatically
183
+ - [x] Support multiple repos
184
+ - [x] `.standuprc` config file for team name, format preference
185
+ - [x] Weekly summary mode
123
186
 
124
187
  ## License
125
188
 
@@ -2,12 +2,14 @@
2
2
 
3
3
  > Generate your daily standup from git commits — right in your terminal.
4
4
 
5
- Never manually write a standup again. `standup-cli` scans your git commits from the last 24 hours, asks what you're working on today and if you have blockers, then formats a clean standup message ready to paste anywhere.
6
-
7
- ```
8
- $ standup
9
-
10
- standup-cli
5
+ Never manually write a standup again. `standup-cli` scans your git commits from the last 24 hours, asks what you're working on today and if you have blockers, then formats a clean standup message ready to paste anywhere.
6
+
7
+ ![standup-cli demo](https://github.com/muhtalhakhan/standup-cli/raw/main/standup-cli.gif)
8
+
9
+ ```bash
10
+ $ standup
11
+
12
+ ⚡ standup-cli-tool
11
13
  Generate your daily standup in seconds
12
14
 
13
15
  🔍 Scanning git commits from last 24hrs...
@@ -36,20 +38,22 @@ $ standup
36
38
 
37
39
  ## Install
38
40
 
39
- **via npm:**
41
+ **via npm**:
42
+
40
43
  ```bash
41
- npm install -g standup-cli
44
+ npm install -g standup-cli-tool
42
45
  ```
43
46
 
44
- **via pip:**
47
+ **via pip**:
48
+
45
49
  ```bash
46
- pip install standup-cli
50
+ pip install standup-cli-tool
47
51
  ```
48
52
 
49
53
  ## Usage
50
54
 
51
55
  ```bash
52
- # Default (plain text output)
56
+ # Default (plain output, current repo, clipboard on)
53
57
  standup
54
58
 
55
59
  # Slack-ready output
@@ -57,14 +61,71 @@ standup --format slack
57
61
 
58
62
  # Markdown output
59
63
  standup --format markdown
64
+
65
+ # Team label
66
+ standup --team "Platform"
67
+
68
+ # Disable auto-copy
69
+ standup --no-copy
70
+
71
+ # Scan multiple repositories
72
+ standup --repo . --repo ../another-repo
73
+
74
+ # Weekly summary mode
75
+ standup --weekly
76
+ ```
77
+
78
+ ## What It Includes
79
+
80
+ - Conventional Commit parsing (`feat`, `fix`, `docs`, etc.) into grouped sections
81
+ - Files changed count per repository (last 24h or weekly window)
82
+ - Output grouped by repository
83
+ - Clipboard auto-copy by default
84
+ - `.standuprc` support for defaults
85
+ - Weekly summaries with `--weekly`
86
+
87
+ ## .standuprc
88
+
89
+ Place `.standuprc` in the current project or your home directory.
90
+
91
+ JSON format:
92
+
93
+ ```json
94
+ {
95
+ "format": "slack",
96
+ "team": "Platform",
97
+ "copy": true,
98
+ "weekly": false,
99
+ "repos": [".", "../service-api"]
100
+ }
60
101
  ```
61
102
 
62
- ## Output Formats
103
+ Key-value format is also supported:
63
104
 
64
- **Plain** (default) — paste anywhere:
105
+ ```ini
106
+ format=plain
107
+ team=Platform
108
+ copy=true
109
+ weekly=false
110
+ repos=.,../service-api
65
111
  ```
66
- Yesterday: Fixed auth bug, updated docs
67
- Today: Stripe integration
112
+
113
+ ## Output Example (plain)
114
+
115
+ ```text
116
+ Team: Platform
117
+ Yesterday:
118
+ standup-cli (3 commits, 9 files changed):
119
+ Features:
120
+ - Add repo grouping support
121
+ Fixes:
122
+ - Handle empty commit logs
123
+
124
+ service-api (2 commits, 4 files changed):
125
+ Docs:
126
+ - Update API usage notes
127
+
128
+ Today: Finish release checks
68
129
  Blockers: None
69
130
  ```
70
131
 
@@ -91,19 +152,21 @@ None
91
152
 
92
153
  ## How it works
93
154
 
94
- 1. Runs `git log --since="24 hours ago"` in your current directory
95
- 2. Prompts you for today's focus and any blockers
96
- 3. Formats and prints your standup
97
-
98
- > **Tip:** Run it from your project root for best results. Works with any git repo.
155
+ 1. Runs `git log --since="24 hours ago"` in your current directory
156
+ 2. Prompts you for today's focus and any blockers
157
+ 3. Formats and prints your standup
158
+
159
+ Use `--weekly` to scan commits from the last 7 days and label the summary as `Last week` / `Next`.
160
+
161
+ > **Tip:** Run it from your project root for best results. Works with any git repo.
99
162
 
100
163
  ## Roadmap (v1 ideas)
101
164
 
102
- - [ ] Copy to clipboard automatically
103
- - [ ] Support multiple repos
104
- - [ ] `.standuprc` config file for team name, format preference
105
- - [ ] Weekly summary mode
165
+ - [x] Copy to clipboard automatically
166
+ - [x] Support multiple repos
167
+ - [x] `.standuprc` config file for team name, format preference
168
+ - [x] Weekly summary mode
106
169
 
107
170
  ## License
108
171
 
109
- MIT © [Muhammad Talha Khan](https://github.com/muhtalhakhan)
172
+ MIT © [Muhammad Talha Khan](https://github.com/muhtalhakhan)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "standup-cli-tool"
7
- version = "0.1.0"
7
+ version = "0.3.0"
8
8
  description = "⚡ Generate your daily standup from git commits — right in your terminal"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -24,4 +24,4 @@ standup = "standup_cli.main:main"
24
24
  [project.urls]
25
25
  Homepage = "https://github.com/muhtalhakhan/standup-cli"
26
26
  Repository = "https://github.com/muhtalhakhan/standup-cli"
27
- Issues = "https://github.com/muhtalhakhan/standup-cli/issues"
27
+ Issues = "https://github.com/muhtalhakhan/standup-cli/issues"
@@ -0,0 +1,459 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ import re
7
+ import shutil
8
+ import subprocess
9
+ import sys
10
+
11
+ # ANSI colors
12
+ RESET = "\x1b[0m"
13
+ BOLD = "\x1b[1m"
14
+ DIM = "\x1b[2m"
15
+ CYAN = "\x1b[36m"
16
+ GREEN = "\x1b[32m"
17
+ YELLOW = "\x1b[33m"
18
+ MAGENTA = "\x1b[35m"
19
+ GRAY = "\x1b[90m"
20
+
21
+ TYPE_ORDER = [
22
+ "feat",
23
+ "fix",
24
+ "refactor",
25
+ "perf",
26
+ "docs",
27
+ "test",
28
+ "chore",
29
+ "build",
30
+ "ci",
31
+ "style",
32
+ "other",
33
+ ]
34
+
35
+ TYPE_LABELS = {
36
+ "feat": "Features",
37
+ "fix": "Fixes",
38
+ "refactor": "Refactors",
39
+ "perf": "Performance",
40
+ "docs": "Docs",
41
+ "test": "Tests",
42
+ "chore": "Chores",
43
+ "build": "Build",
44
+ "ci": "CI",
45
+ "style": "Style",
46
+ "other": "Other",
47
+ }
48
+
49
+
50
+ def paint(color, text):
51
+ return f"{color}{text}{RESET}"
52
+
53
+
54
+ def parse_bool(value, default):
55
+ if value is None:
56
+ return default
57
+ if isinstance(value, bool):
58
+ return value
59
+ raw = str(value).strip().lower()
60
+ if raw in ("1", "true", "yes", "on"):
61
+ return True
62
+ if raw in ("0", "false", "no", "off"):
63
+ return False
64
+ return default
65
+
66
+
67
+ def normalize_repos(value):
68
+ if value is None:
69
+ return []
70
+ if isinstance(value, str):
71
+ return [v.strip() for v in value.split(",") if v.strip()]
72
+ if isinstance(value, list):
73
+ return [str(v).strip() for v in value if str(v).strip()]
74
+ return []
75
+
76
+
77
+ def load_config():
78
+ paths = [
79
+ os.path.join(os.getcwd(), ".standuprc"),
80
+ os.path.join(os.path.expanduser("~"), ".standuprc"),
81
+ ]
82
+ for path in paths:
83
+ if not os.path.isfile(path):
84
+ continue
85
+ try:
86
+ with open(path, "r", encoding="utf-8") as handle:
87
+ raw = handle.read().strip()
88
+ if not raw:
89
+ return {}
90
+ if raw.lstrip().startswith("{"):
91
+ data = json.loads(raw)
92
+ else:
93
+ data = {}
94
+ for line in raw.splitlines():
95
+ line = line.strip()
96
+ if not line or line.startswith("#") or "=" not in line:
97
+ continue
98
+ key, value = line.split("=", 1)
99
+ data[key.strip()] = value.strip()
100
+ config = {}
101
+ config["format"] = data.get("format")
102
+ config["team"] = data.get("team")
103
+ config["copy"] = parse_bool(data.get("copy"), True)
104
+ config["weekly"] = parse_bool(data.get("weekly"), False)
105
+ if "no_copy" in data:
106
+ config["copy"] = not parse_bool(data.get("no_copy"), False)
107
+ config["repos"] = normalize_repos(data.get("repos"))
108
+ return config
109
+ except Exception:
110
+ return {}
111
+ return {}
112
+
113
+
114
+ def parse_commit_subject(subject):
115
+ subject = subject.strip()
116
+ match = re.match(
117
+ r"^(?P<type>[a-zA-Z]+)(\((?P<scope>[^)]+)\))?(?P<breaking>!)?:\s*(?P<msg>.+)$",
118
+ subject,
119
+ )
120
+ if match:
121
+ ctype = match.group("type").lower()
122
+ scope = match.group("scope")
123
+ msg = match.group("msg").strip()
124
+ else:
125
+ ctype = "other"
126
+ scope = None
127
+ msg = subject
128
+ msg = msg.rstrip(".")
129
+ if msg:
130
+ msg = msg[0].upper() + msg[1:]
131
+ if scope:
132
+ msg = f"{scope}: {msg}"
133
+ return {"type": ctype, "message": msg}
134
+
135
+
136
+ def parse_git_log_with_numstat(raw):
137
+ commits = []
138
+ current = None
139
+ for line in raw.splitlines():
140
+ if line.startswith("__COMMIT__\x1f"):
141
+ if current is not None:
142
+ commits.append(current)
143
+ subject = line.split("\x1f", 1)[1].strip()
144
+ parsed = parse_commit_subject(subject)
145
+ current = {
146
+ "type": parsed["type"],
147
+ "message": parsed["message"],
148
+ "files_changed": 0,
149
+ }
150
+ continue
151
+
152
+ if not line.strip():
153
+ continue
154
+
155
+ if current is None:
156
+ continue
157
+
158
+ parts = line.split("\t")
159
+ if len(parts) >= 3:
160
+ current["files_changed"] += 1
161
+
162
+ if current is not None:
163
+ commits.append(current)
164
+
165
+ return commits
166
+
167
+
168
+ def get_repo_summary(repo_path, since):
169
+ abs_path = os.path.abspath(repo_path)
170
+ repo_name = os.path.basename(abs_path.rstrip("\\/")) or abs_path
171
+ try:
172
+ result = subprocess.run(
173
+ [
174
+ "git",
175
+ "-C",
176
+ abs_path,
177
+ "log",
178
+ f"--since={since}",
179
+ "--no-merges",
180
+ "--pretty=format:__COMMIT__%x1f%s",
181
+ "--numstat",
182
+ ],
183
+ capture_output=True,
184
+ text=True,
185
+ )
186
+ if result.returncode != 0:
187
+ stderr = (result.stderr or "") + "\n" + (result.stdout or "")
188
+ if "detected dubious ownership in repository" in stderr.lower():
189
+ return {
190
+ "error": "dubious_ownership",
191
+ "path": abs_path,
192
+ "fix": f'git config --global --add safe.directory "{abs_path}"',
193
+ }
194
+ return None
195
+ commits = parse_git_log_with_numstat(result.stdout.strip())
196
+ return {
197
+ "name": repo_name,
198
+ "path": abs_path,
199
+ "commits": commits,
200
+ "commit_count": len(commits),
201
+ "files_changed": sum(c["files_changed"] for c in commits),
202
+ }
203
+ except FileNotFoundError:
204
+ return None
205
+
206
+
207
+ def summarize_commits(commits, empty_message):
208
+ if not commits:
209
+ return [empty_message]
210
+
211
+ seen = set()
212
+ grouped = {k: [] for k in TYPE_ORDER}
213
+
214
+ for commit in commits:
215
+ message = commit["message"]
216
+ key = message.lower()
217
+ if key in seen:
218
+ continue
219
+ seen.add(key)
220
+ ctype = commit["type"] if commit["type"] in grouped else "other"
221
+ grouped[ctype].append(message)
222
+
223
+ lines = []
224
+ for ctype in TYPE_ORDER:
225
+ items = grouped[ctype]
226
+ if not items:
227
+ continue
228
+ lines.append(f"{TYPE_LABELS.get(ctype, 'Other')}:")
229
+ for item in items:
230
+ lines.append(f"- {item}")
231
+ return lines
232
+
233
+
234
+ def format_output(repo_summaries, today, blockers, fmt, mode, team=None):
235
+ repo_lines = []
236
+ if not repo_summaries:
237
+ repo_lines.append("No repositories scanned")
238
+ for repo in repo_summaries:
239
+ repo_lines.append(
240
+ f"{repo['name']} ({repo['commit_count']} commits, {repo['files_changed']} files changed):"
241
+ )
242
+ repo_lines.extend(summarize_commits(repo["commits"], mode["empty_message"]))
243
+ repo_lines.append("")
244
+ if repo_lines and repo_lines[-1] == "":
245
+ repo_lines.pop()
246
+
247
+ if fmt == "slack":
248
+ lines = []
249
+ if team:
250
+ lines.append(f"*Team:* {team}")
251
+ lines.append(f"*{mode['history_label']}:*")
252
+ for line in repo_lines:
253
+ if not line:
254
+ lines.append("")
255
+ elif line.endswith(":"):
256
+ lines.append(f"*{line}*")
257
+ else:
258
+ lines.append(f"- {line[2:]}" if line.startswith("- ") else f"- {line}")
259
+ lines.append(f"*{mode['next_label']}:* {today}")
260
+ lines.append(f"*Blockers:* {blockers or 'None'}")
261
+ return "\n".join(lines)
262
+
263
+ if fmt == "markdown":
264
+ lines = [f"### {mode['title']}", ""]
265
+ if team:
266
+ lines.extend(["**Team:**", team, ""])
267
+ lines.append(f"**{mode['history_label']}:**")
268
+ for line in repo_lines:
269
+ lines.append(line)
270
+ lines.extend(
271
+ ["", f"**{mode['next_label']}:**", today, "", "**Blockers:**", blockers or "None"]
272
+ )
273
+ return "\n".join(lines)
274
+
275
+ lines = []
276
+ if team:
277
+ lines.append(f"Team: {team}")
278
+ lines.append(f"{mode['history_label']}:")
279
+ lines.extend(repo_lines)
280
+ lines.append(f"{mode['next_label']}: {today}")
281
+ lines.append(f"Blockers: {blockers or 'None'}")
282
+ return "\n".join(lines)
283
+
284
+
285
+ def copy_to_clipboard(text):
286
+ if not text:
287
+ return False
288
+ try:
289
+ if sys.platform.startswith("win"):
290
+ subprocess.run(["clip"], input=text, text=True, check=True)
291
+ return True
292
+ if sys.platform == "darwin" and shutil.which("pbcopy"):
293
+ subprocess.run(["pbcopy"], input=text, text=True, check=True)
294
+ return True
295
+ if shutil.which("xclip"):
296
+ subprocess.run(
297
+ ["xclip", "-selection", "clipboard"],
298
+ input=text,
299
+ text=True,
300
+ check=True,
301
+ )
302
+ return True
303
+ if shutil.which("xsel"):
304
+ subprocess.run(
305
+ ["xsel", "--clipboard", "--input"], input=text, text=True, check=True
306
+ )
307
+ return True
308
+ except Exception:
309
+ return False
310
+ return False
311
+
312
+
313
+ def collect_repo_paths(config, cli_repos):
314
+ source = cli_repos if cli_repos else config.get("repos") or [os.getcwd()]
315
+ seen = set()
316
+ ordered = []
317
+ for repo in source:
318
+ abs_path = os.path.abspath(repo)
319
+ key = abs_path.lower()
320
+ if key in seen:
321
+ continue
322
+ seen.add(key)
323
+ ordered.append(abs_path)
324
+ return ordered
325
+
326
+
327
+ def main():
328
+ config = load_config()
329
+
330
+ parser = argparse.ArgumentParser(
331
+ prog="standup", description="Generate your daily standup from git commits"
332
+ )
333
+ parser.add_argument(
334
+ "--format",
335
+ "-f",
336
+ choices=["plain", "slack", "markdown"],
337
+ default=None,
338
+ help="Output format (default: plain)",
339
+ )
340
+ parser.add_argument("--team", "-t", default=None, help="Team name for standup header")
341
+ parser.add_argument(
342
+ "--repo",
343
+ action="append",
344
+ default=[],
345
+ help="Repository path to scan (repeatable). Defaults to cwd or .standuprc repos.",
346
+ )
347
+ parser.add_argument(
348
+ "--weekly",
349
+ action="store_true",
350
+ help="Scan the last 7 days and format a weekly summary",
351
+ )
352
+ parser.add_argument("--no-copy", action="store_true", help="Disable clipboard auto-copy")
353
+ args = parser.parse_args()
354
+
355
+ fmt = args.format or config.get("format") or "plain"
356
+ team = args.team or config.get("team")
357
+ copy_enabled = (not args.no_copy) and config.get("copy", True)
358
+ repo_paths = collect_repo_paths(config, args.repo)
359
+ weekly = args.weekly or bool(config.get("weekly"))
360
+ mode = (
361
+ {
362
+ "title": "Weekly Standup",
363
+ "since": "7 days ago",
364
+ "scan_label": "last 7 days",
365
+ "history_label": "Last week",
366
+ "next_label": "Next",
367
+ "prompt": " What are you working on next?\n ",
368
+ "empty_message": "No commits in the last 7 days",
369
+ }
370
+ if weekly
371
+ else {
372
+ "title": "Daily Standup",
373
+ "since": "24 hours ago",
374
+ "scan_label": "last 24hrs",
375
+ "history_label": "Yesterday",
376
+ "next_label": "Today",
377
+ "prompt": " What are you working on today?\n ",
378
+ "empty_message": "No commits in the last 24 hours",
379
+ }
380
+ )
381
+
382
+ print()
383
+ print(paint(BOLD + CYAN, " standup-cli"))
384
+ print(paint(GRAY, " Generate your daily standup in seconds\n"))
385
+
386
+ print(paint(DIM, f" Scanning git commits from {mode['scan_label']}..."))
387
+ repo_summaries = []
388
+ skipped = []
389
+ dubious = []
390
+ for repo_path in repo_paths:
391
+ summary = get_repo_summary(repo_path, mode["since"])
392
+ if summary is None:
393
+ skipped.append(repo_path)
394
+ continue
395
+ if "error" in summary:
396
+ if summary["error"] == "dubious_ownership":
397
+ dubious.append(summary)
398
+ else:
399
+ skipped.append(repo_path)
400
+ continue
401
+ repo_summaries.append(summary)
402
+ print(
403
+ paint(
404
+ GREEN,
405
+ f" {summary['name']}: {summary['commit_count']} commit(s), "
406
+ f"{summary['files_changed']} file(s) changed",
407
+ )
408
+ )
409
+
410
+ for issue in dubious:
411
+ print(
412
+ paint(
413
+ YELLOW,
414
+ f" Warning: Git safe.directory blocked repo {issue['path']}",
415
+ )
416
+ )
417
+ print(paint(YELLOW, f" Run: {issue['fix']}"))
418
+ if skipped:
419
+ for repo_path in skipped:
420
+ print(paint(YELLOW, f" Warning: skipped non-git repo {repo_path}"))
421
+ print()
422
+
423
+ today = input(paint(BOLD, mode["prompt"]) + "> ").strip()
424
+ print()
425
+ blockers = input(paint(BOLD, ' Any blockers? (press Enter for "None")\n ') + "> ").strip()
426
+ print()
427
+
428
+ output = format_output(
429
+ repo_summaries,
430
+ today or "(not specified)",
431
+ blockers,
432
+ fmt,
433
+ mode,
434
+ team=team,
435
+ )
436
+
437
+ divider = paint(GRAY, " " + "-" * 50)
438
+ fmt_label = paint(MAGENTA, f"[{fmt}]")
439
+
440
+ print(divider)
441
+ print(paint(BOLD + GREEN, f" Your Standup {fmt_label}\n"))
442
+ for line in output.split("\n"):
443
+ print(" " + line)
444
+ print()
445
+ print(divider)
446
+ print()
447
+ print(paint(GRAY, " Tip: use --format slack | markdown | plain"))
448
+ print()
449
+
450
+ if copy_enabled:
451
+ if copy_to_clipboard(output):
452
+ print(paint(GREEN, " Copied standup to clipboard"))
453
+ else:
454
+ print(paint(YELLOW, " Warning: clipboard copy unavailable"))
455
+ print()
456
+
457
+
458
+ if __name__ == "__main__":
459
+ main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: standup-cli-tool
3
- Version: 0.1.0
3
+ Version: 0.3.0
4
4
  Summary: ⚡ Generate your daily standup from git commits — right in your terminal
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/muhtalhakhan/standup-cli
@@ -21,10 +21,12 @@ Description-Content-Type: text/markdown
21
21
 
22
22
  Never manually write a standup again. `standup-cli` scans your git commits from the last 24 hours, asks what you're working on today and if you have blockers, then formats a clean standup message ready to paste anywhere.
23
23
 
24
- ```
24
+ ![standup-cli demo](https://github.com/muhtalhakhan/standup-cli/raw/main/standup-cli.gif)
25
+
26
+ ```bash
25
27
  $ standup
26
28
 
27
- ⚡ standup-cli
29
+ ⚡ standup-cli-tool
28
30
  Generate your daily standup in seconds
29
31
 
30
32
  🔍 Scanning git commits from last 24hrs...
@@ -53,20 +55,22 @@ $ standup
53
55
 
54
56
  ## Install
55
57
 
56
- **via npm:**
58
+ **via npm**:
59
+
57
60
  ```bash
58
- npm install -g standup-cli
61
+ npm install -g standup-cli-tool
59
62
  ```
60
63
 
61
- **via pip:**
64
+ **via pip**:
65
+
62
66
  ```bash
63
- pip install standup-cli
67
+ pip install standup-cli-tool
64
68
  ```
65
69
 
66
70
  ## Usage
67
71
 
68
72
  ```bash
69
- # Default (plain text output)
73
+ # Default (plain output, current repo, clipboard on)
70
74
  standup
71
75
 
72
76
  # Slack-ready output
@@ -74,14 +78,71 @@ standup --format slack
74
78
 
75
79
  # Markdown output
76
80
  standup --format markdown
81
+
82
+ # Team label
83
+ standup --team "Platform"
84
+
85
+ # Disable auto-copy
86
+ standup --no-copy
87
+
88
+ # Scan multiple repositories
89
+ standup --repo . --repo ../another-repo
90
+
91
+ # Weekly summary mode
92
+ standup --weekly
77
93
  ```
78
94
 
79
- ## Output Formats
95
+ ## What It Includes
96
+
97
+ - Conventional Commit parsing (`feat`, `fix`, `docs`, etc.) into grouped sections
98
+ - Files changed count per repository (last 24h or weekly window)
99
+ - Output grouped by repository
100
+ - Clipboard auto-copy by default
101
+ - `.standuprc` support for defaults
102
+ - Weekly summaries with `--weekly`
103
+
104
+ ## .standuprc
80
105
 
81
- **Plain** (default) paste anywhere:
106
+ Place `.standuprc` in the current project or your home directory.
107
+
108
+ JSON format:
109
+
110
+ ```json
111
+ {
112
+ "format": "slack",
113
+ "team": "Platform",
114
+ "copy": true,
115
+ "weekly": false,
116
+ "repos": [".", "../service-api"]
117
+ }
118
+ ```
119
+
120
+ Key-value format is also supported:
121
+
122
+ ```ini
123
+ format=plain
124
+ team=Platform
125
+ copy=true
126
+ weekly=false
127
+ repos=.,../service-api
82
128
  ```
83
- Yesterday: Fixed auth bug, updated docs
84
- Today: Stripe integration
129
+
130
+ ## Output Example (plain)
131
+
132
+ ```text
133
+ Team: Platform
134
+ Yesterday:
135
+ standup-cli (3 commits, 9 files changed):
136
+ Features:
137
+ - Add repo grouping support
138
+ Fixes:
139
+ - Handle empty commit logs
140
+
141
+ service-api (2 commits, 4 files changed):
142
+ Docs:
143
+ - Update API usage notes
144
+
145
+ Today: Finish release checks
85
146
  Blockers: None
86
147
  ```
87
148
 
@@ -112,14 +173,16 @@ None
112
173
  2. Prompts you for today's focus and any blockers
113
174
  3. Formats and prints your standup
114
175
 
176
+ Use `--weekly` to scan commits from the last 7 days and label the summary as `Last week` / `Next`.
177
+
115
178
  > **Tip:** Run it from your project root for best results. Works with any git repo.
116
179
 
117
180
  ## Roadmap (v1 ideas)
118
181
 
119
- - [ ] Copy to clipboard automatically
120
- - [ ] Support multiple repos
121
- - [ ] `.standuprc` config file for team name, format preference
122
- - [ ] Weekly summary mode
182
+ - [x] Copy to clipboard automatically
183
+ - [x] Support multiple repos
184
+ - [x] `.standuprc` config file for team name, format preference
185
+ - [x] Weekly summary mode
123
186
 
124
187
  ## License
125
188
 
@@ -1,117 +0,0 @@
1
- #!/usr/bin/env python3
2
-
3
- import subprocess
4
- import sys
5
- import argparse
6
-
7
- # ANSI colors
8
- RESET = "\x1b[0m"
9
- BOLD = "\x1b[1m"
10
- DIM = "\x1b[2m"
11
- CYAN = "\x1b[36m"
12
- GREEN = "\x1b[32m"
13
- YELLOW = "\x1b[33m"
14
- MAGENTA= "\x1b[35m"
15
- GRAY = "\x1b[90m"
16
-
17
- def paint(color, text):
18
- return f"{color}{text}{RESET}"
19
-
20
- def get_git_commits():
21
- try:
22
- result = subprocess.run(
23
- ["git", "log", '--since=24 hours ago', '--pretty=format:%s', '--no-merges'],
24
- capture_output=True, text=True
25
- )
26
- if result.returncode != 0:
27
- return None
28
- lines = [l.strip() for l in result.stdout.strip().split('\n') if l.strip()]
29
- return lines
30
- except FileNotFoundError:
31
- return None
32
-
33
- def format_output(commits, today, blockers, fmt):
34
- yesterday = ', '.join(
35
- c[0].upper() + c[1:] for c in commits
36
- ) if commits else 'No commits in the last 24 hours'
37
-
38
- if fmt == 'slack':
39
- return '\n'.join([
40
- f"*📋 Yesterday:* {yesterday}",
41
- f"*🚀 Today:* {today}",
42
- f"*🚧 Blockers:* {blockers or 'None'}",
43
- ])
44
- elif fmt == 'markdown':
45
- return '\n'.join([
46
- "### Daily Standup",
47
- "",
48
- "**Yesterday:**",
49
- yesterday,
50
- "",
51
- "**Today:**",
52
- today,
53
- "",
54
- "**Blockers:**",
55
- blockers or 'None',
56
- ])
57
- else: # plain
58
- return '\n'.join([
59
- f"Yesterday: {yesterday}",
60
- f"Today: {today}",
61
- f"Blockers: {blockers or 'None'}",
62
- ])
63
-
64
- def main():
65
- parser = argparse.ArgumentParser(
66
- prog='standup',
67
- description='⚡ Generate your daily standup from git commits'
68
- )
69
- parser.add_argument(
70
- '--format', '-f',
71
- choices=['plain', 'slack', 'markdown'],
72
- default='plain',
73
- help='Output format (default: plain)'
74
- )
75
- args = parser.parse_args()
76
- fmt = args.format
77
-
78
- print()
79
- print(paint(BOLD + CYAN, ' ⚡ standup-cli'))
80
- print(paint(GRAY, ' Generate your daily standup in seconds\n'))
81
-
82
- print(paint(DIM, ' 🔍 Scanning git commits from last 24hrs...'))
83
- commits = get_git_commits()
84
-
85
- if commits is None:
86
- print(paint(YELLOW, ' ⚠️ Not a git repo — skipping commit scan.\n'))
87
- commits = []
88
- elif len(commits) == 0:
89
- print(paint(YELLOW, ' ⚠️ No commits found in the last 24hrs.\n'))
90
- else:
91
- print(paint(GREEN, f' ✅ Found {len(commits)} commit(s):\n'))
92
- for msg in commits:
93
- print(paint(GRAY, f' • {msg}'))
94
- print()
95
-
96
- today = input(paint(BOLD, ' 🚀 What are you working on today?\n ') + '> ').strip()
97
- print()
98
- blockers = input(paint(BOLD, ' 🚧 Any blockers? (press Enter for "None")\n ') + '> ').strip()
99
- print()
100
-
101
- output = format_output(commits, today or '(not specified)', blockers, fmt)
102
-
103
- divider = paint(GRAY, ' ' + '─' * 50)
104
- fmt_label = paint(MAGENTA, f'[{fmt}]')
105
-
106
- print(divider)
107
- print(paint(BOLD + GREEN, f' ✅ Your Standup {fmt_label}\n'))
108
- for line in output.split('\n'):
109
- print(' ' + line)
110
- print()
111
- print(divider)
112
- print()
113
- print(paint(GRAY, ' 💡 Tip: use --format slack | markdown | plain'))
114
- print()
115
-
116
- if __name__ == '__main__':
117
- main()