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.
- {dotman_git-1.3.1 → dotman_git-1.4.0}/CHANGELOG.md +44 -0
- {dotman_git-1.3.1/dotman_git.egg-info → dotman_git-1.4.0}/PKG-INFO +6 -7
- {dotman_git-1.3.1 → dotman_git-1.4.0}/README.md +5 -6
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/__init__.py +1 -1
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/common.py +22 -0
- dotman_git-1.4.0/dot_man/cli/completion_engine.py +445 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/completions_cmd.py +36 -21
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/main.py +3 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/navigate_cmd.py +2 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/save_cmd.py +2 -0
- dotman_git-1.4.0/dot_man/completions/dot-man.fish +13 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/dotman_config.py +2 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/section.py +7 -0
- dotman_git-1.4.0/dot_man/shared_sync.py +301 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0/dotman_git.egg-info}/PKG-INFO +6 -7
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dotman_git.egg-info/SOURCES.txt +8 -1
- {dotman_git-1.3.1 → dotman_git-1.4.0}/pyproject.toml +1 -1
- dotman_git-1.4.0/tests/test_audit_cmd_extended.py +217 -0
- dotman_git-1.4.0/tests/test_bootstrap_cmd.py +269 -0
- dotman_git-1.4.0/tests/test_completion_engine.py +333 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_completions_cmd.py +33 -3
- dotman_git-1.4.0/tests/test_export_cmd.py +184 -0
- dotman_git-1.4.0/tests/test_revert_cmd_extended.py +106 -0
- dotman_git-1.4.0/tests/test_shared_sync.py +330 -0
- dotman_git-1.4.0/tests/test_tag_cmd.py +205 -0
- dotman_git-1.4.0/tests/test_vault.py +281 -0
- dotman_git-1.4.0/tests/test_vault_cmd.py +165 -0
- dotman_git-1.4.0/tests/test_verify_cmd.py +276 -0
- dotman_git-1.3.1/dot_man/completions/dot-man.fish +0 -58
- dotman_git-1.3.1/dot_man/completions/install.sh +0 -26
- dotman_git-1.3.1/tests/test_export_cmd.py +0 -94
- dotman_git-1.3.1/tests/test_tag_cmd.py +0 -105
- dotman_git-1.3.1/tests/test_vault.py +0 -148
- dotman_git-1.3.1/tests/test_verify_cmd.py +0 -93
- {dotman_git-1.3.1 → dotman_git-1.4.0}/CONTRIBUTING.md +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/LICENSE +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/MANIFEST.in +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/backups.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/branch_ops.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/__init__.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/add_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/audit_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/backup_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/bootstrap_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/branch_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/clean_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/completions.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/config_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/config_tutorial.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/deploy_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/discover_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/doctor_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/edit_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/encrypt_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/export_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/hooks_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/import_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/init_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/interface.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/log_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/navigate_preview.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/onboarding.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/profile_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/remote_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/restore_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/revert_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/rollback_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/show_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/status_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/switch_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/tag_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/template_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/tui_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/vault_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/verify_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/cli/watch_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/completions/_dot-man.zsh +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/completions/dot-man.bash +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/config.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/config_detector.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/constants.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/core.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/encryption.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/exceptions.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/files.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/global_config.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/hooks.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/interactive.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/lock.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/merge.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/operations.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/py.typed +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/save_deploy_ops.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/secrets.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/status_ops.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/tui_log.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/ui.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/utils.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dot_man/vault.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dotman_git.egg-info/dependency_links.txt +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dotman_git.egg-info/entry_points.txt +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dotman_git.egg-info/requires.txt +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/dotman_git.egg-info/top_level.txt +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/setup.cfg +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_add_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_audit_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_backup_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_backups.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_branch_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_branch_ops.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_clean.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_cli_commands.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_cli_extra.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_cli_help.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_cli_revert.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_cli_v1.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_common_v1.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_completion.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_comprehensive.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_config.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_config_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_config_cmd_v1.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_config_ops_v1.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_config_preservation.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_config_section.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_core.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_core_extended.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_core_v1.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_custom_secrets.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_deploy_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_discover_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_doctor_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_dotman_config.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_edit_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_encrypt_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_encryption.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_exceptions.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_files_atomic.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_files_comprehensive.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_files_extended.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_final_push.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_fixtures.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_git_manager.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_git_manager_extended.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_global_config.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_hooks.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_hooks_new.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_import_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_init_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_integration.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_interactive.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_lock.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_log_checkout_tag.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_log_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_log_cmd_v1.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_merge.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_more_integration.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_navigate_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_navigate_helpers.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_navigate_switch_v1.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_onboarding.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_operations.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_ops_v1.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_performance_logic.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_profile_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_profile_cmd_v1.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_profile_extra.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_remote_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_restore_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_rollback_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_save_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_save_deploy_ops.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_secrets.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_section.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_shell_completions.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_show_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_status_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_status_ops.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_switch_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_switch_enhancements.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_symlink_deploy.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_template_cmd.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_template_cmd_v1.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_template_extra.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_ui.py +0 -0
- {dotman_git-1.3.1 → dotman_git-1.4.0}/tests/test_utils.py +0 -0
- {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
|
+
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
|
[](https://github.com/BeshoyEhab/dot-man/actions/workflows/ci.yml)
|
|
64
64
|
[](https://pypi.org/project/dotman-git/)
|
|
65
65
|
[](https://pypi.org/project/dotman-git/)
|
|
66
|
-
[](https://pypi.org/project/dotman-git/)
|
|
66
|
+
[](https://pypi.org/project/dotman-git/)
|
|
67
67
|
[](https://opensource.org/licenses/MIT)
|
|
68
68
|
[](https://github.com/psf/black)
|
|
69
69
|
[](https://github.com/astral-sh/ruff)
|
|
70
70
|
[](https://mypy-lang.org/)
|
|
71
|
-
[](https://github.com/BeshoyEhab/dot-man)
|
|
72
72
|
[](https://beshoyehab.github.io/dot-man/)
|
|
73
73
|
[](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.
|
|
482
|
+
| Version | `1.4.0` |
|
|
484
483
|
| Python | `3.10 · 3.11 · 3.12 · 3.13` |
|
|
485
484
|
| Platforms | Linux, macOS |
|
|
486
|
-
| Test Coverage |
|
|
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
|
[](https://github.com/BeshoyEhab/dot-man/actions/workflows/ci.yml)
|
|
8
8
|
[](https://pypi.org/project/dotman-git/)
|
|
9
9
|
[](https://pypi.org/project/dotman-git/)
|
|
10
|
-
[](https://pypi.org/project/dotman-git/)
|
|
10
|
+
[](https://pypi.org/project/dotman-git/)
|
|
11
11
|
[](https://opensource.org/licenses/MIT)
|
|
12
12
|
[](https://github.com/psf/black)
|
|
13
13
|
[](https://github.com/astral-sh/ruff)
|
|
14
14
|
[](https://mypy-lang.org/)
|
|
15
|
-
[](https://github.com/BeshoyEhab/dot-man)
|
|
16
16
|
[](https://beshoyehab.github.io/dot-man/)
|
|
17
17
|
[](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.
|
|
426
|
+
| Version | `1.4.0` |
|
|
428
427
|
| Python | `3.10 · 3.11 · 3.12 · 3.13` |
|
|
429
428
|
| Platforms | Linux, macOS |
|
|
430
|
-
| Test Coverage |
|
|
429
|
+
| Test Coverage | 84% (1682 tests) |
|
|
431
430
|
| Commands | 30+ |
|
|
432
431
|
| PyPI | [`dotman-git`](https://pypi.org/project/dotman-git/) |
|
|
433
432
|
|
|
@@ -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
|
+
]
|