gitwise-cli 0.26.1__tar.gz → 0.28.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.
Files changed (173) hide show
  1. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/CHANGELOG.md +28 -0
  2. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/PKG-INFO +12 -4
  3. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/README.md +11 -3
  4. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/__init__.py +1 -1
  5. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/__main__.py +33 -1
  6. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/_cli_setup_agents.py +31 -0
  7. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/_i18n_data.json +92 -0
  8. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/branches.py +46 -27
  9. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/clean.py +3 -1
  10. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/conflicts.py +12 -2
  11. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/context.py +7 -6
  12. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/diff.py +15 -3
  13. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/doctor.py +10 -8
  14. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/git.py +1 -1
  15. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/health.py +3 -2
  16. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/log.py +4 -3
  17. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/output.py +10 -0
  18. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/pr.py +10 -8
  19. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/show.py +5 -3
  20. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/snapshot.py +3 -2
  21. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/stash.py +5 -2
  22. gitwise_cli-0.28.0/gitwise/status.py +123 -0
  23. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/suggest.py +4 -4
  24. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/summarize.py +16 -14
  25. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/tag.py +11 -9
  26. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/update.py +14 -2
  27. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/utils/git_output.py +24 -0
  28. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/worktree.py +8 -4
  29. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/install.sh +6 -2
  30. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/pyproject.toml +2 -1
  31. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/conftest.py +6 -0
  32. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_adapters.py +13 -0
  33. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_audit.py +5 -1
  34. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_i18n.py +28 -0
  35. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_log.py +16 -2
  36. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_snapshot.py +3 -1
  37. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_tag.py +20 -0
  38. gitwise_cli-0.26.1/gitwise/status.py +0 -93
  39. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/.gitignore +0 -0
  40. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/LICENSE +0 -0
  41. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/bin/gitwise +0 -0
  42. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/_cli_completions.py +0 -0
  43. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/_cli_dispatch.py +0 -0
  44. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/_cli_introspection.py +0 -0
  45. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/_cli_parser.py +0 -0
  46. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/_paths.py +0 -0
  47. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/_runtime_config.py +0 -0
  48. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/audit.py +0 -0
  49. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/commit.py +0 -0
  50. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/design.py +0 -0
  51. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/i18n.py +0 -0
  52. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/merge.py +0 -0
  53. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/optimize.py +0 -0
  54. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/pick.py +0 -0
  55. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/py.typed +0 -0
  56. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/schema.py +0 -0
  57. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/setup.py +0 -0
  58. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/setup_agents/__init__.py +0 -0
  59. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/setup_agents/adapters/__init__.py +0 -0
  60. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/setup_agents/adapters/aider.py +0 -0
  61. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/setup_agents/adapters/base.py +0 -0
  62. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/setup_agents/adapters/codex.py +0 -0
  63. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/setup_agents/adapters/continue_adapter.py +0 -0
  64. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/setup_agents/adapters/cursor.py +0 -0
  65. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/setup_agents/adapters/opencode.py +0 -0
  66. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/setup_agents/adapters/pi.py +0 -0
  67. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/setup_agents/exec.py +0 -0
  68. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/setup_agents/format.py +0 -0
  69. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/setup_agents/plan.py +0 -0
  70. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/setup_agents/plan_gitfiles.py +0 -0
  71. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/setup_agents/plan_skills.py +0 -0
  72. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/setup_agents/providers/__init__.py +0 -0
  73. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/setup_agents/providers/aider.py +0 -0
  74. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/setup_agents/providers/base.py +0 -0
  75. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/setup_agents/providers/claude.py +0 -0
  76. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/setup_agents/providers/codex.py +0 -0
  77. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/setup_agents/providers/continue_adapter.py +0 -0
  78. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/setup_agents/providers/cursor.py +0 -0
  79. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/setup_agents/providers/opencode.py +0 -0
  80. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/setup_agents/providers/pi.py +0 -0
  81. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/setup_agents/state.py +0 -0
  82. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/setup_agents/types.py +0 -0
  83. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/sync.py +0 -0
  84. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/undo.py +0 -0
  85. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/utils/__init__.py +0 -0
  86. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/utils/json_envelope.py +0 -0
  87. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/gitwise/utils/parsing.py +0 -0
  88. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/agents/skills/git-audit/SKILL.md +0 -0
  89. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/agents/skills/git-clean/SKILL.md +0 -0
  90. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/agents/skills/git-optimize/SKILL.md +0 -0
  91. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/aider/CONVENTIONS.md.template +0 -0
  92. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/aider/aider.conf.yml.template +0 -0
  93. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/claude/CLAUDE.md.template +0 -0
  94. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/claude/rules/gitwise.md +0 -0
  95. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/claude/settings.json.template +0 -0
  96. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/claude/skills/git-audit/SKILL.md +0 -0
  97. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/claude/skills/git-clean/SKILL.md +0 -0
  98. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/claude/skills/git-optimize/SKILL.md +0 -0
  99. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/codex/agents/gitwise.toml.template +0 -0
  100. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/continue/rules/gitwise.md.template +0 -0
  101. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/cursor/rules/gitwise.mdc.template +0 -0
  102. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/git-config-modern.txt +0 -0
  103. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/hooks/commit-msg +0 -0
  104. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/hooks/pre-commit +0 -0
  105. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/opencode/agents/gitwise.md.template +0 -0
  106. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/pi/skills/gitwise.md.template +0 -0
  107. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/schemas/v1/input/audit.json +0 -0
  108. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/schemas/v1/input/branches.json +0 -0
  109. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/schemas/v1/input/clean.json +0 -0
  110. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/schemas/v1/input/commands.json +0 -0
  111. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/schemas/v1/input/commit.json +0 -0
  112. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/schemas/v1/input/completions.json +0 -0
  113. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/schemas/v1/input/conflicts.json +0 -0
  114. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/schemas/v1/input/context.json +0 -0
  115. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/schemas/v1/input/diff.json +0 -0
  116. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/schemas/v1/input/doctor.json +0 -0
  117. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/schemas/v1/input/health.json +0 -0
  118. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/schemas/v1/input/log.json +0 -0
  119. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/schemas/v1/input/merge.json +0 -0
  120. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/schemas/v1/input/optimize.json +0 -0
  121. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/schemas/v1/input/pick.json +0 -0
  122. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/schemas/v1/input/pr.json +0 -0
  123. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/schemas/v1/input/schema.json +0 -0
  124. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/schemas/v1/input/setup-agents.json +0 -0
  125. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/schemas/v1/input/setup.json +0 -0
  126. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/schemas/v1/input/show.json +0 -0
  127. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/schemas/v1/input/snapshot.json +0 -0
  128. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/schemas/v1/input/stash.json +0 -0
  129. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/schemas/v1/input/status.json +0 -0
  130. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/schemas/v1/input/suggest.json +0 -0
  131. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/schemas/v1/input/summarize.json +0 -0
  132. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/schemas/v1/input/sync.json +0 -0
  133. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/schemas/v1/input/tag.json +0 -0
  134. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/schemas/v1/input/undo.json +0 -0
  135. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/schemas/v1/input/update.json +0 -0
  136. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/share/schemas/v1/input/worktree.json +0 -0
  137. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/__init__.py +0 -0
  138. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_branches.py +0 -0
  139. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_clean.py +0 -0
  140. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_commit.py +0 -0
  141. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_conflicts.py +0 -0
  142. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_context.py +0 -0
  143. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_design.py +0 -0
  144. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_diff.py +0 -0
  145. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_doctor.py +0 -0
  146. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_edge_cases.py +0 -0
  147. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_git.py +0 -0
  148. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_git_output_utils.py +0 -0
  149. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_health.py +0 -0
  150. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_json_envelope_utils.py +0 -0
  151. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_main.py +0 -0
  152. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_md_links_script.py +0 -0
  153. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_merge.py +0 -0
  154. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_optimize.py +0 -0
  155. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_output.py +0 -0
  156. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_parsing_utils.py +0 -0
  157. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_pick.py +0 -0
  158. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_pr.py +0 -0
  159. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_roadmap_baseline_script.py +0 -0
  160. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_sa_plan.py +0 -0
  161. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_sa_unit.py +0 -0
  162. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_schema_catalog.py +0 -0
  163. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_setup.py +0 -0
  164. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_setup_agents.py +0 -0
  165. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_show.py +0 -0
  166. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_stash.py +0 -0
  167. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_status.py +0 -0
  168. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_suggest.py +0 -0
  169. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_summarize.py +0 -0
  170. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_sync.py +0 -0
  171. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_sync_changelog_es_script.py +0 -0
  172. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_undo.py +0 -0
  173. {gitwise_cli-0.26.1 → gitwise_cli-0.28.0}/tests/test_worktree.py +0 -0
