gitwise-cli 0.29.0__tar.gz → 0.30.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 (175) hide show
  1. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/CHANGELOG.md +6 -0
  2. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/PKG-INFO +1 -1
  3. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/__init__.py +4 -1
  4. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/__main__.py +4 -0
  5. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/_cli_completions.py +4 -0
  6. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/_cli_dispatch.py +89 -0
  7. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/_cli_introspection.py +14 -0
  8. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/_cli_parser.py +19 -0
  9. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/_cli_setup_agents.py +5 -0
  10. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/_i18n_data.json +12 -0
  11. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/_runtime_config.py +17 -0
  12. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/audit.py +11 -0
  13. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/branches.py +10 -0
  14. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/clean.py +6 -0
  15. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/commit.py +15 -0
  16. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/conflicts.py +8 -0
  17. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/context.py +9 -0
  18. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/design.py +26 -0
  19. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/diff.py +196 -23
  20. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/doctor.py +5 -0
  21. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/git.py +40 -0
  22. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/health.py +25 -0
  23. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/i18n.py +13 -0
  24. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/log.py +20 -0
  25. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/merge.py +10 -0
  26. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/optimize.py +12 -0
  27. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/output.py +45 -0
  28. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/pick.py +6 -0
  29. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/pr.py +31 -0
  30. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/schema.py +5 -0
  31. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/setup.py +28 -0
  32. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/setup_agents/exec.py +31 -3
  33. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/setup_agents/format.py +8 -0
  34. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/setup_agents/plan.py +17 -0
  35. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/setup_agents/plan_gitfiles.py +11 -0
  36. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/setup_agents/plan_skills.py +10 -0
  37. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/setup_agents/providers/__init__.py +9 -0
  38. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/setup_agents/providers/base.py +9 -0
  39. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/setup_agents/providers/claude.py +18 -0
  40. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/setup_agents/state.py +7 -0
  41. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/setup_agents/types.py +5 -0
  42. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/show.py +9 -0
  43. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/snapshot.py +6 -0
  44. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/stash.py +8 -0
  45. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/status.py +1 -0
  46. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/suggest.py +10 -0
  47. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/summarize.py +5 -0
  48. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/sync.py +13 -0
  49. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/tag.py +11 -0
  50. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/undo.py +9 -0
  51. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/update.py +1 -0
  52. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/worktree.py +5 -0
  53. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/pyproject.toml +1 -1
  54. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/schemas/v1/input/diff.json +16 -0
  55. gitwise_cli-0.30.0/tests/test_diff.py +327 -0
  56. gitwise_cli-0.29.0/tests/test_diff.py +0 -168
  57. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/.gitignore +0 -0
  58. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/LICENSE +0 -0
  59. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/README.md +0 -0
  60. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/bin/gitwise +0 -0
  61. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/_paths.py +0 -0
  62. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/py.typed +0 -0
  63. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/setup_agents/__init__.py +0 -0
  64. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/setup_agents/adapters/__init__.py +0 -0
  65. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/setup_agents/adapters/aider.py +0 -0
  66. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/setup_agents/adapters/base.py +0 -0
  67. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/setup_agents/adapters/codex.py +0 -0
  68. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/setup_agents/adapters/continue_adapter.py +0 -0
  69. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/setup_agents/adapters/cursor.py +0 -0
  70. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/setup_agents/adapters/opencode.py +0 -0
  71. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/setup_agents/adapters/pi.py +0 -0
  72. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/setup_agents/providers/aider.py +0 -0
  73. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/setup_agents/providers/codex.py +0 -0
  74. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/setup_agents/providers/continue_adapter.py +0 -0
  75. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/setup_agents/providers/cursor.py +0 -0
  76. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/setup_agents/providers/opencode.py +0 -0
  77. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/setup_agents/providers/pi.py +0 -0
  78. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/utils/__init__.py +0 -0
  79. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/utils/git_output.py +0 -0
  80. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/utils/in_progress.py +0 -0
  81. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/utils/json_envelope.py +0 -0
  82. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/gitwise/utils/parsing.py +0 -0
  83. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/install.sh +0 -0
  84. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/agents/skills/git-audit/SKILL.md +0 -0
  85. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/agents/skills/git-clean/SKILL.md +0 -0
  86. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/agents/skills/git-optimize/SKILL.md +0 -0
  87. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/aider/CONVENTIONS.md.template +0 -0
  88. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/aider/aider.conf.yml.template +0 -0
  89. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/claude/CLAUDE.md.template +0 -0
  90. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/claude/rules/gitwise.md +0 -0
  91. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/claude/settings.json.template +0 -0
  92. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/claude/skills/git-audit/SKILL.md +0 -0
  93. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/claude/skills/git-clean/SKILL.md +0 -0
  94. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/claude/skills/git-optimize/SKILL.md +0 -0
  95. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/codex/agents/gitwise.toml.template +0 -0
  96. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/continue/rules/gitwise.md.template +0 -0
  97. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/cursor/rules/gitwise.mdc.template +0 -0
  98. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/git-config-modern.txt +0 -0
  99. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/hooks/commit-msg +0 -0
  100. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/hooks/pre-commit +0 -0
  101. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/opencode/agents/gitwise.md.template +0 -0
  102. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/pi/skills/gitwise.md.template +0 -0
  103. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/schemas/v1/input/audit.json +0 -0
  104. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/schemas/v1/input/branches.json +0 -0
  105. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/schemas/v1/input/clean.json +0 -0
  106. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/schemas/v1/input/commands.json +0 -0
  107. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/schemas/v1/input/commit.json +0 -0
  108. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/schemas/v1/input/completions.json +0 -0
  109. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/schemas/v1/input/conflicts.json +0 -0
  110. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/schemas/v1/input/context.json +0 -0
  111. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/schemas/v1/input/doctor.json +0 -0
  112. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/schemas/v1/input/health.json +0 -0
  113. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/schemas/v1/input/log.json +0 -0
  114. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/schemas/v1/input/merge.json +0 -0
  115. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/schemas/v1/input/optimize.json +0 -0
  116. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/schemas/v1/input/pick.json +0 -0
  117. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/schemas/v1/input/pr.json +0 -0
  118. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/schemas/v1/input/schema.json +0 -0
  119. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/schemas/v1/input/setup-agents.json +0 -0
  120. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/schemas/v1/input/setup.json +0 -0
  121. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/schemas/v1/input/show.json +0 -0
  122. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/schemas/v1/input/snapshot.json +0 -0
  123. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/schemas/v1/input/stash.json +0 -0
  124. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/schemas/v1/input/status.json +0 -0
  125. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/schemas/v1/input/suggest.json +0 -0
  126. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/schemas/v1/input/summarize.json +0 -0
  127. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/schemas/v1/input/sync.json +0 -0
  128. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/schemas/v1/input/tag.json +0 -0
  129. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/schemas/v1/input/undo.json +0 -0
  130. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/schemas/v1/input/update.json +0 -0
  131. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/share/schemas/v1/input/worktree.json +0 -0
  132. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/__init__.py +0 -0
  133. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/conftest.py +0 -0
  134. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_adapters.py +0 -0
  135. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_audit.py +0 -0
  136. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_branches.py +0 -0
  137. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_clean.py +0 -0
  138. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_commit.py +0 -0
  139. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_conflicts.py +0 -0
  140. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_context.py +0 -0
  141. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_design.py +0 -0
  142. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_doctor.py +0 -0
  143. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_edge_cases.py +0 -0
  144. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_git.py +0 -0
  145. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_git_output_utils.py +0 -0
  146. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_health.py +0 -0
  147. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_i18n.py +0 -0
  148. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_in_progress.py +0 -0
  149. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_json_envelope_utils.py +0 -0
  150. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_log.py +0 -0
  151. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_main.py +0 -0
  152. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_md_links_script.py +0 -0
  153. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_merge.py +0 -0
  154. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_optimize.py +0 -0
  155. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_output.py +0 -0
  156. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_parsing_utils.py +0 -0
  157. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_pick.py +0 -0
  158. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_pr.py +0 -0
  159. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_roadmap_baseline_script.py +0 -0
  160. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_sa_plan.py +0 -0
  161. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_sa_unit.py +0 -0
  162. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_schema_catalog.py +0 -0
  163. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_setup.py +0 -0
  164. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_setup_agents.py +0 -0
  165. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_show.py +0 -0
  166. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_snapshot.py +0 -0
  167. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_stash.py +0 -0
  168. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_status.py +0 -0
  169. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_suggest.py +0 -0
  170. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_summarize.py +0 -0
  171. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_sync.py +0 -0
  172. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_sync_changelog_es_script.py +0 -0
  173. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_tag.py +0 -0
  174. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_undo.py +0 -0
  175. {gitwise_cli-0.29.0 → gitwise_cli-0.30.0}/tests/test_worktree.py +0 -0
