dotman-git 1.3.1__tar.gz → 1.4.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 (187) hide show
  1. {dotman_git-1.3.1 → dotman_git-1.4.0}/CHANGELOG.md +44 -0
  2. {dotman_git-1.3.1/dotman_git.egg-info → dotman_git-1.4.0}/PKG-INFO +6 -7
  3. {dotman_git-1.3.1 → dotman_git-1.4.0}/README.md +5 -6
  4. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/__init__.py +1 -1
  5. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/common.py +22 -0
  6. dotman_git-1.4.0/dot_man/cli/completion_engine.py +445 -0
  7. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/completions_cmd.py +36 -21
  8. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/main.py +3 -0
  9. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/navigate_cmd.py +2 -0
  10. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/save_cmd.py +2 -0
  11. dotman_git-1.4.0/dot_man/completions/dot-man.fish +13 -0
  12. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/dotman_config.py +2 -0
  13. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/section.py +7 -0
  14. dotman_git-1.4.0/dot_man/shared_sync.py +301 -0
  15. {dotman_git-1.3.1 → dotman_git-1.4.0/dotman_git.egg-info}/PKG-INFO +6 -7
  16. {dotman_git-1.3.1 → dotman_git-1.4.0}/dotman_git.egg-info/SOURCES.txt +8 -1
  17. {dotman_git-1.3.1 → dotman_git-1.4.0}/pyproject.toml +1 -1
  18. dotman_git-1.4.0/tests/test_audit_cmd_extended.py +217 -0
  19. dotman_git-1.4.0/tests/test_bootstrap_cmd.py +269 -0
  20. dotman_git-1.4.0/tests/test_completion_engine.py +333 -0
  21. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_completions_cmd.py +33 -3
  22. dotman_git-1.4.0/tests/test_export_cmd.py +184 -0
  23. dotman_git-1.4.0/tests/test_revert_cmd_extended.py +106 -0
  24. dotman_git-1.4.0/tests/test_shared_sync.py +330 -0
  25. dotman_git-1.4.0/tests/test_tag_cmd.py +205 -0
  26. dotman_git-1.4.0/tests/test_vault.py +281 -0
  27. dotman_git-1.4.0/tests/test_vault_cmd.py +165 -0
  28. dotman_git-1.4.0/tests/test_verify_cmd.py +276 -0
  29. dotman_git-1.3.1/dot_man/completions/dot-man.fish +0 -58
  30. dotman_git-1.3.1/dot_man/completions/install.sh +0 -26
  31. dotman_git-1.3.1/tests/test_export_cmd.py +0 -94
  32. dotman_git-1.3.1/tests/test_tag_cmd.py +0 -105
  33. dotman_git-1.3.1/tests/test_vault.py +0 -148
  34. dotman_git-1.3.1/tests/test_verify_cmd.py +0 -93
  35. {dotman_git-1.3.1 → dotman_git-1.4.0}/CONTRIBUTING.md +0 -0
  36. {dotman_git-1.3.1 → dotman_git-1.4.0}/LICENSE +0 -0
  37. {dotman_git-1.3.1 → dotman_git-1.4.0}/MANIFEST.in +0 -0
  38. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/backups.py +0 -0
  39. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/branch_ops.py +0 -0
  40. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/__init__.py +0 -0
  41. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/add_cmd.py +0 -0
  42. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/audit_cmd.py +0 -0
  43. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/backup_cmd.py +0 -0
  44. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/bootstrap_cmd.py +0 -0
  45. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/branch_cmd.py +0 -0
  46. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/clean_cmd.py +0 -0
  47. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/completions.py +0 -0
  48. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/config_cmd.py +0 -0
  49. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/config_tutorial.py +0 -0
  50. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/deploy_cmd.py +0 -0
  51. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/discover_cmd.py +0 -0
  52. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/doctor_cmd.py +0 -0
  53. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/edit_cmd.py +0 -0
  54. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/encrypt_cmd.py +0 -0
  55. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/export_cmd.py +0 -0
  56. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/hooks_cmd.py +0 -0
  57. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/import_cmd.py +0 -0
  58. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/init_cmd.py +0 -0
  59. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/interface.py +0 -0
  60. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/log_cmd.py +0 -0
  61. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/navigate_preview.py +0 -0
  62. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/onboarding.py +0 -0
  63. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/profile_cmd.py +0 -0
  64. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/remote_cmd.py +0 -0
  65. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/restore_cmd.py +0 -0
  66. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/revert_cmd.py +0 -0
  67. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/rollback_cmd.py +0 -0
  68. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/show_cmd.py +0 -0
  69. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/status_cmd.py +0 -0
  70. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/switch_cmd.py +0 -0
  71. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/tag_cmd.py +0 -0
  72. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/template_cmd.py +0 -0
  73. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/tui_cmd.py +0 -0
  74. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/vault_cmd.py +0 -0
  75. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/verify_cmd.py +0 -0
  76. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/watch_cmd.py +0 -0
  77. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/completions/_dot-man.zsh +0 -0
  78. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/completions/dot-man.bash +0 -0
  79. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/config.py +0 -0
  80. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/config_detector.py +0 -0
  81. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/constants.py +0 -0
  82. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/core.py +0 -0
  83. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/encryption.py +0 -0
  84. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/exceptions.py +0 -0
  85. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/files.py +0 -0
  86. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/global_config.py +0 -0
  87. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/hooks.py +0 -0
  88. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/interactive.py +0 -0
  89. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/lock.py +0 -0
  90. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/merge.py +0 -0
  91. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/operations.py +0 -0
  92. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/py.typed +0 -0
  93. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/save_deploy_ops.py +0 -0
  94. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/secrets.py +0 -0
  95. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/status_ops.py +0 -0
  96. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/tui_log.py +0 -0
  97. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/ui.py +0 -0
  98. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/utils.py +0 -0
  99. {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/vault.py +0 -0
  100. {dotman_git-1.3.1 → dotman_git-1.4.0}/dotman_git.egg-info/dependency_links.txt +0 -0
  101. {dotman_git-1.3.1 → dotman_git-1.4.0}/dotman_git.egg-info/entry_points.txt +0 -0
  102. {dotman_git-1.3.1 → dotman_git-1.4.0}/dotman_git.egg-info/requires.txt +0 -0
  103. {dotman_git-1.3.1 → dotman_git-1.4.0}/dotman_git.egg-info/top_level.txt +0 -0
  104. {dotman_git-1.3.1 → dotman_git-1.4.0}/setup.cfg +0 -0
  105. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_add_cmd.py +0 -0
  106. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_audit_cmd.py +0 -0
  107. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_backup_cmd.py +0 -0
  108. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_backups.py +0 -0
  109. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_branch_cmd.py +0 -0
  110. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_branch_ops.py +0 -0
  111. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_clean.py +0 -0
  112. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_cli_commands.py +0 -0
  113. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_cli_extra.py +0 -0
  114. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_cli_help.py +0 -0
  115. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_cli_revert.py +0 -0
  116. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_cli_v1.py +0 -0
  117. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_common_v1.py +0 -0
  118. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_completion.py +0 -0
  119. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_comprehensive.py +0 -0
  120. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_config.py +0 -0
  121. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_config_cmd.py +0 -0
  122. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_config_cmd_v1.py +0 -0
  123. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_config_ops_v1.py +0 -0
  124. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_config_preservation.py +0 -0
  125. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_config_section.py +0 -0
  126. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_core.py +0 -0
  127. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_core_extended.py +0 -0
  128. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_core_v1.py +0 -0
  129. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_custom_secrets.py +0 -0
  130. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_deploy_cmd.py +0 -0
  131. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_discover_cmd.py +0 -0
  132. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_doctor_cmd.py +0 -0
  133. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_dotman_config.py +0 -0
  134. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_edit_cmd.py +0 -0
  135. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_encrypt_cmd.py +0 -0
  136. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_encryption.py +0 -0
  137. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_exceptions.py +0 -0
  138. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_files_atomic.py +0 -0
  139. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_files_comprehensive.py +0 -0
  140. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_files_extended.py +0 -0
  141. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_final_push.py +0 -0
  142. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_fixtures.py +0 -0
  143. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_git_manager.py +0 -0
  144. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_git_manager_extended.py +0 -0
  145. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_global_config.py +0 -0
  146. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_hooks.py +0 -0
  147. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_hooks_new.py +0 -0
  148. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_import_cmd.py +0 -0
  149. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_init_cmd.py +0 -0
  150. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_integration.py +0 -0
  151. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_interactive.py +0 -0
  152. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_lock.py +0 -0
  153. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_log_checkout_tag.py +0 -0
  154. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_log_cmd.py +0 -0
  155. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_log_cmd_v1.py +0 -0
  156. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_merge.py +0 -0
  157. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_more_integration.py +0 -0
  158. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_navigate_cmd.py +0 -0
  159. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_navigate_helpers.py +0 -0
  160. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_navigate_switch_v1.py +0 -0
  161. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_onboarding.py +0 -0
  162. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_operations.py +0 -0
  163. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_ops_v1.py +0 -0
  164. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_performance_logic.py +0 -0
  165. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_profile_cmd.py +0 -0
  166. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_profile_cmd_v1.py +0 -0
  167. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_profile_extra.py +0 -0
  168. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_remote_cmd.py +0 -0
  169. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_restore_cmd.py +0 -0
  170. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_rollback_cmd.py +0 -0
  171. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_save_cmd.py +0 -0
  172. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_save_deploy_ops.py +0 -0
  173. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_secrets.py +0 -0
  174. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_section.py +0 -0
  175. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_shell_completions.py +0 -0
  176. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_show_cmd.py +0 -0
  177. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_status_cmd.py +0 -0
  178. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_status_ops.py +0 -0
  179. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_switch_cmd.py +0 -0
  180. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_switch_enhancements.py +0 -0
  181. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_symlink_deploy.py +0 -0
  182. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_template_cmd.py +0 -0
  183. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_template_cmd_v1.py +0 -0
  184. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_template_extra.py +0 -0
  185. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_ui.py +0 -0
  186. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_utils.py +0 -0
  187. {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_watch_cmd.py +0 -0
@@ -7,6 +7,50 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Added
11
+
12
+ - **Shell completions for `add` command** — file/directory path completion using `@completes("add", param="path")` decorator
13
+ - **Shell completions for `bootstrap` command** — package manager name completion (`brew`, `apt`, `dnf`, `pacman`, etc.) via `@completes("bootstrap", param="pm")` decorator
14
+ - **19 new documentation pages** — dedicated pages for `init`, `add`, `deploy`, `tag`, `log`, `diff`, `revert`, `import`, `export`, `discover`, `encrypt`, `hooks`, `template`, `profile`, `watch`, `rollback`, `backup`, `verify`, `completions` commands
15
+ - **Docs navigation restructured** — commands grouped by category (Core, Configuration, Inspection, Tags, Encryption, Import/Export, Hooks, Shell)
16
+ - **Roadmap updated** — consistent with v1.4.0 state, removed stale duplicate sections
17
+
18
+ ### Fixed
19
+
20
+ - **`save_cmd.py`** — fixed `_sync_shared_sections` → `sync_shared_sections` name mismatch that caused `NameError` on `--commit` flag
21
+ - **`shared_sync.py`** — `_sync_branch` now records overridden sections in `report.skipped` even when other shared sections are still wanted (partial overrides)
22
+ - **`test_shared_sync.py`** — fixed test expectations for `test_edits_reach_non_overriding_branches` (work overrides fish, so only server gets propagated), `test_unparsable_target_config_is_skipped_safely` (fixed `run_git()` call to use `subprocess.run` with `input=`), `test_source_branch_never_modified` (fixed log assertion to use `-1` flag)
23
+ - **Tests** — all 1700 tests passing (up from 1691), 1 skipped
24
+
25
+ ### Changed
26
+
27
+ - **Repository cleanup** — single source of truth for completion scripts (`dot_man/completions/`); removed the duplicated `scripts/completions/` copies and the unused packaged `install.sh`. `install.sh` now installs completions via the built-in `dot-man completions` command. Removed the stale `docs/development/roadmap.md` duplicate and surfaced `docs/roadmap.md` in the docs site nav.
28
+
29
+ ### Removed
30
+
31
+ - **Tracked artifacts** — dropped `coverage.json` (256 KB CI artifact) and the author's personal `config.toml` from version control; both now gitignored along with benchmark output.
32
+ - **Local clutter** — `.gitignore` fixed (broken merged pattern) and extended: coverage reports, benchmarks, stale build/dist/site outputs no longer pollute the working tree.
33
+
34
+ ## [1.4.0] - 2026-08-25
35
+
36
+ ### Added
37
+
38
+ - **Decorator-driven completion engine** — new `dot_man/cli/completion_engine.py` gathers commands, subcommands, options, accepted values (static or dynamic) and defaults from the Click CLI definition itself. Extended via the `@completes(path, param=...)` decorator registry; a `(path, param)` registration wins over the generic one.
39
+ - **`dot-man --complete <shell>` protocol** — handled before onboarding/git/side effects in `main()`. Fish output uses `value<TAB>description` lines so descriptions render natively.
40
+ - **Defaults & choices surfaced** — option completions now show `[choices: bash|zsh|fish|all]` and `[default: all]` inline (e.g. `dot-man completions --s<TAB>`).
41
+ - **Alias labeling** — shell shortcuts like `nav`, `cpl`, `aud` are listed as `(alias)` entries instead of indistinguishable duplicates.
42
+ - **`--option=<TAB>` form** — completing after `=` emits full tokens (`--shell=fish`).
43
+ - **Static choice provider** — `config set KEY <TAB>` suggests `true`/`false`.
44
+
45
+ ### Fixed
46
+
47
+ - **Fish completions showed invalid/stale choices** — the hand-written `dot-man.fish` listed missing commands (`backup`, `tag`, `template`, `profile`, `doctor`, ...), treated navigate options as subcommands, and kept deprecated `switch` prominent. It is now a tiny delegating script calling `dot-man --complete fish` on every TAB (wifi-tracker style), so completions can never go stale.
48
+ - **Unformatted fish output** — the old script's naive `string split ","` broke on help text containing commas and dropped descriptions. Tab-separated engine output renders descriptions natively in fish.
49
+
50
+ ### Tests
51
+
52
+ - 37 new tests in `tests/test_completion_engine.py` (registry priority, root/subcommand listing, static choices, defaults display, alias labeling, eq-form values, dynamic git-backed callbacks, request parsing, formatting, entry safety). Suite: 1682 passed, coverage 84% (up from 81%).
53
+
10
54
  ## [1.3.0] - 2026-06-27
11
55
 
12
56
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dotman-git
3
- Version: 1.3.1
3
+ Version: 1.4.0
4
4
  Summary: Dotfile manager with git-powered branching
5
5
  Author-email: Bishoy Ehab <beshoyehabhhmm@gmail.com>
6
6
  License: MIT
@@ -63,12 +63,12 @@ Dynamic: license-file
63
63
  [![CI](https://github.com/BeshoyEhab/dot-man/actions/workflows/ci.yml/badge.svg)](https://github.com/BeshoyEhab/dot-man/actions/workflows/ci.yml)
64
64
  [![PyPI version](https://img.shields.io/pypi/v/dotman-git?color=blue&logo=pypi&logoColor=white)](https://pypi.org/project/dotman-git/)
65
65
  [![PyPI downloads](https://img.shields.io/pypi/dm/dotman-git?color=blue&logo=pypi&logoColor=white)](https://pypi.org/project/dotman-git/)
66
- [![Python](https://img.shields.io/pypi/pyversions/dotman-git?logo=python&logoColor=white)](https://pypi.org/project/dotman-git/)
66
+ [![Python](https://img.shields.io/pypi/pyversions/dotman-git?logo=python&logoColor=white&v=2)](https://pypi.org/project/dotman-git/)
67
67
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
68
68
  [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
69
69
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
70
70
  [![Checked with mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/)
71
- [![Coverage](https://img.shields.io/badge/coverage-83%25-green)](https://github.com/BeshoyEhab/dot-man)
71
+ [![Coverage](https://img.shields.io/badge/coverage-84%25-green)](https://github.com/BeshoyEhab/dot-man)
72
72
  [![Docs](https://img.shields.io/badge/docs-online-blue)](https://beshoyehab.github.io/dot-man/)
73
73
  [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/BeshoyEhab/dot-man/pulls)
74
74
 
@@ -125,7 +125,7 @@ Your Machine dot-man Repo Any Machine
125
125
  <td>
126
126
 
127
127
  **Developer**
128
- - 🐚 Shell completions (bash/zsh/fish)
128
+ - 🐚 Shell completions (bash/zsh/fish) — decorator-driven engine, always in sync with the CLI
129
129
  - 🎨 Syntax-highlighted rich diffs
130
130
  - 🩺 `doctor` and `verify` diagnostics
131
131
  - 📦 PyPI — `pip install dotman-git`
@@ -181,7 +181,6 @@ dot-man add ~/.config/nvim
181
181
 
182
182
  # 3. Create a "work" profile (branch)
183
183
  dot-man navigate work
184
- # btw it was "switch" instead of "navigate" untill Bishoy decided to change it to "navigate" for some silly reasons and push that changes so as community help us to make it "switch" again.
185
184
 
186
185
  # 4. Edit your work-specific configs, then switch back — changes are saved automatically
187
186
  dot-man navigate main
@@ -480,10 +479,10 @@ System variables (`{{HOSTNAME}}`, `{{USER}}`, `{{SHELL}}`, etc.) are auto-popula
480
479
 
481
480
  | Metric | Value |
482
481
  |--------|-------|
483
- | Version | `1.2.3` |
482
+ | Version | `1.4.0` |
484
483
  | Python | `3.10 · 3.11 · 3.12 · 3.13` |
485
484
  | Platforms | Linux, macOS |
486
- | Test Coverage | 81% (1526 tests) |
485
+ | Test Coverage | 84% (1682 tests) |
487
486
  | Commands | 30+ |
488
487
  | PyPI | [`dotman-git`](https://pypi.org/project/dotman-git/) |
489
488
 
@@ -7,12 +7,12 @@
7
7
  [![CI](https://github.com/BeshoyEhab/dot-man/actions/workflows/ci.yml/badge.svg)](https://github.com/BeshoyEhab/dot-man/actions/workflows/ci.yml)
8
8
  [![PyPI version](https://img.shields.io/pypi/v/dotman-git?color=blue&logo=pypi&logoColor=white)](https://pypi.org/project/dotman-git/)
9
9
  [![PyPI downloads](https://img.shields.io/pypi/dm/dotman-git?color=blue&logo=pypi&logoColor=white)](https://pypi.org/project/dotman-git/)
10
- [![Python](https://img.shields.io/pypi/pyversions/dotman-git?logo=python&logoColor=white)](https://pypi.org/project/dotman-git/)
10
+ [![Python](https://img.shields.io/pypi/pyversions/dotman-git?logo=python&logoColor=white&v=2)](https://pypi.org/project/dotman-git/)
11
11
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
12
12
  [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
13
13
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
14
14
  [![Checked with mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/)
15
- [![Coverage](https://img.shields.io/badge/coverage-83%25-green)](https://github.com/BeshoyEhab/dot-man)
15
+ [![Coverage](https://img.shields.io/badge/coverage-84%25-green)](https://github.com/BeshoyEhab/dot-man)
16
16
  [![Docs](https://img.shields.io/badge/docs-online-blue)](https://beshoyehab.github.io/dot-man/)
17
17
  [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/BeshoyEhab/dot-man/pulls)
18
18
 
@@ -69,7 +69,7 @@ Your Machine dot-man Repo Any Machine
69
69
  <td>
70
70
 
71
71
  **Developer**
72
- - 🐚 Shell completions (bash/zsh/fish)
72
+ - 🐚 Shell completions (bash/zsh/fish) — decorator-driven engine, always in sync with the CLI
73
73
  - 🎨 Syntax-highlighted rich diffs
74
74
  - 🩺 `doctor` and `verify` diagnostics
75
75
  - 📦 PyPI — `pip install dotman-git`
@@ -125,7 +125,6 @@ dot-man add ~/.config/nvim
125
125
 
126
126
  # 3. Create a "work" profile (branch)
127
127
  dot-man navigate work
128
- # btw it was "switch" instead of "navigate" untill Bishoy decided to change it to "navigate" for some silly reasons and push that changes so as community help us to make it "switch" again.
129
128
 
130
129
  # 4. Edit your work-specific configs, then switch back — changes are saved automatically
131
130
  dot-man navigate main
@@ -424,10 +423,10 @@ System variables (`{{HOSTNAME}}`, `{{USER}}`, `{{SHELL}}`, etc.) are auto-popula
424
423
 
425
424
  | Metric | Value |
426
425
  |--------|-------|
427
- | Version | `1.2.3` |
426
+ | Version | `1.4.0` |
428
427
  | Python | `3.10 · 3.11 · 3.12 · 3.13` |
429
428
  | Platforms | Linux, macOS |
430
- | Test Coverage | 81% (1526 tests) |
429
+ | Test Coverage | 84% (1682 tests) |
431
430
  | Commands | 30+ |
432
431
  | PyPI | [`dotman-git`](https://pypi.org/project/dotman-git/) |
433
432
 
@@ -1,4 +1,4 @@
1
1
  """dot-man: Dotfile manager with git-powered branching."""
2
2
 
3
- __version__ = "1.3.1"
3
+ __version__ = "1.4.0"
4
4
  __author__ = "Bishoy Ehab"
@@ -111,6 +111,28 @@ class DotManGroup(click.Group):
111
111
  ctx.exit(2)
112
112
 
113
113
 
114
+ def sync_shared_sections(ops) -> None:
115
+ """Propagate ``shared = true`` sections to non-overriding branches.
116
+
117
+ Called after a commit was created on the current branch. Failures
118
+ are reported but never abort the surrounding command.
119
+ """
120
+ try:
121
+ from ..shared_sync import SharedSectionSync
122
+
123
+ report = SharedSectionSync(ops).sync()
124
+ if not report.shared_sections:
125
+ return
126
+ for branch, sections in report.propagated.items():
127
+ ui.console.print(
128
+ f" [dim]↔ shared → '{branch}': {', '.join(sections)}[/dim]"
129
+ )
130
+ for branch, reason in report.errors.items():
131
+ warn(f"Shared-section sync failed for '{branch}': {reason}")
132
+ except Exception as e:
133
+ logging.debug("Shared-section sync skipped: %s", e)
134
+
135
+
114
136
  def require_init(func):
115
137
  """Decorator to require initialization before running command."""
116
138
 
@@ -0,0 +1,445 @@
1
+ """Decorator-driven shell completion engine for dot-man.
2
+
3
+ Single source of truth for shell completions: commands, subcommands,
4
+ options, accepted values (static or dynamic) and defaults are gathered
5
+ from the Click CLI definition itself, extended via the :func:`completes`
6
+ decorator registry. The fish completion script delegates every TAB press
7
+ to ``dot-man --complete fish`` so completions can never go stale.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ import sys
14
+ from collections.abc import Callable, Iterable
15
+ from dataclasses import dataclass, field
16
+
17
+ import click
18
+
19
+ PROG_NAME = "dot-man"
20
+
21
+ CompletionEntry = tuple[str, str]
22
+ ProviderResult = Iterable[
23
+ "str | tuple[str, str] | click.shell_completion.CompletionItem"
24
+ ]
25
+ ProviderFn = Callable[["CompletionContext"], ProviderResult]
26
+
27
+
28
+ @dataclass
29
+ class CompletionContext:
30
+ """State passed to value providers registered with :func:`completes`."""
31
+
32
+ args: list[str] = field(default_factory=list)
33
+ incomplete: str = ""
34
+ command_path: str = ""
35
+
36
+
37
+ _PROVIDERS: dict[tuple[str, str], ProviderFn] = {}
38
+
39
+
40
+ def completes(command_path: str, param: str | None = None):
41
+ """Register a value provider for a command path (and optional param).
42
+
43
+ The decorated callable receives a :class:`CompletionContext` and may
44
+ return plain strings, ``(value, description)`` tuples or Click
45
+ ``CompletionItem`` objects. A ``(path, param)`` registration wins
46
+ over the generic ``(path, None)`` one.
47
+
48
+ Example::
49
+
50
+ @completes("config set", param="value")
51
+ def _config_values(ctx):
52
+ return ["true", "false"]
53
+ """
54
+
55
+ def decorator(fn: ProviderFn) -> ProviderFn:
56
+ _PROVIDERS[(command_path, param or "")] = fn
57
+ return fn
58
+
59
+ return decorator
60
+
61
+
62
+ def _normalize(result: ProviderResult) -> list[CompletionEntry]:
63
+ """Normalize provider output to (value, description) pairs."""
64
+ pairs: list[CompletionEntry] = []
65
+ if result is None:
66
+ return pairs
67
+ for item in result:
68
+ if isinstance(item, str):
69
+ pairs.append((item, ""))
70
+ elif isinstance(item, tuple):
71
+ pairs.append((item[0], item[1]))
72
+ else:
73
+ pairs.append((item.value, item.help or ""))
74
+ return pairs
75
+
76
+
77
+ class CompletionEngine:
78
+ """Gathers completions by walking the Click command tree."""
79
+
80
+ def __init__(self, cli_root: click.Command, prog_name: str = PROG_NAME):
81
+ self.cli = cli_root
82
+ self.prog_name = prog_name
83
+
84
+ def complete(self, args: list[str], incomplete: str) -> list[CompletionEntry]:
85
+ """Return (value, description) pairs for the given word list.
86
+
87
+ ``args`` holds the words before the word being completed (the
88
+ leading program name is optional and ignored), ``incomplete`` is
89
+ the partial word itself.
90
+ """
91
+ try:
92
+ args = self._strip_prog_name(args)
93
+ chain, rest = self._walk(args)
94
+
95
+ if incomplete.startswith("-") and "--" not in rest:
96
+ if "=" in incomplete:
97
+ entries = self._split_option_value(chain, args, incomplete)
98
+ else:
99
+ entries = self._option_names(chain, args, incomplete)
100
+ elif chain:
101
+ entries = self._word_values(chain, args, incomplete, rest)
102
+ else:
103
+ entries = []
104
+ return self._filter(entries, incomplete)
105
+ except Exception as e:
106
+ logging.debug("Completion engine error: %s", e)
107
+ return []
108
+
109
+ def _strip_prog_name(self, args: list[str]) -> list[str]:
110
+ if args and (
111
+ args[0] == self.prog_name or args[0].endswith("/" + self.prog_name)
112
+ ):
113
+ return args[1:]
114
+ return args
115
+
116
+ def _walk(self, args: list[str]) -> tuple[list[click.Command], list[str]]:
117
+ """Follow command words through nested groups.
118
+
119
+ Returns the resolved command chain (root first) and the words
120
+ left over after the deepest command was reached. Options
121
+ interleaved with subcommand names are skipped, consuming one
122
+ following token when they take a value.
123
+ """
124
+ cmd: click.Command = self.cli
125
+ chain = [cmd]
126
+ rest = list(args)
127
+ ctx = click.Context(cmd, info_name=self.prog_name, resilient_parsing=True)
128
+
129
+ while isinstance(cmd, click.Group) and rest:
130
+ word = rest[0]
131
+ if word == "--":
132
+ break
133
+ if word.startswith("-"):
134
+ opt = self._find_option(cmd, ctx, word)
135
+ rest.pop(0)
136
+ if (
137
+ opt is not None
138
+ and not opt.is_flag
139
+ and rest
140
+ and not rest[0].startswith("-")
141
+ ):
142
+ rest.pop(0)
143
+ continue
144
+ sub = click.Group.get_command(cmd, ctx, word)
145
+ if sub is None:
146
+ break
147
+ rest.pop(0)
148
+ chain.append(sub)
149
+ cmd = sub
150
+ return chain, rest
151
+
152
+ def _find_option(
153
+ self, cmd: click.Command, ctx: click.Context, word: str
154
+ ) -> click.Option | None:
155
+ name = word.split("=", 1)[0]
156
+ for param in cmd.get_params(ctx):
157
+ if isinstance(param, click.Option) and name in (
158
+ *param.opts,
159
+ *param.secondary_opts,
160
+ ):
161
+ return param
162
+ return None
163
+
164
+ def _describe_option(self, param: click.Option, ctx: click.Context) -> str:
165
+ """Build the description shown next to an option name.
166
+
167
+ Includes the help text plus the accepted choices and the
168
+ declared default when applicable.
169
+ """
170
+ desc = param.help or ""
171
+ choices = getattr(param.type, "choices", None)
172
+ if choices:
173
+ desc = f"{desc} [choices: {'|'.join(str(c) for c in choices)}]".strip()
174
+ default = param.get_default(ctx, call=False)
175
+ if default is not None and not param.is_flag:
176
+ desc = f"{desc} [default: {default}]".strip()
177
+ return desc
178
+
179
+ def _option_names(
180
+ self, chain: list[click.Command], args: list[str], incomplete: str
181
+ ) -> list[CompletionEntry]:
182
+ """Complete option names of the current command and the root."""
183
+ cmd = chain[-1]
184
+ ctx = click.Context(cmd, info_name=cmd.name, resilient_parsing=True)
185
+
186
+ entries: list[CompletionEntry] = []
187
+ seen: set[int] = set()
188
+ for owner in {id(cmd): cmd, id(self.cli): self.cli}.values():
189
+ for param in owner.get_params(ctx):
190
+ if (
191
+ id(param) in seen
192
+ or not isinstance(param, click.Option)
193
+ or param.hidden
194
+ ):
195
+ continue
196
+ seen.add(id(param))
197
+ if not param.multiple and any(o in args for o in param.opts):
198
+ continue
199
+ desc = self._describe_option(param, ctx)
200
+ for name in (*param.opts, *param.secondary_opts):
201
+ if name.startswith(incomplete):
202
+ entries.append((name, desc))
203
+ return entries
204
+
205
+ def _split_option_value(
206
+ self, chain: list[click.Command], args: list[str], incomplete: str
207
+ ) -> list[CompletionEntry]:
208
+ """Complete ``--option=<TAB>`` style values, emitting full tokens."""
209
+ name, _, _prefix = incomplete.partition("=")
210
+ cmd = chain[-1]
211
+ path = self._path_of(chain)
212
+ ctx = click.Context(cmd, info_name=cmd.name, resilient_parsing=True)
213
+ opt = self._find_option(cmd, ctx, name)
214
+ if opt is None or opt.is_flag:
215
+ return []
216
+ values = self._values_for_param(cmd, path, opt, ctx, _prefix, args)
217
+ return [(f"{name}={v}", d) for v, d in values]
218
+
219
+ def _word_values(
220
+ self,
221
+ chain: list[click.Command],
222
+ args: list[str],
223
+ incomplete: str,
224
+ rest: list[str],
225
+ ) -> list[CompletionEntry]:
226
+ """Complete option values, argument values or subcommands."""
227
+ cmd = chain[-1]
228
+ path = self._path_of(chain)
229
+ ctx = click.Context(cmd, info_name=cmd.name, resilient_parsing=True)
230
+
231
+ prev = args[-1] if args else ""
232
+ if prev.startswith("-") and prev != "--":
233
+ opt = self._find_option(cmd, ctx, prev)
234
+ if opt is not None and not opt.is_flag:
235
+ return self._values_for_param(cmd, path, opt, ctx, incomplete, args)
236
+
237
+ if isinstance(cmd, click.Group):
238
+ return self._subcommands(cmd, incomplete)
239
+
240
+ arg = self._next_argument(cmd, ctx, rest)
241
+ if arg is not None:
242
+ return self._values_for_param(cmd, path, arg, ctx, incomplete, args)
243
+ return []
244
+
245
+ @staticmethod
246
+ def _path_of(chain: list[click.Command]) -> str:
247
+ return " ".join(c.name or "" for c in chain[1:]).strip()
248
+
249
+ def _subcommands(
250
+ self, group: click.Group, incomplete: str
251
+ ) -> list[CompletionEntry]:
252
+ """List subcommands; aliases are labeled and sorted last."""
253
+ primaries: list[CompletionEntry] = []
254
+ aliases: list[CompletionEntry] = []
255
+ for name, sub in group.commands.items():
256
+ if sub.hidden:
257
+ continue
258
+ short = sub.get_short_help_str(limit=60)
259
+ if name == sub.name:
260
+ primaries.append((name, short))
261
+ elif short:
262
+ aliases.append((name, f"(alias) {short}"))
263
+ return primaries + aliases
264
+
265
+ def _next_argument(
266
+ self, cmd: click.Command, ctx: click.Context, rest: list[str]
267
+ ) -> click.Argument | None:
268
+ """Find the argument slot matching the number of given values."""
269
+ positional = [w for w in rest if not w.startswith("-")]
270
+ arguments = [p for p in cmd.get_params(ctx) if isinstance(p, click.Argument)]
271
+ index = len(positional)
272
+ if 0 <= index < len(arguments):
273
+ return arguments[index]
274
+ if arguments and arguments[-1].nargs == -1:
275
+ return arguments[-1]
276
+ return None
277
+
278
+ def _values_for_param(
279
+ self,
280
+ cmd: click.Command,
281
+ path: str,
282
+ param: click.Parameter,
283
+ ctx: click.Context,
284
+ incomplete: str,
285
+ args: list[str],
286
+ ) -> list[CompletionEntry]:
287
+ """Values for an option/argument slot.
288
+
289
+ Priority: decorator-registered provider, static ``click.Choice``
290
+ choices, then the parameter's own completion callback (the
291
+ dynamic branches/tags/commits providers attached throughout the
292
+ CLI).
293
+ """
294
+ specific = _PROVIDERS.get((path, param.name or ""))
295
+ if specific is None:
296
+ specific = _PROVIDERS.get((path, ""))
297
+ if specific is not None:
298
+ try:
299
+ ctx_info = CompletionContext(
300
+ args=args, incomplete=incomplete, command_path=path
301
+ )
302
+ return _normalize(specific(ctx_info))
303
+ except Exception as e:
304
+ logging.debug("Provider %s failed: %s", path, e)
305
+ return []
306
+
307
+ choices = getattr(param.type, "choices", None)
308
+ if choices is not None:
309
+ return [(str(c), "") for c in choices]
310
+
311
+ try:
312
+ return _normalize(param.shell_complete(ctx, incomplete))
313
+ except Exception as e:
314
+ logging.debug(
315
+ "Completion callback failed for %s %s: %s", path, param.name, e
316
+ )
317
+ return []
318
+
319
+ @staticmethod
320
+ def _filter(
321
+ entries: list[CompletionEntry], incomplete: str
322
+ ) -> list[CompletionEntry]:
323
+ seen: set[str] = set()
324
+ out: list[CompletionEntry] = []
325
+ for value, desc in entries:
326
+ if not value.startswith(incomplete) or value in seen:
327
+ continue
328
+ seen.add(value)
329
+ out.append((value, desc))
330
+ out.sort(key=lambda pair: pair[0].lower())
331
+ return out
332
+
333
+
334
+ def format_fish(pairs: list[CompletionEntry]) -> str:
335
+ """Format entries as fish lines: ``value<TAB>description``."""
336
+ return "\n".join(f"{v}\t{d}" if d else v for v, d in pairs)
337
+
338
+
339
+ def parse_request(argv: list[str]) -> tuple[list[str], str] | None:
340
+ """Extract (words, incomplete) from a ``--complete`` invocation.
341
+
342
+ Expected shape::
343
+
344
+ dot-man --complete <shell> [program words...] -- <incomplete>
345
+
346
+ Returns None when argv is not a valid completion request.
347
+ """
348
+ try:
349
+ idx = argv.index("--complete")
350
+ rest = argv[idx + 2 :]
351
+ except (ValueError, IndexError):
352
+ return None
353
+ if not argv[idx + 1 : idx + 2]:
354
+ return None
355
+
356
+ if "--" in rest:
357
+ sep = len(rest) - 1 - rest[::-1].index("--")
358
+ incomplete = rest[sep + 1] if sep + 1 < len(rest) else ""
359
+ return rest[:sep], incomplete
360
+ if not rest:
361
+ return [], ""
362
+ return rest[:-1], rest[-1]
363
+
364
+
365
+ def run_completion_request(argv: list[str]) -> int:
366
+ """Run the engine for a ``--complete`` invocation and print results."""
367
+ request = parse_request(argv)
368
+ if request is None:
369
+ return 1
370
+ args, incomplete = request
371
+
372
+ from .interface import cli
373
+
374
+ engine = CompletionEngine(cli)
375
+ pairs = engine.complete(args, incomplete)
376
+
377
+ shell = argv[argv.index("--complete") + 1]
378
+ if shell == "fish":
379
+ output = format_fish(pairs)
380
+ else:
381
+ output = "\n".join(v for v, _ in pairs)
382
+ if output:
383
+ click.echo(output)
384
+ return 0
385
+
386
+
387
+ def maybe_run_completion(argv: list[str] | None = None) -> None:
388
+ """Run the completion engine and exit when ``--complete`` is requested.
389
+
390
+ Called from the CLI entry point before anything else so completions
391
+ never trigger onboarding, git access or other side effects.
392
+ """
393
+ argv = list(sys.argv if argv is None else argv)
394
+ if "--complete" not in argv:
395
+ return
396
+ sys.exit(run_completion_request(argv))
397
+
398
+
399
+ @completes("config set", param="value")
400
+ def _complete_config_value(ctx: CompletionContext) -> ProviderResult:
401
+ """Static choices for boolean config values."""
402
+ return ["true", "false"]
403
+
404
+
405
+ @completes("add", param="path")
406
+ def _complete_add_path(ctx: CompletionContext) -> ProviderResult:
407
+ """Complete local file/directory paths for the add command."""
408
+ import os
409
+ from pathlib import Path
410
+
411
+ incomplete = ctx.incomplete or "."
412
+ expanded = os.path.expanduser(incomplete)
413
+ parent = Path(expanded).parent if expanded else Path(".")
414
+ prefix = Path(expanded).name if expanded else ""
415
+
416
+ if not parent.exists():
417
+ return []
418
+
419
+ results: list[str] = []
420
+ try:
421
+ for entry in sorted(parent.iterdir()):
422
+ if entry.name.startswith(prefix) and not entry.name.startswith("."):
423
+ if incomplete.startswith("~"):
424
+ display = str(entry).replace(str(Path.home()), "~", 1)
425
+ else:
426
+ display = str(entry)
427
+ results.append(display + "/" if entry.is_dir() else display)
428
+ except PermissionError:
429
+ pass
430
+ return results
431
+
432
+
433
+ @completes("bootstrap", param="pm")
434
+ def _complete_bootstrap_pm(ctx: CompletionContext) -> ProviderResult:
435
+ """Complete package manager names for the bootstrap command."""
436
+ return [
437
+ ("brew", "macOS package manager"),
438
+ ("apt", "Debian/Ubuntu"),
439
+ ("dnf", "Fedora/RHEL"),
440
+ ("pacman", "Arch Linux"),
441
+ ("zypper", "openSUSE"),
442
+ ("nix-env", "NixOS"),
443
+ ("xbps-install", "Void Linux"),
444
+ ("pkg", "FreeBSD"),
445
+ ]