@@ -3,6 +3,34 @@
3
3
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
4
4
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
5
 
6
+ ## v0.28.0 (2026-06-19)
7
+
8
+ ### BREAKING CHANGE
9
+
10
+ - three JSON contract changes for agents. (1) branches --json: current/in_worktree are now bool (were strings); ahead/behind are now int|null (were strings); upstream/tracking are now string|null. (2) log --json: date field changes from --date=iso to --date=iso-strict (e.g. "2026-06-18T14:30:00+00:00"). (3) tag list --json: date field changes from %(creatordate:iso) to %(creatordate:iso-strict), same shift as log. Agents and scripts parsing these fields must update their deserializers. With major_version_zero=true this rotates the minor (0.27.x -> 0.28.0); the auto-release workflow will bump pyproject.toml and regenerate CHANGELOG.md on push to main, surfacing all three breaks under the Breaking Changes section.
11
+ - `branches --json` changes types in 5 fields. `current`
12
+ and `in_worktree` are now bool (were "true"/"false" strings); `ahead` and
13
+ `behind` are now int|null (were strings); `upstream` and `tracking` are
14
+ now string|null (were always strings). Agents and scripts parsing these
15
+ fields must update their deserializers. With major_version_zero=true this
16
+ rotates the minor (0.27.x -> 0.28.0); the auto-release workflow will bump
17
+ pyproject.toml and regenerate CHANGELOG.md on push to main.
18
+
19
+ ### Feat
20
+
21
+ - unified loading feedback + structured JSON (BREAKING branches/log/tag) (#64)
22
+ - unified loading feedback + structured JSON for status/branches
23
+
24
+ ### Fix
25
+
26
+ - address CI failures and CodeRabbit review findings
27
+
28
+ ## v0.27.0 (2026-06-19)
29
+
30
+ ### Feat
31
+
32
+ - **install**: Windows installer + extend audit to detect unpinned uses: (#63)
33
+
6
34
  ## v0.26.1 (2026-06-18)
7
35
 
8
36
  ### Fix
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitwise-cli
3
- Version: 0.26.1
3
+ Version: 0.28.0
4
4
  Summary: Python CLI for optimizing git workflows and Claude Code integration
5
5
  Project-URL: Homepage, https://github.com/drzioner/gitwise
6
6
  Project-URL: Repository, https://github.com/drzioner/gitwise
@@ -89,11 +89,19 @@ uv sync
89
89
  uv run python -m gitwise doctor
90
90
  ```
91
91
 
92
+ **Windows** (PowerShell 5.1+, auto-installs `uv` if missing):
93
+
94
+ ```powershell
95
+ irm https://raw.githubusercontent.com/drzioner/gitwise/main/install.ps1 | iex
96
+ ```
97
+
98
+ For a version-pinned install (e.g. for reproducible setups), see `Get-Help .\install.ps1 -Detailed` after download.
99
+
92
100
  Update an existing installation:
93
101
 
94
102
  ```bash
95
- brew upgrade gitwise # if installed via Homebrew
96
- uv tool upgrade gitwise-cli # if installed via uv
103
+ brew upgrade gitwise # if installed via Homebrew (macOS/Linux)
104
+ uv tool upgrade gitwise-cli # if installed via uv (any OS)
97
105
  # or re-run the curl | bash installer, which always pulls latest
98
106
  ```
99
107
 
@@ -101,7 +109,7 @@ Uninstall:
101
109
 
102
110
  ```bash
103
111
  brew uninstall gitwise # if installed via Homebrew
104
- uv tool uninstall gitwise-cli # if installed via uv
112
+ uv tool uninstall gitwise-cli # if installed via uv (any OS)
105
113
  ```
106
114
 
107
115
  ## Quick Start
@@ -56,11 +56,19 @@ uv sync
56
56
  uv run python -m gitwise doctor
57
57
  ```
58
58
 
59
+ **Windows** (PowerShell 5.1+, auto-installs `uv` if missing):
60
+
61
+ ```powershell
62
+ irm https://raw.githubusercontent.com/drzioner/gitwise/main/install.ps1 | iex
63
+ ```
64
+
65
+ For a version-pinned install (e.g. for reproducible setups), see `Get-Help .\install.ps1 -Detailed` after download.
66
+
59
67
  Update an existing installation:
60
68
 
61
69
  ```bash
62
- brew upgrade gitwise # if installed via Homebrew
63
- uv tool upgrade gitwise-cli # if installed via uv
70
+ brew upgrade gitwise # if installed via Homebrew (macOS/Linux)
71
+ uv tool upgrade gitwise-cli # if installed via uv (any OS)
64
72
  # or re-run the curl | bash installer, which always pulls latest
65
73
  ```
66
74
 
@@ -68,7 +76,7 @@ Uninstall:
68
76
 
69
77
  ```bash
70
78
  brew uninstall gitwise # if installed via Homebrew
71
- uv tool uninstall gitwise-cli # if installed via uv
79
+ uv tool uninstall gitwise-cli # if installed via uv (any OS)
72
80
  ```
73
81
 
74
82
  ## Quick Start
@@ -1,4 +1,4 @@
1
- __version__ = "0.26.1"
1
+ __version__ = "0.28.0"
2
2
 
3
3
 
4
4
  def get_version() -> str:
@@ -7,7 +7,7 @@ from ._cli_dispatch import DISPATCH
7
7
  from ._cli_introspection import extract_command_token, help_payload
8
8
  from ._cli_parser import build_parser
9
9
  from .i18n import t
10
- from .output import print_dim, print_json, set_json_pretty
10
+ from .output import print_dim, print_json, set_json_mode, set_json_pretty
11
11
 
12
12
 
13
13
  def _is_log_json_enabled() -> bool:
@@ -32,12 +32,36 @@ def _install_rich_traceback() -> None:
32
32
  return
33
33
 
34
34
 
35
+ def _ensure_utf8_stdio() -> None:
36
+ """Force stdout/stderr to UTF-8.
37
+
38
+ Windows defaults to a system codepage (often cp1252) for the Python
39
+ embedded in console apps. That codepage cannot encode characters like
40
+ U+2713 (✓) used throughout gitwise's status output, causing
41
+ UnicodeEncodeError when the user is not running through a wrapper that
42
+ sets PYTHONIOENCODING. macOS/Linux are already UTF-8 by default, so
43
+ this reconfigure is a no-op there.
44
+ """
45
+ for stream_name in ("stdout", "stderr"):
46
+ stream = getattr(sys, stream_name, None)
47
+ # TextIOWrapper.reconfigure exists on Python 3.7+; guard just in case
48
+ # the stream has been replaced with something non-standard.
49
+ reconfigure = getattr(stream, "reconfigure", None)
50
+ if callable(reconfigure):
51
+ try:
52
+ reconfigure(encoding="utf-8", errors="replace")
53
+ except (TypeError, ValueError):
54
+ # Stream does not accept these kwargs or is closed; ignore.
55
+ pass
56
+
57
+
35
58
  def main() -> int:
36
59
  import os
37
60
 
38
61
  from ._runtime_config import reset_runtime_config
39
62
  from .i18n import set_locale
40
63
 
64
+ _ensure_utf8_stdio()
41
65
  _install_rich_traceback()
42
66
 
43
67
  parser = build_parser()
@@ -76,6 +100,7 @@ def main() -> int:
76
100
  reset_runtime_config()
77
101
 
78
102
  set_json_pretty(args.json_pretty)
103
+ set_json_mode(args.json)
79
104
 
80
105
  if args.lang:
81
106
  set_locale(args.lang)
@@ -93,6 +118,13 @@ def main() -> int:
93
118
  except Exception:
94
119
  if _should_show_rich_traceback():
95
120
  raise
121
+ # No Rich traceback available (CI / non-tty / LOG_JSON mode).
122
+ # Still emit the raw traceback to stderr so CI logs are
123
+ # diagnostic. Otherwise the user only sees "unexpected error"
124
+ # and has no way to identify the root cause.
125
+ import traceback as _traceback
126
+
127
+ _traceback.print_exc()
96
128
  from .output import error as _error
97
129
 
98
130
  _error(t("unexpected_error"))
@@ -385,6 +385,37 @@ def run_setup_agents(
385
385
  adapters_legacy_used: bool = False,
386
386
  ) -> int:
387
387
  """Dispatcher: global mode (default) or per-repo mode (--local)."""
388
+ import platform
389
+
390
+ if platform.system() == "Windows":
391
+ if not local:
392
+ # Global mode creates symlinks in ~/.claude/ (skills, CLAUDE.md
393
+ # pointer, etc.). Windows does not support POSIX-style symlinks
394
+ # without Developer Mode, and even with it the semantics around
395
+ # relative targets differ enough that the existing setup_agents
396
+ # code produces broken links. Fail fast with a clear workaround
397
+ # instead of letting the user hit a confusing traceback.
398
+ if as_json:
399
+ print_json(
400
+ {
401
+ "ok": False,
402
+ "error": "windows_global_unsupported",
403
+ "message": t("setup_agents_windows_global_unsupported"),
404
+ "workaround": "gitwise setup-agents --local --no-symlinks",
405
+ }
406
+ )
407
+ else:
408
+ error(t("setup_agents_windows_global_unsupported"))
409
+ info(t("setup_agents_windows_workaround_local"))
410
+ return 1
411
+ if not no_symlinks:
412
+ # Local mode auto-enables --no-symlinks on Windows. Same root
413
+ # cause: os.symlink() either errors out (no Developer Mode) or
414
+ # produces links that do not behave like POSIX symlinks for our
415
+ # sandbox check (os.path.realpath over a not-yet-existing target).
416
+ warn(t("setup_agents_windows_auto_no_symlinks"))
417
+ no_symlinks = True
418
+
388
419
  if local:
389
420
  return _run_setup_local(
390
421
  target,
@@ -519,6 +519,18 @@
519
519
  "es": "fsmonitor integrado no está soportado en Linux (solo macOS y Windows)",
520
520
  "en": "built-in fsmonitor not supported on Linux (macOS and Windows only)"
521
521
  },
522
+ "setup_agents_windows_global_unsupported": {
523
+ "es": "setup-agents en modo global no está soportado en Windows (requiere symlinks POSIX en ~/.claude/).",
524
+ "en": "setup-agents global mode is not supported on Windows (requires POSIX symlinks in ~/.claude/)."
525
+ },
526
+ "setup_agents_windows_workaround_local": {
527
+ "es": "Workaround: usá 'gitwise setup-agents --local --no-symlinks' dentro del repo donde quieras configurar los agentes.",
528
+ "en": "Workaround: use 'gitwise setup-agents --local --no-symlinks' inside the repo where you want to set up agents."
529
+ },
530
+ "setup_agents_windows_auto_no_symlinks": {
531
+ "es": "Windows detectado: activando --no-symlinks automáticamente (los symlinks POSIX no son confiables en Windows).",
532
+ "en": "Windows detected: auto-enabling --no-symlinks (POSIX symlinks are not reliable on Windows)."
533
+ },
522
534
  "gc_already_running": {
523
535
  "es": "git gc/maintenance ya está en ejecución — esperá a que termine",
524
536
  "en": "git gc/maintenance already running — wait for it to finish"
@@ -1559,6 +1571,78 @@
1559
1571
  "es": "Auditando: ejecutando git-sizer...",
1560
1572
  "en": "Auditing: running git-sizer..."
1561
1573
  },
1574
+ "status_worktree_add": {
1575
+ "es": "Creando worktree para {branch}...",
1576
+ "en": "Creating worktree for {branch}..."
1577
+ },
1578
+ "status_health_scan": {
1579
+ "es": "Calculando salud del repositorio...",
1580
+ "en": "Computing repository health..."
1581
+ },
1582
+ "status_context_scan": {
1583
+ "es": "Recolectando contexto del repositorio...",
1584
+ "en": "Gathering repository context..."
1585
+ },
1586
+ "status_snapshot_gen": {
1587
+ "es": "Generando snapshot...",
1588
+ "en": "Generating snapshot..."
1589
+ },
1590
+ "status_reading_status": {
1591
+ "es": "Leyendo estado del repositorio...",
1592
+ "en": "Reading repository status..."
1593
+ },
1594
+ "status_summarizing": {
1595
+ "es": "Resumiendo repositorio...",
1596
+ "en": "Summarizing repository..."
1597
+ },
1598
+ "status_reading_diff": {
1599
+ "es": "Calculando cambios...",
1600
+ "en": "Computing changes..."
1601
+ },
1602
+ "status_reading_log": {
1603
+ "es": "Leyendo historial de commits...",
1604
+ "en": "Reading commit history..."
1605
+ },
1606
+ "status_loading_commit": {
1607
+ "es": "Cargando commit...",
1608
+ "en": "Loading commit..."
1609
+ },
1610
+ "status_analyzing_branches": {
1611
+ "es": "Analizando ramas...",
1612
+ "en": "Analyzing branches..."
1613
+ },
1614
+ "status_analyzing_staged": {
1615
+ "es": "Analizando cambios preparados...",
1616
+ "en": "Analyzing staged changes..."
1617
+ },
1618
+ "status_detecting_conflicts": {
1619
+ "es": "Detectando conflictos...",
1620
+ "en": "Detecting conflicts..."
1621
+ },
1622
+ "status_reading_stashes": {
1623
+ "es": "Leyendo stashes...",
1624
+ "en": "Reading stashes..."
1625
+ },
1626
+ "status_reading_tags": {
1627
+ "es": "Leyendo tags...",
1628
+ "en": "Reading tags..."
1629
+ },
1630
+ "status_querying_github": {
1631
+ "es": "Consultando GitHub...",
1632
+ "en": "Querying GitHub..."
1633
+ },
1634
+ "status_checking_env": {
1635
+ "es": "Verificando entorno...",
1636
+ "en": "Checking environment..."
1637
+ },
1638
+ "status_scanning_stale": {
1639
+ "es": "Buscando ramas y refs obsoletos...",
1640
+ "en": "Scanning stale branches and refs..."
1641
+ },
1642
+ "status_updating": {
1643
+ "es": "Actualizando gitwise...",
1644
+ "en": "Updating gitwise..."
1645
+ },
1562
1646
  "step_commit_graph": {
1563
1647
  "es": "git commit-graph write --reachable --changed-paths",
1564
1648
  "en": "git commit-graph write --reachable --changed-paths"
@@ -1915,6 +1999,14 @@
1915
1999
  "es": "update requiere una instalación vía git clone (no se encontró .git/)",
1916
2000
  "en": "update requires a git clone installation (.git/ not found)"
1917
2001
  },
2002
+ "update_no_upstream": {
2003
+ "es": "la rama actual '{branch}' no tiene rama de seguimiento; update solo puede actualizar una rama con upstream",
2004
+ "en": "current branch '{branch}' has no upstream; update can only fast-forward a tracking branch"
2005
+ },
2006
+ "update_no_upstream_hint": {
2007
+ "es": "cambia a tu rama principal (p.ej. 'git switch main') o configura el upstream con 'git branch --set-upstream-to=origin/{branch}'",
2008
+ "en": "switch to your main branch (e.g. 'git switch main') or set an upstream with 'git branch --set-upstream-to=origin/{branch}'"
2009
+ },
1918
2010
  "invalid_grep_pattern": {
1919
2011
  "es": "patrón de búsqueda inválido o peligroso: {pattern}",
1920
2012
  "en": "invalid or unsafe grep pattern: {pattern}"
@@ -1,14 +1,37 @@
1
1
  """gitwise branches — intelligence dashboard with ahead/behind, merged, stale, worktree info."""
2
2
 
3
+ from pathlib import Path
4
+ from typing import TypedDict
5
+
3
6
  from .git import require_root, stale_branches, worktree_branches
4
7
  from .git import run as git_run
5
8
  from .i18n import t
6
- from .output import error, info, print_dim, print_json, print_table
9
+ from .output import error, info, print_dim, print_json, print_table, status
7
10
  from .utils.json_envelope import ok_envelope
8
11
 
9
12
 
10
- def _parse_branches(raw: str, wt_branches: set[str]) -> list[dict[str, str]]:
11
- branches: list[dict[str, str]] = []
13
+ class BranchEntry(TypedDict):
14
+ name: str
15
+ current: bool
16
+ sha: str
17
+ subject: str
18
+ age: str
19
+ upstream: str | None
20
+ ahead: int | None
21
+ behind: int | None
22
+ tracking: str | None
23
+ in_worktree: bool
24
+
25
+
26
+ def _parse_track_count(tracking: str, marker: str) -> int | None:
27
+ if marker not in tracking:
28
+ return None
29
+ raw = tracking.split(marker)[1].split(",")[0].strip().rstrip("]")
30
+ return int(raw) if raw.isdigit() else None
31
+
32
+
33
+ def _parse_branches(raw: str, wt_branches: set[str]) -> list[BranchEntry]:
34
+ branches: list[BranchEntry] = []
12
35
  for line in raw.splitlines():
13
36
  if not line.strip():
14
37
  continue
@@ -23,24 +46,18 @@ def _parse_branches(raw: str, wt_branches: set[str]) -> list[dict[str, str]]:
23
46
  tracking = parts[5] if len(parts) > 5 else ""
24
47
  upstream = parts[6] if len(parts) > 6 else ""
25
48
 
26
- ahead = behind = ""
27
- if "[ahead" in tracking:
28
- ahead = tracking.split("ahead")[1].split(",")[0].strip().rstrip("]")
29
- if "behind" in tracking:
30
- behind = tracking.split("behind")[1].strip().rstrip("]")
31
-
32
49
  branches.append(
33
50
  {
34
51
  "name": name,
35
- "current": str(is_current).lower(),
52
+ "current": is_current,
36
53
  "sha": sha,
37
54
  "subject": subject,
38
55
  "age": age,
39
- "upstream": upstream,
40
- "ahead": ahead,
41
- "behind": behind,
42
- "tracking": tracking,
43
- "in_worktree": str(name in wt_branches).lower(),
56
+ "upstream": upstream or None,
57
+ "ahead": _parse_track_count(tracking, "ahead"),
58
+ "behind": _parse_track_count(tracking, "behind"),
59
+ "tracking": tracking or None,
60
+ "in_worktree": name in wt_branches,
44
61
  }
45
62
  )
46
63
  return branches
@@ -72,7 +89,7 @@ def _print_stale_branches(*, names: list[str], as_json: bool) -> int:
72
89
  return 0
73
90
 
74
91
 
75
- def _fetch_branch_rows(*, root, remote: bool, sort: str) -> list[dict[str, str]] | None:
92
+ def _fetch_branch_rows(*, root: Path, remote: bool, sort: str) -> list[BranchEntry] | None:
76
93
  wt_branches = worktree_branches(cwd=root)
77
94
  ref_pattern = "refs/remotes/" if remote else "refs/heads/"
78
95
  fmt = "%(HEAD)\t%(refname:short)\t%(objectname:short)\t%(subject)\t%(committerdate:relative)\t%(upstream:track)\t%(upstream:short)"
@@ -94,7 +111,7 @@ def _fetch_branch_rows(*, root, remote: bool, sort: str) -> list[dict[str, str]]
94
111
 
95
112
 
96
113
  def _build_branch_rows(
97
- branches: list[dict[str, str]],
114
+ branches: list[BranchEntry],
98
115
  ) -> tuple[list[list[str]], set[int], int | None]:
99
116
  rows: list[list[str]] = []
100
117
  highlight_rows: set[int] = set()
@@ -102,28 +119,28 @@ def _build_branch_rows(
102
119
  for idx, branch_item in enumerate(branches):
103
120
  sha = branch_item["sha"][:8]
104
121
  subject = branch_item["subject"][:40]
105
- age = branch_item.get("age", "")
122
+ age = branch_item["age"]
106
123
  flags: list[str] = []
107
- if branch_item.get("ahead"):
124
+ if branch_item["ahead"]:
108
125
  flags.append(f"↑{branch_item['ahead']}")
109
- if branch_item.get("behind"):
126
+ if branch_item["behind"]:
110
127
  flags.append(f"↓{branch_item['behind']}")
111
- if branch_item.get("in_worktree") == "true":
128
+ if branch_item["in_worktree"]:
112
129
  flags.append("wt")
113
- if branch_item.get("upstream"):
130
+ if branch_item["upstream"]:
114
131
  flags.append(f"→{branch_item['upstream']}")
115
132
  status = " ".join(flags) if flags else ""
116
133
  name_display = (
117
- f"* {branch_item['name']}" if branch_item["current"] == "true" else branch_item["name"]
134
+ f"* {branch_item['name']}" if branch_item["current"] else branch_item["name"]
118
135
  )
119
136
  rows.append([name_display, sha, subject, age, status])
120
- if branch_item["current"] == "true":
137
+ if branch_item["current"]:
121
138
  current_idx = idx
122
139
  highlight_rows.add(idx)
123
140
  return rows, highlight_rows, current_idx
124
141
 
125
142
 
126
- def _print_branch_table(branches: list[dict[str, str]]) -> None:
143
+ def _print_branch_table(branches: list[BranchEntry]) -> None:
127
144
  columns = [
128
145
  (t("col_branch"), "name"),
129
146
  (t("col_sha"), "sha"),
@@ -163,13 +180,15 @@ def run_branches(
163
180
  return 1
164
181
 
165
182
  if stale:
166
- names = stale_branches(cwd=root)
183
+ with status(t("status_analyzing_branches")):
184
+ names = stale_branches(cwd=root)
167
185
  return _print_stale_branches(names=names, as_json=as_json)
168
186
 
169
187
  if not _validate_sort_field(sort):
170
188
  return 1
171
189
 
172
- branches = _fetch_branch_rows(root=root, remote=remote, sort=sort)
190
+ with status(t("status_analyzing_branches")):
191
+ branches = _fetch_branch_rows(root=root, remote=remote, sort=sort)
173
192
  if branches is None:
174
193
  return 1
175
194
  if not branches:
@@ -22,6 +22,7 @@ from .output import (
22
22
  print_header,
23
23
  print_json,
24
24
  print_success,
25
+ status,
25
26
  warn,
26
27
  )
27
28
  from .utils.json_envelope import error_envelope, ok_envelope
@@ -78,7 +79,8 @@ def run_clean(
78
79
  return 1
79
80
  cwd = root
80
81
 
81
- deletable, skipped = _categorize(cwd)
82
+ with status(t("status_scanning_stale")):
83
+ deletable, skipped = _categorize(cwd)
82
84
 
83
85
  if dry_run:
84
86
  if as_json:
@@ -5,7 +5,16 @@ from pathlib import Path
5
5
  from .git import require_root
6
6
  from .git import run as git_run
7
7
  from .i18n import t
8
- from .output import error, ok, print_accent, print_blank, print_dim, print_header, print_json
8
+ from .output import (
9
+ error,
10
+ ok,
11
+ print_accent,
12
+ print_blank,
13
+ print_dim,
14
+ print_header,
15
+ print_json,
16
+ status,
17
+ )
9
18
  from .utils.json_envelope import error_envelope, ok_envelope
10
19
  from .utils.parsing import stripped_non_empty_lines, to_int
11
20
 
@@ -90,7 +99,8 @@ def run_conflicts(
90
99
  if root is None:
91
100
  return 1
92
101
 
93
- conflicts = _find_conflict_files(root)
102
+ with status(t("status_detecting_conflicts")):
103
+ conflicts = _find_conflict_files(root)
94
104
 
95
105
  if not conflicts:
96
106
  return _report_no_conflicts(as_json=as_json)
@@ -5,7 +5,7 @@ from pathlib import Path
5
5
  from .git import require_root
6
6
  from .git import run as git_run
7
7
  from .i18n import t
8
- from .output import info, print_blank, print_bracket, print_dim, print_header, print_json
8
+ from .output import info, print_blank, print_bracket, print_dim, print_header, print_json, status
9
9
 
10
10
 
11
11
  def _directory_tree(root: Path, max_depth: int = 3) -> list[str]:
@@ -108,11 +108,12 @@ def run_context(*, as_json: bool = False) -> int:
108
108
  if root is None:
109
109
  return 1
110
110
 
111
- tree = _directory_tree(root)
112
- contributors = _top_contributors(root)
113
- file_types = _file_type_breakdown(root)
114
- todo_fixme = _todo_fixme_counts(root)
115
- topology = _branch_topology(root)
111
+ with status(t("status_context_scan")):
112
+ tree = _directory_tree(root)
113
+ contributors = _top_contributors(root)
114
+ file_types = _file_type_breakdown(root)
115
+ todo_fixme = _todo_fixme_counts(root)
116
+ topology = _branch_topology(root)
116
117
 
117
118
  if as_json:
118
119
  from .health import compute_health
@@ -15,8 +15,9 @@ from .output import (
15
15
  print_file_status,
16
16
  print_header,
17
17
  print_json,
18
+ status,
18
19
  )
19
- from .utils.git_output import parse_name_status_entries
20
+ from .utils.git_output import parse_name_status_entries, status_label
20
21
  from .utils.json_envelope import ok_envelope
21
22
 
22
23
  DiffValue = str | int | bool
@@ -205,6 +206,9 @@ def _render_stat_output(
205
206
  numstat_details=numstat_details,
206
207
  )
207
208
  if as_json:
209
+ for entry in merged_files:
210
+ raw_code = str(entry.get("code", entry.get("status", "")))
211
+ entry["status_label"] = status_label(raw_code)
208
212
  print_json(
209
213
  ok_envelope(
210
214
  files=merged_files,
@@ -251,7 +255,14 @@ def _render_non_stat_output(
251
255
  for line in lines:
252
256
  parts = line.split("\t", 1)
253
257
  if len(parts) == 2:
254
- files.append({"status": parts[0].strip(), "path": parts[1].strip()})
258
+ code = parts[0].strip()
259
+ files.append(
260
+ {
261
+ "status": code,
262
+ "status_label": status_label(code),
263
+ "path": parts[1].strip(),
264
+ }
265
+ )
255
266
 
256
267
  if as_json:
257
268
  print_json(ok_envelope(files=files, count=len(files)))
@@ -287,7 +298,8 @@ def run_diff(
287
298
 
288
299
  use_stat = stat or (not staged and not name_only and not full)
289
300
  cmd = _diff_cmd(use_stat=use_stat, staged=staged, name_only=name_only, full=full)
290
- result = git_run(cmd, cwd=cwd, check=False)
301
+ with status(t("status_reading_diff")):
302
+ result = git_run(cmd, cwd=cwd, check=False)
291
303
  if result.returncode != 0:
292
304
  error(t("git_diff_failed", error=result.stderr.strip()))
293
305
  return 1
@@ -15,6 +15,7 @@ from .output import (
15
15
  print_json,
16
16
  print_kv,
17
17
  print_status_line,
18
+ status,
18
19
  warn,
19
20
  )
20
21
 
@@ -33,17 +34,18 @@ _TOOL_INFO: dict[str, tuple[str, str]] = {
33
34
 
34
35
 
35
36
  def run_doctor(*, as_json: bool = False) -> int:
36
- git_ver = git_version()
37
- git_ok = git_ver >= MIN_GIT
37
+ with status(t("status_checking_env")):
38
+ git_ver = git_version()
39
+ git_ok = git_ver >= MIN_GIT
38
40
 
39
- python_ver = sys.version_info[:3]
40
- python_ok = python_ver >= (3, 9)
41
+ python_ver = sys.version_info[:3]
42
+ python_ok = python_ver >= (3, 9)
41
43
 
42
- platform_name = platform.system()
43
- fsmonitor_supported = platform_name in ("Darwin", "Windows")
44
+ platform_name = platform.system()
45
+ fsmonitor_supported = platform_name in ("Darwin", "Windows")
44
46
 
45
- optional = {tool: bool(shutil.which(tool)) for tool in _OPTIONAL_TOOLS}
46
- gpg = gpg_status()
47
+ optional = {tool: bool(shutil.which(tool)) for tool in _OPTIONAL_TOOLS}
48
+ gpg = gpg_status()
47
49
 
48
50
  result = {
49
51
  "v": 2,
@@ -61,7 +61,7 @@ def run(
61
61
  ) -> subprocess.CompletedProcess[str]:
62
62
  from .output import debug
63
63
 
64
- cmd_name = args[0] if args else None
64
+ cmd_name = next((arg for arg in args if not arg.startswith("-")), None)
65
65
  actual_timeout = timeout if timeout is not None else _get_timeout(cmd_name)
66
66
  debug(f"git {' '.join(args)}")
67
67
  try:
@@ -12,7 +12,7 @@ from .git import (
12
12
  )
13
13
  from .git import run as git_run
14
14
  from .i18n import t
15
- from .output import print_header, print_json, print_status_line
15
+ from .output import print_header, print_json, print_status_line, status
16
16
  from .utils.json_envelope import ok_envelope
17
17
  from .utils.parsing import non_empty_lines, to_int
18
18
 
@@ -330,7 +330,8 @@ def run_health(*, as_json: bool = False) -> int:
330
330
  if root is None:
331
331
  return 1
332
332
 
333
- h = compute_health(root)
333
+ with status(t("status_health_scan")):
334
+ h = compute_health(root)
334
335
 
335
336
  if as_json:
336
337
  print_json(ok_envelope(payload=h))