@@ -3,6 +3,12 @@
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.30.0 (2026-06-19)
7
+
8
+ ### Feat
9
+
10
+ - **diff**: support refs, path scope, and summary (Sprint 2 PR-A) (#66)
11
+
6
12
  ## v0.29.0 (2026-06-19)
7
13
 
8
14
  ### Feat
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitwise-cli
3
- Version: 0.29.0
3
+ Version: 0.30.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
@@ -1,7 +1,10 @@
1
- __version__ = "0.29.0"
1
+ """gitwise -- CLI for optimizing git workflows and coding-agent integration."""
2
+
3
+ __version__ = "0.30.0"
2
4
 
3
5
 
4
6
  def get_version() -> str:
7
+ """Return the installed package version, falling back to the source version."""
5
8
  from importlib.metadata import PackageNotFoundError
6
9
  from importlib.metadata import version as _version
7
10
 
@@ -11,16 +11,19 @@ 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:
14
+ """Return True when GITWISE_LOG_JSON is set to a truthy value."""
14
15
  import os
15
16
 
16
17
  return os.environ.get("GITWISE_LOG_JSON", "").lower() in ("1", "true")
17
18
 
18
19
 
19
20
  def _should_show_rich_traceback() -> bool:
21
+ """Return True when rich tracebacks should be used (tty and not in JSON log mode)."""
20
22
  return (not _is_log_json_enabled()) and sys.stderr.isatty()
21
23
 
22
24
 
23
25
  def _install_rich_traceback() -> None:
26
+ """Install the rich traceback handler if conditions allow; no-op otherwise."""
24
27
  if not _should_show_rich_traceback():
25
28
  return
26
29
  try:
@@ -56,6 +59,7 @@ def _ensure_utf8_stdio() -> None:
56
59
 
57
60
 
58
61
  def main() -> int:
62
+ """Parse args, dispatch to the matching subcommand handler, and return an exit code."""
59
63
  import os
60
64
 
61
65
  from ._runtime_config import reset_runtime_config
@@ -7,6 +7,7 @@ from ._cli_parser import build_parser
7
7
 
8
8
 
9
9
  def build_completions_script(*, shell: str, prog: str) -> str:
10
+ """Return a shell completion script for the given shell and program name."""
10
11
  parser = build_parser()
11
12
  parser.prog = prog
12
13
 
@@ -23,10 +24,12 @@ def build_completions_script(*, shell: str, prog: str) -> str:
23
24
 
24
25
 
25
26
  def _fish_escape(text: str) -> str:
27
+ """Escape single quotes for use inside a fish completion string literal."""
26
28
  return text.replace("'", "\\'")
27
29
 
28
30
 
29
31
  def _build_fish_option_line(*, prog: str, condition: str, flag: str, help_text: str) -> str:
32
+ """Return a single fish ``complete`` line for an option, or an empty string for unrecognized flags."""
30
33
  escaped_help = _fish_escape(help_text)
31
34
  escaped_prog = _fish_escape(prog)
32
35
  escaped_condition = _fish_escape(condition)
@@ -44,6 +47,7 @@ def _build_fish_option_line(*, prog: str, condition: str, flag: str, help_text:
44
47
 
45
48
 
46
49
  def _build_fish_completions_script(*, parser: argparse.ArgumentParser, prog: str) -> str:
50
+ """Generate a fish completion script from the full argparse parser tree."""
47
51
  escaped_prog = _fish_escape(prog)
48
52
  lines: list[str] = [f"# fish completion for {escaped_prog}"]
49
53
 
@@ -1,6 +1,7 @@
1
1
  """Command dispatchers: thin wrappers that delegate to subcommand modules."""
2
2
 
3
3
  import argparse
4
+ import sys
4
5
  from collections.abc import Callable
5
6
 
6
7
  from . import __version__
@@ -17,18 +18,21 @@ from .utils.json_envelope import error_envelope
17
18
 
18
19
 
19
20
  def _run_update(args: argparse.Namespace) -> int:
21
+ """Dispatch to ``update`` subcommand."""
20
22
  from .update import run_update
21
23
 
22
24
  return run_update(dry_run=args.dry_run, as_json=args.json)
23
25
 
24
26
 
25
27
  def _run_doctor(args: argparse.Namespace) -> int:
28
+ """Dispatch to ``doctor`` subcommand."""
26
29
  from .doctor import run_doctor
27
30
 
28
31
  return run_doctor(as_json=args.json)
29
32
 
30
33
 
31
34
  def _run_setup_agents(args: argparse.Namespace) -> int:
35
+ """Dispatch to ``setup-agents`` subcommand, handling provider listing."""
32
36
  if getattr(args, "list_providers", False) or getattr(args, "list_adapters", False):
33
37
  from .i18n import t as _t
34
38
  from .setup_agents.providers import list_providers
@@ -68,6 +72,7 @@ def _run_setup_agents(args: argparse.Namespace) -> int:
68
72
 
69
73
 
70
74
  def _run_setup(args: argparse.Namespace) -> int:
75
+ """Dispatch to ``setup`` subcommand."""
71
76
  from .setup import run_setup
72
77
 
73
78
  return run_setup(
@@ -79,24 +84,28 @@ def _run_setup(args: argparse.Namespace) -> int:
79
84
 
80
85
 
81
86
  def _run_audit(args: argparse.Namespace) -> int:
87
+ """Dispatch to ``audit`` subcommand."""
82
88
  from .audit import run_audit
83
89
 
84
90
  return run_audit(quick=args.quick, as_json=args.json)
85
91
 
86
92
 
87
93
  def _run_summarize(args: argparse.Namespace) -> int:
94
+ """Dispatch to ``summarize`` subcommand."""
88
95
  from .summarize import run_summarize
89
96
 
90
97
  return run_summarize(as_json=args.json, diff=args.diff, max_commits=args.max_commits)
91
98
 
92
99
 
93
100
  def _run_snapshot(args: argparse.Namespace) -> int:
101
+ """Dispatch to ``snapshot`` subcommand."""
94
102
  from .snapshot import run_snapshot
95
103
 
96
104
  return run_snapshot(as_json=args.json)
97
105
 
98
106
 
99
107
  def _run_clean(args: argparse.Namespace) -> int:
108
+ """Dispatch to ``clean`` subcommand."""
100
109
  from .clean import run_clean
101
110
 
102
111
  return run_clean(
@@ -109,12 +118,14 @@ def _run_clean(args: argparse.Namespace) -> int:
109
118
 
110
119
 
111
120
  def _run_optimize(args: argparse.Namespace) -> int:
121
+ """Dispatch to ``optimize`` subcommand."""
112
122
  from .optimize import run_optimize
113
123
 
114
124
  return run_optimize(dry_run=args.dry_run, yes=args.yes, as_json=args.json)
115
125
 
116
126
 
117
127
  def _run_worktree(args: argparse.Namespace) -> int:
128
+ """Dispatch to ``worktree`` subcommand."""
118
129
  from .worktree import run_worktree
119
130
 
120
131
  return run_worktree(
@@ -122,19 +133,72 @@ def _run_worktree(args: argparse.Namespace) -> int:
122
133
  )
123
134
 
124
135
 
136
+ def _recover_diff_pathspec_boundary() -> tuple[str | None, list[str] | None]:
137
+ """Recover the explicit ``--`` pathspec boundary argparse loses.
138
+
139
+ With ``refspec nargs="?"`` + ``paths nargs="*"``, a path-only invocation
140
+ like ``gitwise diff -- src/`` mis-assigns ``src/`` to ``refspec``. git
141
+ treats ``--`` as a hard pathspec separator, so honor it here: anything
142
+ positional before ``--`` (excluding flags) feeds ``refspec``; anything
143
+ after ``--`` is literal paths, even values starting with ``-``.
144
+
145
+ Raises ``ValueError`` when more than one positional precedes ``--``, since
146
+ ``gitwise diff`` accepts a single refspec (two-commit diffs use ``a..b``);
147
+ failing fast beats silently dropping the extras.
148
+ """
149
+ try:
150
+ sub_idx = sys.argv.index("diff")
151
+ except ValueError:
152
+ return None, None
153
+ cmd_argv = sys.argv[sub_idx + 1 :]
154
+ if "--" not in cmd_argv:
155
+ return None, None
156
+ sep = cmd_argv.index("--")
157
+ before = [a for a in cmd_argv[:sep] if not a.startswith("-")]
158
+ after = list(cmd_argv[sep + 1 :])
159
+ if len(before) > 1:
160
+ raise ValueError(t("diff_too_many_refs", count=str(len(before))))
161
+ refspec = before[0] if before else None
162
+ paths = after if after else None
163
+ return refspec, paths
164
+
165
+
125
166
  def _run_diff(args: argparse.Namespace) -> int:
167
+ """Dispatch to ``diff`` subcommand."""
126
168
  from .diff import run_diff
169
+ from .output import error as error_out
170
+
171
+ refspec = args.refspec
172
+ paths = args.paths
173
+ # Honor `--` as a pathspec separator (git semantics). argparse's nargs="?"
174
+ # + nargs="*" otherwise mis-assigns path-only invocations to refspec.
175
+ if "--" in sys.argv:
176
+ try:
177
+ recovered_refspec, recovered_paths = _recover_diff_pathspec_boundary()
178
+ except ValueError as exc:
179
+ if args.json:
180
+ print_json(error_envelope(error=str(exc), code="too_many_refs"))
181
+ else:
182
+ error_out(str(exc))
183
+ return 1
184
+ if recovered_refspec is not None or recovered_paths is not None:
185
+ refspec = recovered_refspec
186
+ paths = recovered_paths
127
187
 
128
188
  return run_diff(
189
+ refspec=refspec,
190
+ paths=paths,
129
191
  staged=args.staged,
130
192
  stat=args.stat,
131
193
  name_only=args.name_only,
132
194
  full=args.full,
195
+ summary=args.summary,
133
196
  as_json=args.json,
134
197
  )
135
198
 
136
199
 
137
200
  def _run_log(args: argparse.Namespace) -> int:
201
+ """Dispatch to ``log`` subcommand."""
138
202
  from .log import run_log
139
203
 
140
204
  return run_log(
@@ -151,12 +215,14 @@ def _run_log(args: argparse.Namespace) -> int:
151
215
 
152
216
 
153
217
  def _run_show(args: argparse.Namespace) -> int:
218
+ """Dispatch to ``show`` subcommand."""
154
219
  from .show import run_show
155
220
 
156
221
  return run_show(ref=args.ref, stat=args.stat, as_json=args.json)
157
222
 
158
223
 
159
224
  def _run_commit(args: argparse.Namespace) -> int:
225
+ """Dispatch to ``commit`` subcommand."""
160
226
  from .commit import run_commit
161
227
 
162
228
  return run_commit(
@@ -171,12 +237,14 @@ def _run_commit(args: argparse.Namespace) -> int:
171
237
 
172
238
 
173
239
  def _run_branches(args: argparse.Namespace) -> int:
240
+ """Dispatch to ``branches`` subcommand."""
174
241
  from .branches import run_branches
175
242
 
176
243
  return run_branches(stale=args.stale, remote=args.remote, sort=args.sort, as_json=args.json)
177
244
 
178
245
 
179
246
  def _run_sync(args: argparse.Namespace) -> int:
247
+ """Dispatch to ``sync`` subcommand."""
180
248
  from .sync import run_sync
181
249
 
182
250
  return run_sync(
@@ -189,12 +257,14 @@ def _run_sync(args: argparse.Namespace) -> int:
189
257
 
190
258
 
191
259
  def _run_pr(args: argparse.Namespace) -> int:
260
+ """Dispatch to ``pr`` subcommand."""
192
261
  from .pr import run_pr
193
262
 
194
263
  return run_pr(action=args.action, selector=args.selector, as_json=args.json)
195
264
 
196
265
 
197
266
  def _run_undo(args: argparse.Namespace) -> int:
267
+ """Dispatch to ``undo`` subcommand."""
198
268
  from .undo import run_undo
199
269
 
200
270
  return run_undo(
@@ -208,18 +278,21 @@ def _run_undo(args: argparse.Namespace) -> int:
208
278
 
209
279
 
210
280
  def _run_context(args: argparse.Namespace) -> int:
281
+ """Dispatch to ``context`` subcommand."""
211
282
  from .context import run_context
212
283
 
213
284
  return run_context(as_json=args.json)
214
285
 
215
286
 
216
287
  def _run_health(args: argparse.Namespace) -> int:
288
+ """Dispatch to ``health`` subcommand."""
217
289
  from .health import run_health
218
290
 
219
291
  return run_health(as_json=args.json)
220
292
 
221
293
 
222
294
  def _run_stash(args: argparse.Namespace) -> int:
295
+ """Dispatch to ``stash`` subcommand."""
223
296
  from .stash import run_stash
224
297
 
225
298
  return run_stash(
@@ -233,6 +306,7 @@ def _run_stash(args: argparse.Namespace) -> int:
233
306
 
234
307
 
235
308
  def _run_tag(args: argparse.Namespace) -> int:
309
+ """Dispatch to ``tag`` subcommand."""
236
310
  from .tag import run_tag
237
311
 
238
312
  return run_tag(
@@ -247,6 +321,7 @@ def _run_tag(args: argparse.Namespace) -> int:
247
321
 
248
322
 
249
323
  def _run_merge(args: argparse.Namespace) -> int:
324
+ """Dispatch to ``merge`` subcommand."""
250
325
  from .merge import run_merge
251
326
 
252
327
  return run_merge(
@@ -262,18 +337,21 @@ def _run_merge(args: argparse.Namespace) -> int:
262
337
 
263
338
 
264
339
  def _run_conflicts(args: argparse.Namespace) -> int:
340
+ """Dispatch to ``conflicts`` subcommand."""
265
341
  from .conflicts import run_conflicts
266
342
 
267
343
  return run_conflicts(ours=args.ours, theirs=args.theirs, as_json=args.json)
268
344
 
269
345
 
270
346
  def _run_suggest(args: argparse.Namespace) -> int:
347
+ """Dispatch to ``suggest`` subcommand."""
271
348
  from .suggest import run_suggest
272
349
 
273
350
  return run_suggest(as_json=args.json)
274
351
 
275
352
 
276
353
  def _run_pick(args: argparse.Namespace) -> int:
354
+ """Dispatch to ``pick`` / ``cherry-pick`` subcommand."""
277
355
  from .pick import run_pick
278
356
 
279
357
  return run_pick(
@@ -287,12 +365,18 @@ def _run_pick(args: argparse.Namespace) -> int:
287
365
 
288
366
 
289
367
  def _run_status(args: argparse.Namespace) -> int:
368
+ """Dispatch to ``status`` subcommand."""
290
369
  from .status import run_status
291
370
 
292
371
  return run_status(as_json=args.json)
293
372
 
294
373
 
295
374
  def _run_completions(args: argparse.Namespace) -> int:
375
+ """Generate and output a shell completions script.
376
+
377
+ Returns 1 on missing dependency (shtab), unsupported shell, or
378
+ runtime error.
379
+ """
296
380
  shell = args.shell
297
381
  prog = args.prog
298
382
  try:
@@ -346,6 +430,7 @@ def _run_completions(args: argparse.Namespace) -> int:
346
430
 
347
431
 
348
432
  def _run_commands(args: argparse.Namespace) -> int:
433
+ """List all registered subcommands with aliases."""
349
434
  parser = build_parser()
350
435
  commands = commands_metadata(parser)
351
436
  payload = {
@@ -374,6 +459,10 @@ def _run_commands(args: argparse.Namespace) -> int:
374
459
 
375
460
 
376
461
  def _run_schema(args: argparse.Namespace) -> int:
462
+ """Print the JSON Schema for a subcommand's CLI input.
463
+
464
+ Returns 1 when the command is unknown or its schema file is missing.
465
+ """
377
466
  from .schema import load_command_input_schema
378
467
 
379
468
  parser = build_parser()
@@ -7,6 +7,7 @@ from . import __version__
7
7
 
8
8
 
9
9
  def _json_safe(value: object) -> object:
10
+ """Recursively convert an argparse value to a JSON-serializable form."""
10
11
  if value is argparse.SUPPRESS:
11
12
  return None
12
13
  if value is None or isinstance(value, str | int | float | bool):
@@ -19,6 +20,7 @@ def _json_safe(value: object) -> object:
19
20
 
20
21
 
21
22
  def _subparsers_action(parser: argparse.ArgumentParser) -> argparse._SubParsersAction | None:
23
+ """Return the first _SubParsersAction from a parser, or None."""
22
24
  for action in parser._actions:
23
25
  if isinstance(action, argparse._SubParsersAction):
24
26
  return action
@@ -26,6 +28,7 @@ def _subparsers_action(parser: argparse.ArgumentParser) -> argparse._SubParsersA
26
28
 
27
29
 
28
30
  def _serialize_actions(parser: argparse.ArgumentParser) -> list[dict[str, object]]:
31
+ """Serialize a parser's non-help, non-subparser actions into JSON-friendly dicts."""
29
32
  items: list[dict[str, object]] = []
30
33
  for action in parser._actions:
31
34
  if action.dest in {"help"}:
@@ -49,6 +52,7 @@ def _serialize_actions(parser: argparse.ArgumentParser) -> list[dict[str, object
49
52
 
50
53
 
51
54
  def extract_command_token(argv: list[str]) -> str | None:
55
+ """Extract the subcommand name from a raw argv, skipping global flags and their values."""
52
56
  skip_next = False
53
57
  for token in argv:
54
58
  if skip_next:
@@ -64,6 +68,7 @@ def extract_command_token(argv: list[str]) -> str | None:
64
68
 
65
69
 
66
70
  def help_payload(parser: argparse.ArgumentParser, command: str | None = None) -> dict[str, object]:
71
+ """Build a structured help payload for the root parser or a specific subcommand."""
67
72
  payload: dict[str, object] = {
68
73
  "v": 2,
69
74
  "ok": True,
@@ -130,6 +135,8 @@ def help_payload(parser: argparse.ArgumentParser, command: str | None = None) ->
130
135
 
131
136
 
132
137
  class CommandMetadata(TypedDict):
138
+ """Minimal metadata for a single subcommand."""
139
+
133
140
  name: str
134
141
  help: str
135
142
  aliases: list[str]
@@ -137,6 +144,7 @@ class CommandMetadata(TypedDict):
137
144
 
138
145
 
139
146
  def canonical_command_name(command_parser: argparse.ArgumentParser) -> str:
147
+ """Return the canonical name (last prog token) of a subcommand parser."""
140
148
  prog = command_parser.prog.strip()
141
149
  if not prog:
142
150
  return ""
@@ -145,6 +153,7 @@ def canonical_command_name(command_parser: argparse.ArgumentParser) -> str:
145
153
 
146
154
 
147
155
  def commands_metadata(parser: argparse.ArgumentParser) -> list[CommandMetadata]:
156
+ """Return metadata for every unique subcommand, deduplicating by parser identity."""
148
157
  sub_action = _subparsers_action(parser)
149
158
  if sub_action is None:
150
159
  return []
@@ -189,6 +198,7 @@ def commands_metadata(parser: argparse.ArgumentParser) -> list[CommandMetadata]:
189
198
 
190
199
 
191
200
  def _json_type_for_action(action: argparse.Action) -> str:
201
+ """Map an argparse action to a JSON Schema type string."""
192
202
  if isinstance(action, argparse._StoreTrueAction | argparse._StoreFalseAction):
193
203
  return "boolean"
194
204
 
@@ -201,6 +211,7 @@ def _json_type_for_action(action: argparse.Action) -> str:
201
211
 
202
212
 
203
213
  def _action_property_schema(action: argparse.Action) -> dict[str, object]:
214
+ """Build a JSON Schema property dict for a single argparse action."""
204
215
  value_schema: dict[str, object] = {"type": _json_type_for_action(action)}
205
216
 
206
217
  if action.choices:
@@ -230,6 +241,7 @@ def _action_property_schema(action: argparse.Action) -> dict[str, object]:
230
241
 
231
242
 
232
243
  def _action_required(action: argparse.Action) -> bool:
244
+ """Determine whether an argparse action produces a required JSON field."""
233
245
  if action.option_strings:
234
246
  return bool(getattr(action, "required", False))
235
247
 
@@ -242,6 +254,7 @@ def _action_required(action: argparse.Action) -> bool:
242
254
 
243
255
 
244
256
  def command_input_schema(command_parser: argparse.ArgumentParser) -> dict[str, object]:
257
+ """Generate a JSON Schema (draft 2020-12) describing the CLI input of a subcommand."""
245
258
  properties: dict[str, object] = {}
246
259
  required: list[str] = []
247
260
 
@@ -269,6 +282,7 @@ def command_input_schema(command_parser: argparse.ArgumentParser) -> dict[str, o
269
282
  def resolve_command_parser(
270
283
  *, parser: argparse.ArgumentParser, name: str
271
284
  ) -> argparse.ArgumentParser | None:
285
+ """Look up the sub-parser registered under *name*, or return None."""
272
286
  sub_action = _subparsers_action(parser)
273
287
  if sub_action is None:
274
288
  return None
@@ -8,10 +8,12 @@ from .i18n import t
8
8
 
9
9
 
10
10
  def _root_help_epilog() -> str:
11
+ """Return the localized environment-variable epilog for root help."""
11
12
  return t("help_root_environment_epilog")
12
13
 
13
14
 
14
15
  def build_parser() -> argparse.ArgumentParser:
16
+ """Build and return the top-level argparse parser with all subcommands registered."""
15
17
  parent = argparse.ArgumentParser(add_help=False)
16
18
  parent.add_argument(
17
19
  "--lang",
@@ -176,7 +178,24 @@ def build_parser() -> argparse.ArgumentParser:
176
178
  diff_group.add_argument(
177
179
  "--full", "--patch", action="store_true", help="show full patch with delta integration"
178
180
  )
181
+ diff_group.add_argument(
182
+ "--summary",
183
+ action="store_true",
184
+ help="compact summary: additions/deletions per file, no patch",
185
+ )
179
186
  p.add_argument("--stat", action="store_true", help="show diffstat (default behavior)")
187
+ p.add_argument(
188
+ "refspec",
189
+ nargs="?",
190
+ default=None,
191
+ help="commit, branch, or range to diff (e.g. HEAD~3, main..feat, main...feat)",
192
+ )
193
+ p.add_argument(
194
+ "paths",
195
+ nargs="*",
196
+ metavar="PATH",
197
+ help="limit to paths (use -- to separate from refspec)",
198
+ )
180
199
 
181
200
  p = sub.add_parser("log", help="pretty git log with filters", parents=[parent])
182
201
  p.add_argument("--oneline", action="store_true", help="one line per commit")
@@ -194,6 +194,11 @@ def _run_setup_local(
194
194
  providers: list[str] | None = None,
195
195
  adapters_legacy_used: bool = False,
196
196
  ) -> int:
197
+ """Install per-repo setup-agents artifacts (CLAUDE.md, settings, skills).
198
+
199
+ Returns 0 on success, 1 on plan/execution errors, 2 when ``--strict``
200
+ mode encounters warnings.
201
+ """
197
202
  cwd = target or Path.cwd()
198
203
 
199
204
  if not is_repo(cwd):
@@ -447,10 +447,22 @@
447
447
  "es": "Diagnóstico{suffix} — {count} observación(es):",
448
448
  "en": "Diagnostic{suffix} — {count} finding(s):"
449
449
  },
450
+ "diff_binary_lfs_hint": {
451
+ "es": "archivo binario {path} ({mib} MiB) -- considera Git LFS",
452
+ "en": "binary file {path} ({mib} MiB) -- consider Git LFS"
453
+ },
450
454
  "diff_prefix": {
451
455
  "es": "diff: {stat}",
452
456
  "en": "diff: {stat}"
453
457
  },
458
+ "diff_summary_header": {
459
+ "es": "Resumen -- {count} archivo(s):",
460
+ "en": "Summary -- {count} file(s):"
461
+ },
462
+ "diff_too_many_refs": {
463
+ "es": "demasiados argumentos posicionales antes de '--' ({count}); gitwise diff acepta un unico refspec (usa a..b para un rango)",
464
+ "en": "too many positional arguments before '--' ({count}); gitwise diff accepts a single refspec (use a..b for a range)"
465
+ },
454
466
  "directory_exists": {
455
467
  "es": "el directorio ya existe: {path}",
456
468
  "en": "directory already exists: {path}"
@@ -16,6 +16,7 @@ _OSC_TIMEOUT = 0.5
16
16
 
17
17
 
18
18
  def _drain_fd(fd: int, timeout: float = 0.1) -> None:
19
+ """Read and discard pending data from a file descriptor."""
19
20
  while True:
20
21
  ready, _, _ = select.select([fd], [], [], timeout)
21
22
  if not ready:
@@ -27,6 +28,7 @@ def _drain_fd(fd: int, timeout: float = 0.1) -> None:
27
28
 
28
29
 
29
30
  def _query_bg_color() -> str | None:
31
+ """Query the terminal background color via OSC 11; returns a hex string or None."""
30
32
  if os.environ.get("NO_COLOR", "") != "":
31
33
  return None
32
34
  if os.environ.get("GITWISE_NO_COLOR", "").lower() in ("1", "true"):
@@ -67,6 +69,7 @@ def _query_bg_color() -> str | None:
67
69
 
68
70
 
69
71
  def _read_osc_response(fd: int) -> str | None:
72
+ """Read an OSC response from a tty fd, with a timeout."""
70
73
  buf = bytearray()
71
74
  osc_received = False
72
75
  while len(buf) < 50:
@@ -92,6 +95,7 @@ def _read_osc_response(fd: int) -> str | None:
92
95
 
93
96
 
94
97
  def _parse_osc_color(resp: str) -> str | None:
98
+ """Extract a hex color from an OSC 11 response string."""
95
99
  idx = resp.find("\x1b]")
96
100
  if idx == -1:
97
101
  return None
@@ -125,6 +129,7 @@ def _parse_osc_color(resp: str) -> str | None:
125
129
 
126
130
 
127
131
  def _parse_rgb_component(s: str) -> int:
132
+ """Parse a single hex RGB component, normalizing values wider than 8 bits."""
128
133
  val = int(s, 16)
129
134
  if len(s) > 2:
130
135
  val = val >> (4 * (len(s) - 2))
@@ -132,20 +137,25 @@ def _parse_rgb_component(s: str) -> int:
132
137
 
133
138
 
134
139
  def _relative_luminance(hex_color: str) -> float:
140
+ """Compute the sRGB relative luminance of a hex color (WCG 2.4)."""
135
141
  h = hex_color.lstrip("#")
136
142
  r, g, b = int(h[0:2], 16) / 255, int(h[2:4], 16) / 255, int(h[4:6], 16) / 255
137
143
 
138
144
  def linearize(c: float) -> float:
145
+ """Apply the sRGB gamma curve."""
139
146
  return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
140
147
 
141
148
  return 0.2126 * linearize(r) + 0.7152 * linearize(g) + 0.0722 * linearize(b)
142
149
 
143
150
 
144
151
  def _is_dark_background(bg_hex: str) -> bool:
152
+ """Return True if the background luminance is below the dark threshold."""
145
153
  return _relative_luminance(bg_hex) < _BRIGHTNESS_THRESHOLD
146
154
 
147
155
 
148
156
  class RuntimeConfig:
157
+ """Immutable runtime settings detected from the environment."""
158
+
149
159
  __slots__ = (
150
160
  "_theme_tokens",
151
161
  "color_depth",
@@ -167,6 +177,7 @@ class RuntimeConfig:
167
177
  theme: str
168
178
 
169
179
  def __init__(self) -> None:
180
+ """Detect theme, TTY, tools, and color settings from the current environment."""
170
181
  self.has_bat = bool(shutil.which("bat"))
171
182
  self.has_delta = bool(shutil.which("delta"))
172
183
  self.theme = self._detect_theme()
@@ -178,10 +189,12 @@ class RuntimeConfig:
178
189
 
179
190
  @property
180
191
  def theme_tokens(self) -> ThemeTokens:
192
+ """Return the resolved ThemeTokens for the current theme."""
181
193
  return self._theme_tokens
182
194
 
183
195
  @staticmethod
184
196
  def _detect_theme() -> str:
197
+ """Detect theme preference from env vars, OSC 11 query, or COLORFGBG."""
185
198
  explicit = os.environ.get("GITWISE_THEME", "").lower()
186
199
  if explicit in ("dark", "light"):
187
200
  return explicit
@@ -216,6 +229,7 @@ class RuntimeConfig:
216
229
 
217
230
  @staticmethod
218
231
  def _detect_terminal_width() -> int:
232
+ """Return terminal width from GITWISE_WIDTH or shutil detection."""
219
233
  from .design import MAX_WIDTH, MIN_WIDTH, detect_terminal_width
220
234
 
221
235
  env_width = os.environ.get("GITWISE_WIDTH", "")
@@ -226,6 +240,7 @@ class RuntimeConfig:
226
240
 
227
241
  @staticmethod
228
242
  def _detect_color_depth() -> ColorDepth:
243
+ """Delegate color-depth detection to design.detect_color_depth."""
229
244
  from .design import detect_color_depth
230
245
 
231
246
  return detect_color_depth()
@@ -235,6 +250,7 @@ _runtime_config: RuntimeConfig | None = None
235
250
 
236
251
 
237
252
  def get_runtime_config() -> RuntimeConfig:
253
+ """Return the module-level RuntimeConfig singleton, creating it on first call."""
238
254
  global _runtime_config
239
255
  if _runtime_config is None:
240
256
  _runtime_config = RuntimeConfig()
@@ -242,5 +258,6 @@ def get_runtime_config() -> RuntimeConfig:
242
258
 
243
259
 
244
260
  def reset_runtime_config() -> None:
261
+ """Clear the cached RuntimeConfig so it is re-detected on next access."""
245
262
  global _runtime_config
246
263
  _runtime_config = None