@pennyfarthing/core 7.8.1 → 7.8.4

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.
Files changed (178) hide show
  1. package/README.md +1 -1
  2. package/package.json +2 -1
  3. package/packages/core/dist/cli/commands/init.d.ts.map +1 -1
  4. package/packages/core/dist/cli/commands/init.js +7 -6
  5. package/packages/core/dist/cli/commands/init.js.map +1 -1
  6. package/packages/core/dist/cli/utils/symlinks.d.ts +7 -0
  7. package/packages/core/dist/cli/utils/symlinks.d.ts.map +1 -1
  8. package/packages/core/dist/cli/utils/symlinks.js +25 -0
  9. package/packages/core/dist/cli/utils/symlinks.js.map +1 -1
  10. package/pennyfarthing-dist/scripts/core/phase-check-start.sh +1 -1
  11. package/pennyfarthing-dist/scripts/core/prime.sh +23 -0
  12. package/pennyfarthing-dist/scripts/core/run.sh +5 -5
  13. package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +2 -2
  14. package/pennyfarthing-dist/scripts/git/release.sh +2 -2
  15. package/pennyfarthing-dist/scripts/health/drift-detection.sh +1 -1
  16. package/pennyfarthing-dist/scripts/hooks/post-merge.sh +2 -2
  17. package/pennyfarthing-dist/scripts/hooks/pre-push.sh +2 -2
  18. package/pennyfarthing-dist/scripts/hooks/session-stop.sh +1 -1
  19. package/pennyfarthing-dist/scripts/jira/create-jira-epic.sh +1 -1
  20. package/pennyfarthing-dist/scripts/jira/create-jira-story.sh +1 -1
  21. package/pennyfarthing-dist/scripts/jira/jira-reconcile.sh +1 -1
  22. package/pennyfarthing-dist/scripts/lib/common.sh +1 -1
  23. package/pennyfarthing-dist/scripts/lib/find-root.sh +4 -4
  24. package/pennyfarthing-dist/scripts/maintenance/sidecar-health.sh +1 -1
  25. package/pennyfarthing-dist/scripts/misc/add_short_names.py +2 -2
  26. package/pennyfarthing-dist/scripts/misc/backlog.sh +2 -2
  27. package/pennyfarthing-dist/scripts/misc/deploy.sh +2 -2
  28. package/pennyfarthing-dist/scripts/misc/generate-skill-docs.sh +4 -4
  29. package/pennyfarthing-dist/scripts/misc/log-skill-usage.sh +2 -2
  30. package/pennyfarthing-dist/scripts/misc/run-ci.sh +1 -1
  31. package/pennyfarthing-dist/scripts/misc/skill-usage-report.sh +2 -2
  32. package/pennyfarthing-dist/scripts/sprint/archive-story.sh +1 -1
  33. package/pennyfarthing-dist/scripts/sprint/available-stories.sh +1 -1
  34. package/pennyfarthing-dist/scripts/sprint/check-story.sh +1 -1
  35. package/pennyfarthing-dist/scripts/sprint/get-epic-field.sh +1 -1
  36. package/pennyfarthing-dist/scripts/sprint/get-story-field.sh +1 -1
  37. package/pennyfarthing-dist/scripts/sprint/import_epic_to_future.py +2 -2
  38. package/pennyfarthing-dist/scripts/sprint/list-future.sh +1 -1
  39. package/pennyfarthing-dist/scripts/sprint/new-sprint.sh +1 -1
  40. package/pennyfarthing-dist/scripts/sprint/promote-epic.sh +1 -1
  41. package/pennyfarthing-dist/scripts/sprint/sprint-common.sh +3 -3
  42. package/pennyfarthing-dist/scripts/sprint/sprint-info.sh +1 -1
  43. package/pennyfarthing-dist/scripts/sprint/sprint-metrics.sh +2 -2
  44. package/pennyfarthing-dist/scripts/theme/compute_theme_tiers.py +2 -2
  45. package/pennyfarthing-dist/scripts/workflow/check.py +2 -2
  46. package/pennyfarthing-dist/scripts/workflow/finish-story.sh +1 -1
  47. package/pennyfarthing-dist/scripts/workflow/fix-session-phase.sh +1 -1
  48. package/pennyfarthing-dist/scripts/workflow/get-workflow-type.py +2 -2
  49. package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +1 -1
  50. package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +1 -1
  51. package/pennyfarthing-dist/scripts/workflow/resume-workflow.sh +1 -1
  52. package/pennyfarthing-dist/scripts/workflow/show-workflow.sh +1 -1
  53. package/pennyfarthing-dist/scripts/workflow/start-workflow.sh +1 -1
  54. package/pennyfarthing-dist/scripts/workflow/workflow-status.sh +1 -1
  55. package/pennyfarthing_scripts/__init__.py +17 -0
  56. package/pennyfarthing_scripts/__pycache__/__init__.cpython-311.pyc +0 -0
  57. package/pennyfarthing_scripts/__pycache__/__init__.cpython-314.pyc +0 -0
  58. package/pennyfarthing_scripts/__pycache__/config.cpython-314.pyc +0 -0
  59. package/pennyfarthing_scripts/__pycache__/jira.cpython-314.pyc +0 -0
  60. package/pennyfarthing_scripts/__pycache__/jira_epic_creation.cpython-314.pyc +0 -0
  61. package/pennyfarthing_scripts/__pycache__/jira_sync.cpython-314.pyc +0 -0
  62. package/pennyfarthing_scripts/__pycache__/jira_sync_story.cpython-314.pyc +0 -0
  63. package/pennyfarthing_scripts/__pycache__/sprint.cpython-314.pyc +0 -0
  64. package/pennyfarthing_scripts/__pycache__/workflow.cpython-311.pyc +0 -0
  65. package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
  66. package/pennyfarthing_scripts/bellmode_hook.py +154 -0
  67. package/pennyfarthing_scripts/brownfield/__init__.py +35 -0
  68. package/pennyfarthing_scripts/brownfield/__main__.py +7 -0
  69. package/pennyfarthing_scripts/brownfield/__pycache__/__init__.cpython-314.pyc +0 -0
  70. package/pennyfarthing_scripts/brownfield/__pycache__/__main__.cpython-314.pyc +0 -0
  71. package/pennyfarthing_scripts/brownfield/__pycache__/cli.cpython-314.pyc +0 -0
  72. package/pennyfarthing_scripts/brownfield/__pycache__/discover.cpython-314.pyc +0 -0
  73. package/pennyfarthing_scripts/brownfield/cli.py +131 -0
  74. package/pennyfarthing_scripts/brownfield/discover.py +753 -0
  75. package/pennyfarthing_scripts/common/__init__.py +49 -0
  76. package/pennyfarthing_scripts/common/__pycache__/__init__.cpython-314.pyc +0 -0
  77. package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
  78. package/pennyfarthing_scripts/common/__pycache__/output.cpython-314.pyc +0 -0
  79. package/pennyfarthing_scripts/common/config.py +65 -0
  80. package/pennyfarthing_scripts/common/output.py +180 -0
  81. package/pennyfarthing_scripts/config.py +21 -0
  82. package/pennyfarthing_scripts/git/__init__.py +29 -0
  83. package/pennyfarthing_scripts/git/__pycache__/__init__.cpython-314.pyc +0 -0
  84. package/pennyfarthing_scripts/git/__pycache__/create_branches.cpython-314.pyc +0 -0
  85. package/pennyfarthing_scripts/git/__pycache__/status_all.cpython-314.pyc +0 -0
  86. package/pennyfarthing_scripts/git/create_branches.py +439 -0
  87. package/pennyfarthing_scripts/git/status_all.py +310 -0
  88. package/pennyfarthing_scripts/hooks.py +455 -0
  89. package/pennyfarthing_scripts/jira/__init__.py +93 -0
  90. package/pennyfarthing_scripts/jira/__main__.py +10 -0
  91. package/pennyfarthing_scripts/jira/__pycache__/__init__.cpython-314.pyc +0 -0
  92. package/pennyfarthing_scripts/jira/__pycache__/__main__.cpython-314.pyc +0 -0
  93. package/pennyfarthing_scripts/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
  94. package/pennyfarthing_scripts/jira/__pycache__/claim.cpython-314.pyc +0 -0
  95. package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-314.pyc +0 -0
  96. package/pennyfarthing_scripts/jira/__pycache__/client.cpython-314.pyc +0 -0
  97. package/pennyfarthing_scripts/jira/__pycache__/compat.cpython-314.pyc +0 -0
  98. package/pennyfarthing_scripts/jira/__pycache__/epic.cpython-314.pyc +0 -0
  99. package/pennyfarthing_scripts/jira/__pycache__/mappings.cpython-314.pyc +0 -0
  100. package/pennyfarthing_scripts/jira/__pycache__/models.cpython-314.pyc +0 -0
  101. package/pennyfarthing_scripts/jira/__pycache__/story.cpython-314.pyc +0 -0
  102. package/pennyfarthing_scripts/jira/__pycache__/sync.cpython-314.pyc +0 -0
  103. package/pennyfarthing_scripts/jira/bidirectional.py +561 -0
  104. package/pennyfarthing_scripts/jira/claim.py +211 -0
  105. package/pennyfarthing_scripts/jira/cli.py +150 -0
  106. package/pennyfarthing_scripts/jira/client.py +613 -0
  107. package/pennyfarthing_scripts/jira/epic.py +176 -0
  108. package/pennyfarthing_scripts/jira/story.py +219 -0
  109. package/pennyfarthing_scripts/jira/sync.py +350 -0
  110. package/pennyfarthing_scripts/jira_bidirectional_sync.py +37 -0
  111. package/pennyfarthing_scripts/jira_epic_creation.py +30 -0
  112. package/pennyfarthing_scripts/jira_sync.py +36 -0
  113. package/pennyfarthing_scripts/jira_sync_story.py +30 -0
  114. package/pennyfarthing_scripts/output.py +37 -0
  115. package/pennyfarthing_scripts/preflight/__init__.py +17 -0
  116. package/pennyfarthing_scripts/preflight/__main__.py +10 -0
  117. package/pennyfarthing_scripts/preflight/__pycache__/__init__.cpython-314.pyc +0 -0
  118. package/pennyfarthing_scripts/preflight/__pycache__/__main__.cpython-314.pyc +0 -0
  119. package/pennyfarthing_scripts/preflight/__pycache__/cli.cpython-314.pyc +0 -0
  120. package/pennyfarthing_scripts/preflight/__pycache__/finish.cpython-314.pyc +0 -0
  121. package/pennyfarthing_scripts/preflight/cli.py +141 -0
  122. package/pennyfarthing_scripts/preflight/finish.py +382 -0
  123. package/pennyfarthing_scripts/pretooluse_hook.py +142 -0
  124. package/pennyfarthing_scripts/prime/__init__.py +38 -0
  125. package/pennyfarthing_scripts/prime/__main__.py +8 -0
  126. package/pennyfarthing_scripts/prime/__pycache__/__init__.cpython-314.pyc +0 -0
  127. package/pennyfarthing_scripts/prime/__pycache__/__main__.cpython-314.pyc +0 -0
  128. package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
  129. package/pennyfarthing_scripts/prime/__pycache__/loader.cpython-314.pyc +0 -0
  130. package/pennyfarthing_scripts/prime/__pycache__/models.cpython-314.pyc +0 -0
  131. package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
  132. package/pennyfarthing_scripts/prime/__pycache__/session.cpython-314.pyc +0 -0
  133. package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
  134. package/pennyfarthing_scripts/prime/cli.py +220 -0
  135. package/pennyfarthing_scripts/prime/loader.py +239 -0
  136. package/pennyfarthing_scripts/sprint/__init__.py +66 -0
  137. package/pennyfarthing_scripts/sprint/__main__.py +10 -0
  138. package/pennyfarthing_scripts/sprint/__pycache__/__init__.cpython-314.pyc +0 -0
  139. package/pennyfarthing_scripts/sprint/__pycache__/__main__.cpython-314.pyc +0 -0
  140. package/pennyfarthing_scripts/sprint/__pycache__/archive.cpython-314.pyc +0 -0
  141. package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
  142. package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
  143. package/pennyfarthing_scripts/sprint/__pycache__/status.cpython-314.pyc +0 -0
  144. package/pennyfarthing_scripts/sprint/__pycache__/validator.cpython-314.pyc +0 -0
  145. package/pennyfarthing_scripts/sprint/__pycache__/work.cpython-314.pyc +0 -0
  146. package/pennyfarthing_scripts/sprint/archive.py +108 -0
  147. package/pennyfarthing_scripts/sprint/cli.py +124 -0
  148. package/pennyfarthing_scripts/sprint/loader.py +193 -0
  149. package/pennyfarthing_scripts/sprint/status.py +122 -0
  150. package/pennyfarthing_scripts/sprint/validator.py +405 -0
  151. package/pennyfarthing_scripts/sprint/work.py +192 -0
  152. package/pennyfarthing_scripts/story/__init__.py +67 -0
  153. package/pennyfarthing_scripts/story/__main__.py +10 -0
  154. package/pennyfarthing_scripts/story/cli.py +105 -0
  155. package/pennyfarthing_scripts/story/create.py +167 -0
  156. package/pennyfarthing_scripts/story/size.py +113 -0
  157. package/pennyfarthing_scripts/story/template.py +151 -0
  158. package/pennyfarthing_scripts/swebench.py +216 -0
  159. package/pennyfarthing_scripts/tests/__init__.py +1 -0
  160. package/pennyfarthing_scripts/tests/__pycache__/__init__.cpython-314.pyc +0 -0
  161. package/pennyfarthing_scripts/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
  162. package/pennyfarthing_scripts/tests/__pycache__/test_brownfield.cpython-314-pytest-9.0.2.pyc +0 -0
  163. package/pennyfarthing_scripts/tests/__pycache__/test_git_utils.cpython-314-pytest-9.0.2.pyc +0 -0
  164. package/pennyfarthing_scripts/tests/__pycache__/test_prime.cpython-314-pytest-9.0.2.pyc +0 -0
  165. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
  166. package/pennyfarthing_scripts/tests/conftest.py +106 -0
  167. package/pennyfarthing_scripts/tests/test_brownfield.py +842 -0
  168. package/pennyfarthing_scripts/tests/test_cli_modules.py +245 -0
  169. package/pennyfarthing_scripts/tests/test_common.py +180 -0
  170. package/pennyfarthing_scripts/tests/test_git_utils.py +866 -0
  171. package/pennyfarthing_scripts/tests/test_jira_package.py +334 -0
  172. package/pennyfarthing_scripts/tests/test_package_structure.py +372 -0
  173. package/pennyfarthing_scripts/tests/test_prime.py +397 -0
  174. package/pennyfarthing_scripts/tests/test_sprint_package.py +236 -0
  175. package/pennyfarthing_scripts/tests/test_sprint_validator.py +675 -0
  176. package/pennyfarthing_scripts/tests/test_story_package.py +156 -0
  177. package/pennyfarthing_scripts/welcome_hook.py +157 -0
  178. package/pennyfarthing_scripts/workflow.py +183 -0
