standup-cli-tool 0.2.0__tar.gz → 0.3.0__tar.gz

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