@@ -0,0 +1,310 @@
1
+ """
2
+ Git status for all repos - async parallel execution.
3
+
4
+ Story: MSSCI-12402 - Port git utility scripts to Python
5
+
6
+ Replaces: pennyfarthing-dist/scripts/git/git-status-all.sh
7
+
8
+ Features:
9
+ - asyncio.gather for true parallel git operations
10
+ - Structured RepoStatus dataclass for programmatic access
11
+ - Both brief and full output formatting
12
+ - Cross-platform compatible
13
+ """
14
+
15
+ import asyncio
16
+ from dataclasses import dataclass
17
+ from pathlib import Path
18
+ from typing import Sequence
19
+
20
+
21
+ @dataclass
22
+ class RepoStatus:
23
+ """Status information for a single repository."""
24
+
25
+ name: str
26
+ path: Path
27
+ branch: str
28
+ changes: list[str] # List of changed files (git status --short lines)
29
+ unpushed_commits: list[str] # List of unpushed commit messages
30
+ error: str | None = None # Error message if status check failed
31
+
32
+ @property
33
+ def is_clean(self) -> bool:
34
+ """Return True if repo has no uncommitted changes."""
35
+ return len(self.changes) == 0
36
+
37
+ @property
38
+ def has_unpushed(self) -> bool:
39
+ """Return True if repo has unpushed commits."""
40
+ return len(self.unpushed_commits) > 0
41
+
42
+
43
+ async def _run_git_command(args: list[str], cwd: Path) -> tuple[str, str, int]:
44
+ """Run a git command asynchronously.
45
+
46
+ Args:
47
+ args: Git command arguments (without 'git')
48
+ cwd: Working directory for the command
49
+
50
+ Returns:
51
+ Tuple of (stdout, stderr, return_code)
52
+ """
53
+ proc = await asyncio.create_subprocess_exec(
54
+ "git",
55
+ *args,
56
+ cwd=cwd,
57
+ stdout=asyncio.subprocess.PIPE,
58
+ stderr=asyncio.subprocess.PIPE,
59
+ )
60
+ stdout, stderr = await proc.communicate()
61
+ return (
62
+ stdout.decode("utf-8", errors="replace").strip(),
63
+ stderr.decode("utf-8", errors="replace").strip(),
64
+ proc.returncode or 0,
65
+ )
66
+
67
+
68
+ async def get_repo_status(name: str, path: Path) -> RepoStatus:
69
+ """Get git status for a single repository.
70
+
71
+ Args:
72
+ name: Display name for the repo
73
+ path: Path to the repository
74
+
75
+ Returns:
76
+ RepoStatus with current branch, changes, and unpushed commits
77
+ """
78
+ # Check if path exists
79
+ if not path.exists():
80
+ return RepoStatus(
81
+ name=name,
82
+ path=path,
83
+ branch="",
84
+ changes=[],
85
+ unpushed_commits=[],
86
+ error=f"Path not found: {path}",
87
+ )
88
+
89
+ # Check if it's a git repo
90
+ git_dir = path / ".git"
91
+ if not git_dir.exists() and not (path / "..").joinpath(".git").exists():
92
+ # Also check if path itself is a git dir (bare repo or worktree)
93
+ try:
94
+ _, _, rc = await _run_git_command(["rev-parse", "--git-dir"], path)
95
+ if rc != 0:
96
+ return RepoStatus(
97
+ name=name,
98
+ path=path,
99
+ branch="",
100
+ changes=[],
101
+ unpushed_commits=[],
102
+ error=f"Not a git repository: {path}",
103
+ )
104
+ except Exception as e:
105
+ return RepoStatus(
106
+ name=name,
107
+ path=path,
108
+ branch="",
109
+ changes=[],
110
+ unpushed_commits=[],
111
+ error=f"Git command failed: {e}",
112
+ )
113
+
114
+ try:
115
+ # Get current branch
116
+ branch_out, _, branch_rc = await _run_git_command(
117
+ ["branch", "--show-current"], path
118
+ )
119
+ if branch_rc != 0 or not branch_out:
120
+ # Might be in detached HEAD state
121
+ branch_out = "detached"
122
+
123
+ # Get status (uncommitted changes)
124
+ status_out, _, _ = await _run_git_command(["status", "--short"], path)
125
+ changes = [line for line in status_out.split("\n") if line.strip()]
126
+
127
+ # Get unpushed commits (comparing to origin/develop)
128
+ unpushed_out, _, unpushed_rc = await _run_git_command(
129
+ ["log", "origin/develop..HEAD", "--oneline"], path
130
+ )
131
+ if unpushed_rc == 0 and unpushed_out:
132
+ unpushed_commits = [
133
+ line for line in unpushed_out.split("\n") if line.strip()
134
+ ]
135
+ else:
136
+ unpushed_commits = []
137
+
138
+ return RepoStatus(
139
+ name=name,
140
+ path=path,
141
+ branch=branch_out,
142
+ changes=changes,
143
+ unpushed_commits=unpushed_commits,
144
+ )
145
+
146
+ except Exception as e:
147
+ return RepoStatus(
148
+ name=name,
149
+ path=path,
150
+ branch="",
151
+ changes=[],
152
+ unpushed_commits=[],
153
+ error=f"Error getting status: {e}",
154
+ )
155
+
156
+
157
+ async def get_all_repo_status(repos: Sequence[tuple[str, Path]]) -> list[RepoStatus]:
158
+ """Get git status for all repos in parallel using asyncio.gather.
159
+
160
+ Args:
161
+ repos: Sequence of (name, path) tuples for each repo
162
+
163
+ Returns:
164
+ List of RepoStatus objects in same order as input
165
+ """
166
+ if not repos:
167
+ return []
168
+
169
+ tasks = [get_repo_status(name, path) for name, path in repos]
170
+ results = await asyncio.gather(*tasks, return_exceptions=False)
171
+ return list(results)
172
+
173
+
174
+ def format_status_brief(statuses: Sequence[RepoStatus]) -> str:
175
+ """Format repo statuses as brief one-line-per-repo output.
176
+
177
+ Format: "repo_name: branch_name [M] [↑N]"
178
+ - M = has modifications
179
+ - ↑N = N unpushed commits
180
+
181
+ Args:
182
+ statuses: Sequence of RepoStatus objects
183
+
184
+ Returns:
185
+ Multi-line string with brief status for each repo
186
+ """
187
+ lines = []
188
+ for status in statuses:
189
+ indicators = []
190
+
191
+ # Modification indicator
192
+ if not status.is_clean:
193
+ indicators.append("M")
194
+ else:
195
+ indicators.append("✓")
196
+
197
+ # Unpushed indicator
198
+ if status.has_unpushed:
199
+ indicators.append(f"↑{len(status.unpushed_commits)}")
200
+
201
+ indicator_str = " ".join(indicators)
202
+ lines.append(f"{status.name}: {status.branch} {indicator_str}")
203
+
204
+ return "\n".join(lines)
205
+
206
+
207
+ def format_status_full(statuses: Sequence[RepoStatus]) -> str:
208
+ """Format repo statuses as full detailed output.
209
+
210
+ Shows branch, changes (up to 10), and unpushed commits (up to 5).
211
+
212
+ Args:
213
+ statuses: Sequence of RepoStatus objects
214
+
215
+ Returns:
216
+ Multi-line string with detailed status for each repo
217
+ """
218
+ sections = []
219
+
220
+ for status in statuses:
221
+ lines = []
222
+ lines.append(f"=== {status.name} ===")
223
+ lines.append(f"Branch: {status.branch}")
224
+
225
+ if status.error:
226
+ lines.append(f"Error: {status.error}")
227
+ elif status.changes:
228
+ lines.append("Changes:")
229
+ for change in status.changes[:10]:
230
+ lines.append(f" {change}")
231
+ if len(status.changes) > 10:
232
+ lines.append(f" ... and {len(status.changes) - 10} more")
233
+ else:
234
+ lines.append("Clean")
235
+
236
+ if status.unpushed_commits:
237
+ lines.append(f"Unpushed ({len(status.unpushed_commits)}):")
238
+ for commit in status.unpushed_commits[:5]:
239
+ lines.append(f" {commit}")
240
+ if len(status.unpushed_commits) > 5:
241
+ lines.append(f" ... and {len(status.unpushed_commits) - 5} more")
242
+
243
+ lines.append("")
244
+ sections.append("\n".join(lines))
245
+
246
+ return "\n".join(sections)
247
+
248
+
249
+ def format_summary(statuses: Sequence[RepoStatus]) -> str:
250
+ """Format summary of all repo statuses.
251
+
252
+ Args:
253
+ statuses: Sequence of RepoStatus objects
254
+
255
+ Returns:
256
+ Summary string with total changes and unpushed counts
257
+ """
258
+ total_changes = sum(len(s.changes) for s in statuses)
259
+ total_unpushed = sum(len(s.unpushed_commits) for s in statuses)
260
+
261
+ if total_changes == 0 and total_unpushed == 0:
262
+ return "✅ All repos clean and pushed"
263
+
264
+ parts = []
265
+ if total_changes > 0:
266
+ parts.append(f"{total_changes} uncommitted change(s)")
267
+ if total_unpushed > 0:
268
+ parts.append(f"{total_unpushed} unpushed commit(s)")
269
+
270
+ return " | ".join(parts)
271
+
272
+
273
+ async def main(brief: bool = False) -> int:
274
+ """CLI entry point for git-status-all.
275
+
276
+ Args:
277
+ brief: If True, use brief output format
278
+
279
+ Returns:
280
+ 0 if all repos clean, 1 if any have changes/unpushed
281
+ """
282
+ from pennyfarthing_scripts.common.config import get_project_root
283
+
284
+ # For now, just check the current project
285
+ project_root = get_project_root()
286
+ repos = [("pennyfarthing", project_root)]
287
+
288
+ statuses = await get_all_repo_status(repos)
289
+
290
+ if brief:
291
+ print(format_status_brief(statuses))
292
+ else:
293
+ print("━" * 40)
294
+ print(" Git Status - All Repos")
295
+ print("━" * 40)
296
+ print()
297
+ print(format_status_full(statuses))
298
+ print("━" * 40)
299
+ print(format_summary(statuses))
300
+
301
+ # Return 1 if any repo has changes or unpushed
302
+ has_issues = any(not s.is_clean or s.has_unpushed for s in statuses)
303
+ return 1 if has_issues else 0
304
+
305
+
306
+ if __name__ == "__main__":
307
+ import sys
308
+
309
+ brief_mode = "--brief" in sys.argv or "-b" in sys.argv
310
+ sys.exit(asyncio.run(main(brief=brief_mode)))