dotman-git 1.2.3__tar.gz → 1.3.1__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.2.3 → dotman_git-1.3.1}/CHANGELOG.md +25 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/PKG-INFO +5 -5
- {dotman_git-1.2.3 → dotman_git-1.3.1}/README.md +3 -2
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/__init__.py +1 -1
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/cli/__init__.py +6 -2
- dotman_git-1.3.1/dot_man/cli/bootstrap_cmd.py +240 -0
- dotman_git-1.3.1/dot_man/cli/common.py +268 -0
- dotman_git-1.2.3/dot_man/cli/common.py → dotman_git-1.3.1/dot_man/cli/completions.py +17 -300
- dotman_git-1.3.1/dot_man/cli/config_cmd.py +243 -0
- dotman_git-1.2.3/dot_man/cli/config_cmd.py → dotman_git-1.3.1/dot_man/cli/config_tutorial.py +114 -378
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/cli/doctor_cmd.py +4 -2
- dotman_git-1.3.1/dot_man/cli/hooks_cmd.py +66 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/cli/main.py +4 -2
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/cli/navigate_cmd.py +112 -362
- dotman_git-1.3.1/dot_man/cli/navigate_preview.py +189 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/cli/remote_cmd.py +1 -1
- dotman_git-1.3.1/dot_man/cli/save_cmd.py +188 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/cli/status_cmd.py +43 -6
- dotman_git-1.3.1/dot_man/cli/vault_cmd.py +93 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/core.py +8 -4
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/files.py +4 -4
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/global_config.py +49 -15
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/save_deploy_ops.py +22 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/section.py +9 -5
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/vault.py +43 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dotman_git.egg-info/PKG-INFO +5 -5
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dotman_git.egg-info/SOURCES.txt +10 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/pyproject.toml +10 -5
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_completion.py +9 -11
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_config_cmd.py +12 -9
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_doctor_cmd.py +4 -2
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_global_config.py +110 -0
- dotman_git-1.3.1/tests/test_navigate_helpers.py +197 -0
- dotman_git-1.3.1/tests/test_save_cmd.py +289 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_shell_completions.py +9 -11
- dotman_git-1.3.1/tests/test_status_cmd.py +234 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/CONTRIBUTING.md +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/LICENSE +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/MANIFEST.in +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/backups.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/branch_ops.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/cli/add_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/cli/audit_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/cli/backup_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/cli/branch_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/cli/clean_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/cli/completions_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/cli/deploy_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/cli/discover_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/cli/edit_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/cli/encrypt_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/cli/export_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/cli/import_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/cli/init_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/cli/interface.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/cli/log_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/cli/onboarding.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/cli/profile_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/cli/restore_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/cli/revert_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/cli/rollback_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/cli/show_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/cli/switch_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/cli/tag_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/cli/template_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/cli/tui_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/cli/verify_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/cli/watch_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/completions/_dot-man.zsh +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/completions/dot-man.bash +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/completions/dot-man.fish +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/completions/install.sh +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/config.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/config_detector.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/constants.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/dotman_config.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/encryption.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/exceptions.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/hooks.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/interactive.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/lock.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/merge.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/operations.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/py.typed +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/secrets.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/status_ops.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/tui_log.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/ui.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dot_man/utils.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dotman_git.egg-info/dependency_links.txt +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dotman_git.egg-info/entry_points.txt +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dotman_git.egg-info/requires.txt +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/dotman_git.egg-info/top_level.txt +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/setup.cfg +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_add_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_audit_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_backup_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_backups.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_branch_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_branch_ops.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_clean.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_cli_commands.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_cli_extra.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_cli_help.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_cli_revert.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_cli_v1.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_common_v1.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_completions_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_comprehensive.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_config.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_config_cmd_v1.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_config_ops_v1.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_config_preservation.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_config_section.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_core.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_core_extended.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_core_v1.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_custom_secrets.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_deploy_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_discover_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_dotman_config.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_edit_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_encrypt_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_encryption.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_exceptions.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_export_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_files_atomic.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_files_comprehensive.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_files_extended.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_final_push.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_fixtures.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_git_manager.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_git_manager_extended.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_hooks.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_hooks_new.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_import_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_init_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_integration.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_interactive.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_lock.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_log_checkout_tag.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_log_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_log_cmd_v1.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_merge.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_more_integration.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_navigate_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_navigate_switch_v1.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_onboarding.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_operations.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_ops_v1.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_performance_logic.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_profile_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_profile_cmd_v1.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_profile_extra.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_remote_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_restore_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_rollback_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_save_deploy_ops.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_secrets.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_section.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_show_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_status_ops.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_switch_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_switch_enhancements.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_symlink_deploy.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_tag_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_template_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_template_cmd_v1.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_template_extra.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_ui.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_utils.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_vault.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_verify_cmd.py +0 -0
- {dotman_git-1.2.3 → dotman_git-1.3.1}/tests/test_watch_cmd.py +0 -0
|
@@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.3.0] - 2026-06-27
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Template substitution in deployed files** — `{{VAR}}` placeholders in config files (e.g. `{{HOSTNAME}}`, `{{USER}}`, `{{OS}}`) are now rendered automatically during deploy. Enabled by default via `render_templates` on each section.
|
|
15
|
+
- **Conditional template syntax** — `{{ if OS == "darwin" }}...{{ endif }}` and `{{ if VAR != "value" }}...{{ endif }}` blocks in config files. Conditionals are evaluated before variable substitution. Uses stdlib regex, no new dependencies.
|
|
16
|
+
- **Vault key rotation** — `dot-man vault rotate-key` re-encrypts all secrets with a new Fernet key. Old key backed up to `.key.bak`. `dot-man vault status` shows vault stats.
|
|
17
|
+
- **Expanded `--dry-run` on navigate** — now shows all 4 phases: sections that would save, branch status, files that would deploy + hooks, config update.
|
|
18
|
+
- **62 new tests** — `test_save_cmd.py` (23), `test_status_cmd.py` (18), `test_navigate_helpers.py` (21), plus 14 template/conditional tests.
|
|
19
|
+
- **Shell completions split** — extracted completion functions into `completions.py` (310 lines), `common.py` reduced from 642 to 260 lines.
|
|
20
|
+
- **Priority improvement plan** — documented 20-item prioritized improvement roadmap in `docs/PRIORITY_PLAN.md`.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **`doctor_cmd.py:117`** — `.exists()` called on string instead of `Path`. Added `from pathlib import Path` and wrapped `DOT_MAN_TOML` in `Path()`.
|
|
25
|
+
- **`remote_cmd.py:225`** — `Path` passed where API expects `str`. Added `str()` conversion.
|
|
26
|
+
- **Stale coverage claims** — README and TODO updated from 83% to 81% (actual), test count from 1519 to 1526.
|
|
27
|
+
- **`test_doctor_cmd.py`** — fixed test to properly mock `DOT_MAN_DIR` instead of relying on bug-induced crash.
|
|
28
|
+
- **Import patch targets** — `test_completion.py` and `test_shell_completions.py` updated to patch `dot_man.cli.completions._git_runner`.
|
|
29
|
+
|
|
30
|
+
### Refactored
|
|
31
|
+
|
|
32
|
+
- **Deduplicate hook execution in navigate** — extracted `_run_shell_hooks()` helper, replacing 3 identical ~25-line hook blocks.
|
|
33
|
+
- **Split `common.py`** — completion functions moved to `completions.py`, backward-compatible re-exports preserved.
|
|
34
|
+
|
|
10
35
|
## [1.2.3] - 2026-06-11
|
|
11
36
|
|
|
12
37
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dotman-git
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.1
|
|
4
4
|
Summary: Dotfile manager with git-powered branching
|
|
5
5
|
Author-email: Bishoy Ehab <beshoyehabhhmm@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -18,7 +18,6 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
18
18
|
Classifier: Operating System :: POSIX :: Linux
|
|
19
19
|
Classifier: Operating System :: MacOS
|
|
20
20
|
Classifier: Programming Language :: Python :: 3
|
|
21
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
22
21
|
Classifier: Programming Language :: Python :: 3.10
|
|
23
22
|
Classifier: Programming Language :: Python :: 3.11
|
|
24
23
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -26,7 +25,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
26
25
|
Classifier: Topic :: System :: Systems Administration
|
|
27
26
|
Classifier: Topic :: Utilities
|
|
28
27
|
Classifier: Typing :: Typed
|
|
29
|
-
Requires-Python: >=3.
|
|
28
|
+
Requires-Python: >=3.10
|
|
30
29
|
Description-Content-Type: text/markdown
|
|
31
30
|
License-File: LICENSE
|
|
32
31
|
Requires-Dist: click>=8.0
|
|
@@ -70,13 +69,14 @@ Dynamic: license-file
|
|
|
70
69
|
[](https://github.com/astral-sh/ruff)
|
|
71
70
|
[](https://mypy-lang.org/)
|
|
72
71
|
[](https://github.com/BeshoyEhab/dot-man)
|
|
72
|
+
[](https://beshoyehab.github.io/dot-man/)
|
|
73
73
|
[](https://github.com/BeshoyEhab/dot-man/pulls)
|
|
74
74
|
|
|
75
75
|
---
|
|
76
76
|
|
|
77
77
|
*Switch your entire development environment in one command.*
|
|
78
78
|
|
|
79
|
-
[**Install**](#installation) · [**Quick Start**](#quick-start) · [**Commands**](#commands) · [**Config**](#configuration) · [**Docs**](
|
|
79
|
+
[**Install**](https://github.com/BeshoyEhab/dot-man#installation) · [**Quick Start**](https://github.com/BeshoyEhab/dot-man#quick-start) · [**Commands**](https://github.com/BeshoyEhab/dot-man#commands) · [**Config**](https://github.com/BeshoyEhab/dot-man#configuration) · [**Docs**](https://beshoyehab.github.io/dot-man/)
|
|
80
80
|
|
|
81
81
|
</div>
|
|
82
82
|
|
|
@@ -483,7 +483,7 @@ System variables (`{{HOSTNAME}}`, `{{USER}}`, `{{SHELL}}`, etc.) are auto-popula
|
|
|
483
483
|
| Version | `1.2.3` |
|
|
484
484
|
| Python | `3.10 · 3.11 · 3.12 · 3.13` |
|
|
485
485
|
| Platforms | Linux, macOS |
|
|
486
|
-
| Test Coverage |
|
|
486
|
+
| Test Coverage | 81% (1526 tests) |
|
|
487
487
|
| Commands | 30+ |
|
|
488
488
|
| PyPI | [`dotman-git`](https://pypi.org/project/dotman-git/) |
|
|
489
489
|
|
|
@@ -13,13 +13,14 @@
|
|
|
13
13
|
[](https://github.com/astral-sh/ruff)
|
|
14
14
|
[](https://mypy-lang.org/)
|
|
15
15
|
[](https://github.com/BeshoyEhab/dot-man)
|
|
16
|
+
[](https://beshoyehab.github.io/dot-man/)
|
|
16
17
|
[](https://github.com/BeshoyEhab/dot-man/pulls)
|
|
17
18
|
|
|
18
19
|
---
|
|
19
20
|
|
|
20
21
|
*Switch your entire development environment in one command.*
|
|
21
22
|
|
|
22
|
-
[**Install**](#installation) · [**Quick Start**](#quick-start) · [**Commands**](#commands) · [**Config**](#configuration) · [**Docs**](
|
|
23
|
+
[**Install**](https://github.com/BeshoyEhab/dot-man#installation) · [**Quick Start**](https://github.com/BeshoyEhab/dot-man#quick-start) · [**Commands**](https://github.com/BeshoyEhab/dot-man#commands) · [**Config**](https://github.com/BeshoyEhab/dot-man#configuration) · [**Docs**](https://beshoyehab.github.io/dot-man/)
|
|
23
24
|
|
|
24
25
|
</div>
|
|
25
26
|
|
|
@@ -426,7 +427,7 @@ System variables (`{{HOSTNAME}}`, `{{USER}}`, `{{SHELL}}`, etc.) are auto-popula
|
|
|
426
427
|
| Version | `1.2.3` |
|
|
427
428
|
| Python | `3.10 · 3.11 · 3.12 · 3.13` |
|
|
428
429
|
| Platforms | Linux, macOS |
|
|
429
|
-
| Test Coverage |
|
|
430
|
+
| Test Coverage | 81% (1526 tests) |
|
|
430
431
|
| Commands | 30+ |
|
|
431
432
|
| PyPI | [`dotman-git`](https://pypi.org/project/dotman-git/) |
|
|
432
433
|
|
|
@@ -4,7 +4,7 @@ This package provides the CLI entry point for dot-man.
|
|
|
4
4
|
It exposes the modular CLI implementation.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from . import discover_cmd, encrypt_cmd, export_cmd, import_cmd
|
|
7
|
+
from . import bootstrap_cmd, discover_cmd, encrypt_cmd, export_cmd, import_cmd
|
|
8
8
|
from .add_cmd import add
|
|
9
9
|
from .audit_cmd import audit
|
|
10
10
|
from .backup_cmd import backup
|
|
@@ -35,19 +35,21 @@ from .config_cmd import config
|
|
|
35
35
|
from .deploy_cmd import deploy
|
|
36
36
|
from .doctor_cmd import doctor
|
|
37
37
|
from .edit_cmd import edit
|
|
38
|
+
from .hooks_cmd import hooks
|
|
38
39
|
|
|
39
40
|
# Import commands for easier access and backward compatibility
|
|
40
41
|
from .init_cmd import init
|
|
41
42
|
from .interface import cli
|
|
42
43
|
from .log_cmd import checkout, diff, log
|
|
43
44
|
from .main import main
|
|
44
|
-
from .navigate_cmd import
|
|
45
|
+
from .navigate_cmd import navigate
|
|
45
46
|
from .onboarding import is_first_run, mark_onboarded, run_onboarding
|
|
46
47
|
from .profile_cmd import profile
|
|
47
48
|
from .remote_cmd import remote, sync
|
|
48
49
|
from .restore_cmd import restore
|
|
49
50
|
from .revert_cmd import revert
|
|
50
51
|
from .rollback_cmd import rollback
|
|
52
|
+
from .save_cmd import save
|
|
51
53
|
from .show_cmd import show
|
|
52
54
|
from .status_cmd import status
|
|
53
55
|
from .switch_cmd import switch
|
|
@@ -96,6 +98,7 @@ __all__ = [
|
|
|
96
98
|
"config",
|
|
97
99
|
"revert",
|
|
98
100
|
"restore",
|
|
101
|
+
"save",
|
|
99
102
|
"clean",
|
|
100
103
|
"doctor",
|
|
101
104
|
"verify",
|
|
@@ -112,6 +115,7 @@ __all__ = [
|
|
|
112
115
|
"export_cmd",
|
|
113
116
|
"encrypt_cmd",
|
|
114
117
|
"discover_cmd",
|
|
118
|
+
"bootstrap_cmd",
|
|
115
119
|
"completions",
|
|
116
120
|
"watch",
|
|
117
121
|
"rollback",
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""Bootstrap command for dot-man — install packages via system package managers."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from .. import ui
|
|
10
|
+
from .common import require_init, success, warn
|
|
11
|
+
from .interface import cli as main
|
|
12
|
+
|
|
13
|
+
# Package manager detection: command → (install_cmd, update_cmd)
|
|
14
|
+
PACKAGE_MANAGERS = {
|
|
15
|
+
"brew": {
|
|
16
|
+
"install": "brew install {package}",
|
|
17
|
+
"update": "brew update",
|
|
18
|
+
"detect": "brew",
|
|
19
|
+
},
|
|
20
|
+
"apt": {
|
|
21
|
+
"install": "sudo apt-get install -y {package}",
|
|
22
|
+
"update": "sudo apt-get update",
|
|
23
|
+
"detect": "apt-get",
|
|
24
|
+
},
|
|
25
|
+
"dnf": {
|
|
26
|
+
"install": "sudo dnf install -y {package}",
|
|
27
|
+
"update": "sudo dnf check-update",
|
|
28
|
+
"detect": "dnf",
|
|
29
|
+
},
|
|
30
|
+
"pacman": {
|
|
31
|
+
"install": "sudo pacman -S --noconfirm {package}",
|
|
32
|
+
"update": "sudo pacman -Sy",
|
|
33
|
+
"detect": "pacman",
|
|
34
|
+
},
|
|
35
|
+
"zypper": {
|
|
36
|
+
"install": "sudo zypper install -y {package}",
|
|
37
|
+
"update": "sudo zypper refresh",
|
|
38
|
+
"detect": "zypper",
|
|
39
|
+
},
|
|
40
|
+
"nix-env": {
|
|
41
|
+
"install": "nix-env -iA nixpkgs.{package}",
|
|
42
|
+
"update": "nix-channel --update",
|
|
43
|
+
"detect": "nix-env",
|
|
44
|
+
},
|
|
45
|
+
"xbps-install": {
|
|
46
|
+
"install": "sudo xbps-install -y {package}",
|
|
47
|
+
"update": "sudo xbps-install -Su",
|
|
48
|
+
"detect": "xbps-install",
|
|
49
|
+
},
|
|
50
|
+
"pkg": {
|
|
51
|
+
"install": "pkg install -y {package}",
|
|
52
|
+
"update": "pkg update",
|
|
53
|
+
"detect": "pkg",
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _detect_package_manager() -> str | None:
|
|
59
|
+
"""Detect the available package manager on this system."""
|
|
60
|
+
import platform
|
|
61
|
+
|
|
62
|
+
os_name = platform.system().lower()
|
|
63
|
+
|
|
64
|
+
# Preferred order by OS
|
|
65
|
+
if os_name == "darwin":
|
|
66
|
+
preferred = ["brew"]
|
|
67
|
+
elif os_name == "linux":
|
|
68
|
+
preferred = ["apt", "dnf", "pacman", "zypper", "xbps-install", "nix-env"]
|
|
69
|
+
elif os_name == "freebsd":
|
|
70
|
+
preferred = ["pkg"]
|
|
71
|
+
else:
|
|
72
|
+
preferred = list(PACKAGE_MANAGERS.keys())
|
|
73
|
+
|
|
74
|
+
for pm in preferred:
|
|
75
|
+
if shutil.which(PACKAGE_MANAGERS[pm]["detect"]):
|
|
76
|
+
return pm
|
|
77
|
+
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _load_bootstrap_packages() -> dict[str, list[str]]:
|
|
82
|
+
"""Load packages from dot-man config or bootstrap.toml."""
|
|
83
|
+
try:
|
|
84
|
+
from ..config import DotManConfig
|
|
85
|
+
|
|
86
|
+
config = DotManConfig()
|
|
87
|
+
config.load()
|
|
88
|
+
|
|
89
|
+
# Check for [bootstrap] section in config
|
|
90
|
+
bootstrap = config._data.get("bootstrap", {})
|
|
91
|
+
if bootstrap:
|
|
92
|
+
packages = {}
|
|
93
|
+
# Support both flat list and grouped format
|
|
94
|
+
if isinstance(bootstrap, dict):
|
|
95
|
+
for key, value in bootstrap.items():
|
|
96
|
+
if key == "packages" and isinstance(value, list):
|
|
97
|
+
packages["default"] = value
|
|
98
|
+
elif isinstance(value, list):
|
|
99
|
+
packages[key] = value
|
|
100
|
+
return packages
|
|
101
|
+
|
|
102
|
+
except Exception as e:
|
|
103
|
+
logging.debug(f"Failed to load bootstrap config: {e}")
|
|
104
|
+
|
|
105
|
+
return {}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _run_with_pm(pm: str, command_template: str, package: str) -> bool:
|
|
109
|
+
"""Run a package manager command. Returns True on success."""
|
|
110
|
+
cmd = command_template.format(package=package)
|
|
111
|
+
ui.console.print(f" [cyan]$ {cmd}[/cyan]")
|
|
112
|
+
try:
|
|
113
|
+
result = subprocess.run(
|
|
114
|
+
cmd, shell=True, capture_output=True, text=True, timeout=300
|
|
115
|
+
)
|
|
116
|
+
if result.returncode == 0:
|
|
117
|
+
return True
|
|
118
|
+
else:
|
|
119
|
+
warn(f"Failed to install {package} (exit code {result.returncode})")
|
|
120
|
+
if result.stderr:
|
|
121
|
+
for line in result.stderr.splitlines()[:3]:
|
|
122
|
+
ui.console.print(f" [dim]{line}[/dim]")
|
|
123
|
+
return False
|
|
124
|
+
except subprocess.TimeoutExpired:
|
|
125
|
+
warn(f"Timeout installing {package}")
|
|
126
|
+
return False
|
|
127
|
+
except Exception as e:
|
|
128
|
+
warn(f"Error installing {package}: {e}")
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@main.command("bootstrap")
|
|
133
|
+
@click.option(
|
|
134
|
+
"--list",
|
|
135
|
+
"list_only",
|
|
136
|
+
is_flag=True,
|
|
137
|
+
help="List packages that would be installed without installing",
|
|
138
|
+
)
|
|
139
|
+
@click.option("--pm", help="Package manager to use (auto-detected if not specified)")
|
|
140
|
+
@click.option("--update", is_flag=True, help="Update package manager first")
|
|
141
|
+
@click.argument("packages", nargs=-1)
|
|
142
|
+
@require_init
|
|
143
|
+
def bootstrap(list_only: bool, pm: str | None, update: bool, packages: tuple[str, ...]):
|
|
144
|
+
"""Install system packages via package managers.
|
|
145
|
+
|
|
146
|
+
Detects your system package manager (brew, apt, dnf, pacman, etc.)
|
|
147
|
+
and installs the specified packages. Packages can also be defined
|
|
148
|
+
in your dot-man config under [bootstrap].
|
|
149
|
+
|
|
150
|
+
Examples:
|
|
151
|
+
# Install specific packages
|
|
152
|
+
dot-man bootstrap neovim tmux git
|
|
153
|
+
|
|
154
|
+
# List what would be installed from config
|
|
155
|
+
dot-man bootstrap --list
|
|
156
|
+
|
|
157
|
+
# Use a specific package manager
|
|
158
|
+
dot-man bootstrap --pm brew neovim
|
|
159
|
+
|
|
160
|
+
# Update package manager first
|
|
161
|
+
dot-man bootstrap --update neovim
|
|
162
|
+
"""
|
|
163
|
+
# Detect package manager
|
|
164
|
+
if not pm:
|
|
165
|
+
pm = _detect_package_manager()
|
|
166
|
+
if not pm:
|
|
167
|
+
ui.console.print("[red]No supported package manager found.[/red]")
|
|
168
|
+
ui.console.print(
|
|
169
|
+
"Supported: brew, apt, dnf, pacman, zypper, nix-env, xbps-install, pkg"
|
|
170
|
+
)
|
|
171
|
+
ui.console.print("Use --pm to specify one manually.")
|
|
172
|
+
raise SystemExit(1)
|
|
173
|
+
|
|
174
|
+
if pm not in PACKAGE_MANAGERS:
|
|
175
|
+
ui.console.print(f"[red]Unknown package manager: {pm}[/red]")
|
|
176
|
+
raise SystemExit(1)
|
|
177
|
+
|
|
178
|
+
pm_info = PACKAGE_MANAGERS[pm]
|
|
179
|
+
ui.console.print(f"[bold]Using package manager:[/bold] [cyan]{pm}[/cyan]")
|
|
180
|
+
|
|
181
|
+
# Collect packages from args and config
|
|
182
|
+
all_packages: dict[str, list[str]] = {}
|
|
183
|
+
|
|
184
|
+
if packages:
|
|
185
|
+
all_packages["command-line"] = list(packages)
|
|
186
|
+
|
|
187
|
+
config_packages = _load_bootstrap_packages()
|
|
188
|
+
all_packages.update(config_packages)
|
|
189
|
+
|
|
190
|
+
if not all_packages:
|
|
191
|
+
ui.console.print("[yellow]No packages specified.[/yellow]")
|
|
192
|
+
ui.console.print("Usage: dot-man bootstrap <package1> <package2> ...")
|
|
193
|
+
ui.console.print("Or add [bootstrap] section to your dot-man.toml:")
|
|
194
|
+
ui.console.print(' [bootstrap]\n packages = ["neovim", "tmux", "git"]')
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
# List mode
|
|
198
|
+
if list_only:
|
|
199
|
+
ui.console.print()
|
|
200
|
+
ui.console.print("[bold]Packages to install:[/bold]")
|
|
201
|
+
for group, pkgs in all_packages.items():
|
|
202
|
+
ui.console.print(f" [cyan]{group}:[/cyan]")
|
|
203
|
+
for pkg in pkgs:
|
|
204
|
+
ui.console.print(f" • {pkg}")
|
|
205
|
+
ui.console.print()
|
|
206
|
+
total = sum(len(pkgs) for pkgs in all_packages.values())
|
|
207
|
+
ui.console.print(f" [dim]{total} package(s) total[/dim]")
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
# Update package manager if requested
|
|
211
|
+
if update:
|
|
212
|
+
ui.console.print()
|
|
213
|
+
ui.console.print("[bold]Updating package manager...[/bold]")
|
|
214
|
+
_run_with_pm(pm, pm_info["update"], "")
|
|
215
|
+
|
|
216
|
+
# Install packages
|
|
217
|
+
installed = 0
|
|
218
|
+
failed = 0
|
|
219
|
+
|
|
220
|
+
for group, pkgs in all_packages.items():
|
|
221
|
+
ui.console.print()
|
|
222
|
+
ui.console.print(f"[bold]Installing {group}:[/bold]")
|
|
223
|
+
for pkg in pkgs:
|
|
224
|
+
ui.console.print(f"\n[bold]→ {pkg}[/bold]")
|
|
225
|
+
result = _run_with_pm(pm, pm_info["install"], pkg)
|
|
226
|
+
if result:
|
|
227
|
+
installed += 1
|
|
228
|
+
else:
|
|
229
|
+
failed += 1
|
|
230
|
+
|
|
231
|
+
# Summary
|
|
232
|
+
ui.console.print()
|
|
233
|
+
ui.console.print("[bold]Bootstrap complete:[/bold]")
|
|
234
|
+
ui.console.print(f" [green]✓ {installed} installed[/green]")
|
|
235
|
+
if failed:
|
|
236
|
+
ui.console.print(f" [red]✗ {failed} failed[/red]")
|
|
237
|
+
ui.console.print()
|
|
238
|
+
|
|
239
|
+
if installed > 0:
|
|
240
|
+
success(f"Installed {installed} package(s) via {pm}")
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""Common utilities for dot-man CLI commands."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import re
|
|
5
|
+
from functools import wraps
|
|
6
|
+
from typing import Callable
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from .. import ui
|
|
11
|
+
from ..constants import DOT_MAN_DIR, REPO_DIR
|
|
12
|
+
from ..core import GitManager
|
|
13
|
+
from ..secrets import PermanentRedactGuard, SecretGuard, SecretMatch
|
|
14
|
+
from .completions import ( # noqa: F401 — re-exports for backward compat
|
|
15
|
+
_clear_all_caches,
|
|
16
|
+
_clear_completion_cache,
|
|
17
|
+
_set_git_runner,
|
|
18
|
+
complete_branches,
|
|
19
|
+
complete_commits,
|
|
20
|
+
complete_config_keys,
|
|
21
|
+
complete_profiles,
|
|
22
|
+
complete_sections,
|
|
23
|
+
complete_switch_args,
|
|
24
|
+
complete_tags,
|
|
25
|
+
complete_template_keys,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def error(message: str, exit_code: int = 1) -> None:
|
|
30
|
+
"""Print error message and exit."""
|
|
31
|
+
ui.error(message, exit_code)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def success(message: str) -> None:
|
|
35
|
+
"""Print success message."""
|
|
36
|
+
ui.success(message)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def warn(message: str) -> None:
|
|
40
|
+
"""Print warning message."""
|
|
41
|
+
ui.warn(message)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def handle_exception(exc: BaseException, context: str = "Operation") -> None:
|
|
45
|
+
"""Handle exceptions with user-friendly diagnostics."""
|
|
46
|
+
from ..exceptions import DotManError, ErrorDiagnostic
|
|
47
|
+
|
|
48
|
+
if isinstance(exc, DotManError):
|
|
49
|
+
error(str(exc), exc.exit_code)
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
if isinstance(exc, KeyboardInterrupt):
|
|
53
|
+
ui.console.print()
|
|
54
|
+
warn("Operation cancelled by user")
|
|
55
|
+
raise SystemExit(130)
|
|
56
|
+
|
|
57
|
+
diagnostic = ErrorDiagnostic.from_exception(exc) # type: ignore[arg-type]
|
|
58
|
+
ui.console.print()
|
|
59
|
+
ui.console.print(f"[red bold]{diagnostic.title}[/red bold]")
|
|
60
|
+
ui.console.print(f"[red]{diagnostic.details}[/red]")
|
|
61
|
+
ui.console.print()
|
|
62
|
+
ui.console.print(f"[dim]💡 {diagnostic.suggestion}[/dim]")
|
|
63
|
+
raise SystemExit(1)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class AliasedCommand(click.Command):
|
|
67
|
+
"""Custom Command class that supports aliases."""
|
|
68
|
+
|
|
69
|
+
def __init__(self, *args, aliases=None, **kwargs):
|
|
70
|
+
super().__init__(*args, **kwargs)
|
|
71
|
+
self._aliases = aliases or []
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def aliases(self):
|
|
75
|
+
return self._aliases
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def command(name=None, cls=None, aliases=None, **kwargs):
|
|
79
|
+
"""Decorator to create a command with optional aliases."""
|
|
80
|
+
if cls is None:
|
|
81
|
+
cls = AliasedCommand
|
|
82
|
+
|
|
83
|
+
def decorator(f):
|
|
84
|
+
return click.command(name=name, cls=cls, aliases=aliases, **kwargs)(f)
|
|
85
|
+
|
|
86
|
+
return decorator
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class DotManGroup(click.Group):
|
|
90
|
+
"""Custom Click Group to provide suggestions for typos."""
|
|
91
|
+
|
|
92
|
+
def add_command(self, command, name=None):
|
|
93
|
+
name = name or command.name
|
|
94
|
+
super().add_command(command, name)
|
|
95
|
+
if hasattr(command, "aliases") and command.aliases:
|
|
96
|
+
for alias in command.aliases:
|
|
97
|
+
super().add_command(command, alias)
|
|
98
|
+
|
|
99
|
+
def get_command(self, ctx, cmd_name):
|
|
100
|
+
rv = click.Group.get_command(self, ctx, cmd_name)
|
|
101
|
+
if rv is not None:
|
|
102
|
+
return rv
|
|
103
|
+
|
|
104
|
+
matches = [cmd for cmd in self.list_commands(ctx)]
|
|
105
|
+
suggestion = ui.suggest_command(cmd_name, matches)
|
|
106
|
+
|
|
107
|
+
ui.error(f"Unknown command '{cmd_name}'", exit_code=0)
|
|
108
|
+
if suggestion:
|
|
109
|
+
ui.warn(f"Did you mean '{suggestion}'?")
|
|
110
|
+
|
|
111
|
+
ctx.exit(2)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def require_init(func):
|
|
115
|
+
"""Decorator to require initialization before running command."""
|
|
116
|
+
|
|
117
|
+
@wraps(func)
|
|
118
|
+
def wrapper(*args, **kwargs):
|
|
119
|
+
if not DOT_MAN_DIR.exists():
|
|
120
|
+
ui.console.print()
|
|
121
|
+
ui.print_banner("🎯 Welcome to dot-man!")
|
|
122
|
+
ui.console.print()
|
|
123
|
+
ui.console.print("[bold]The Dotfile Manager for Professionals[/bold]")
|
|
124
|
+
ui.console.print()
|
|
125
|
+
ui.console.print("[bold cyan]Coming from another tool?[/bold cyan]")
|
|
126
|
+
ui.console.print(
|
|
127
|
+
" [cyan]dot-man import chezmoi[/cyan] - Import from chezmoi"
|
|
128
|
+
)
|
|
129
|
+
ui.console.print(
|
|
130
|
+
" [cyan]dot-man import yadm[/cyan] - Import from yadm"
|
|
131
|
+
)
|
|
132
|
+
ui.console.print(
|
|
133
|
+
" [cyan]dot-man import stow[/cyan] - Import from GNU Stow"
|
|
134
|
+
)
|
|
135
|
+
ui.console.print(
|
|
136
|
+
" [cyan]dot-man import all[/cyan] - Auto-detect and import"
|
|
137
|
+
)
|
|
138
|
+
ui.console.print()
|
|
139
|
+
ui.console.print("[bold cyan]Get started:[/bold cyan]")
|
|
140
|
+
ui.console.print(
|
|
141
|
+
" [cyan]dot-man init[/cyan] - Initialize your dotfiles repository"
|
|
142
|
+
)
|
|
143
|
+
ui.console.print(
|
|
144
|
+
" [cyan]dot-man init --help[/cyan] - See all init options"
|
|
145
|
+
)
|
|
146
|
+
ui.console.print()
|
|
147
|
+
ui.console.print("[bold cyan]Quick overview:[/bold cyan]")
|
|
148
|
+
ui.console.print(
|
|
149
|
+
" [cyan]dot-man add <path>[/cyan] - Add files to track"
|
|
150
|
+
)
|
|
151
|
+
ui.console.print(
|
|
152
|
+
" [cyan]dot-man status[/cyan] - View tracked files"
|
|
153
|
+
)
|
|
154
|
+
ui.console.print(
|
|
155
|
+
" [cyan]dot-man navigate <branch>[/cyan] - Switch between configurations"
|
|
156
|
+
)
|
|
157
|
+
ui.console.print(
|
|
158
|
+
" [cyan]dot-man --help[/cyan] - See all commands"
|
|
159
|
+
)
|
|
160
|
+
ui.console.print()
|
|
161
|
+
ui.console.print("[dim]💡 Run 'dot-man init' to get started![/dim]")
|
|
162
|
+
ui.console.print()
|
|
163
|
+
raise SystemExit(1)
|
|
164
|
+
|
|
165
|
+
if not REPO_DIR.exists() or not (REPO_DIR / ".git").exists():
|
|
166
|
+
error("Repository not initialized. Run 'dot-man init' first.", exit_code=1)
|
|
167
|
+
|
|
168
|
+
return func(*args, **kwargs)
|
|
169
|
+
|
|
170
|
+
return wrapper
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def parse_branch_arg(arg: str) -> dict:
|
|
174
|
+
"""Parse branch argument with @tag or commit SHA support."""
|
|
175
|
+
match = re.match(r"^(.+)@(.+)$", arg)
|
|
176
|
+
if match:
|
|
177
|
+
base = match.group(1)
|
|
178
|
+
target = match.group(2)
|
|
179
|
+
|
|
180
|
+
if not base:
|
|
181
|
+
base = "HEAD"
|
|
182
|
+
|
|
183
|
+
if re.match(r"^[a-f0-9]{7,40}$", target):
|
|
184
|
+
return {"type": "commit", "base": base, "target": target}
|
|
185
|
+
|
|
186
|
+
return {"type": "tag", "base": base, "target": target}
|
|
187
|
+
|
|
188
|
+
if re.match(r"^[a-f0-9]{7,40}$", arg):
|
|
189
|
+
return {"type": "commit", "base": "HEAD", "target": arg}
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
git = GitManager()
|
|
193
|
+
if arg in git.list_tags():
|
|
194
|
+
return {"type": "tag", "base": "HEAD", "target": arg}
|
|
195
|
+
except Exception as e:
|
|
196
|
+
logging.debug(f"Could not check tags: {e}")
|
|
197
|
+
|
|
198
|
+
return {"type": "branch", "base": arg, "target": arg}
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class BranchParamType(click.ParamType):
|
|
202
|
+
"""Parameter type that accepts branch, branch@tag, or commit SHA."""
|
|
203
|
+
|
|
204
|
+
name = "branch"
|
|
205
|
+
|
|
206
|
+
def convert(self, value, param, ctx):
|
|
207
|
+
if not value:
|
|
208
|
+
return None
|
|
209
|
+
return parse_branch_arg(value)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
BRANCH = BranchParamType()
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def get_secret_handler() -> Callable[[SecretMatch], str]:
|
|
216
|
+
"""Get a secret handler that prompts the user for action."""
|
|
217
|
+
guard = SecretGuard()
|
|
218
|
+
permanent_guard = PermanentRedactGuard()
|
|
219
|
+
|
|
220
|
+
def handle_secret(match: SecretMatch) -> str:
|
|
221
|
+
if permanent_guard.should_redact(
|
|
222
|
+
match.file, match.line_content, match.pattern_name
|
|
223
|
+
):
|
|
224
|
+
return "REDACT"
|
|
225
|
+
|
|
226
|
+
if guard.is_allowed(match.file, match.line_content, match.pattern_name):
|
|
227
|
+
return "IGNORE"
|
|
228
|
+
|
|
229
|
+
ui.console.print()
|
|
230
|
+
ui.warn("Potential secret detected!")
|
|
231
|
+
ui.console.print(f"File: [cyan]{match.file}[/cyan]")
|
|
232
|
+
ui.console.print(f"Line {match.line_number}: {match.line_content[:80]}...")
|
|
233
|
+
ui.console.print(
|
|
234
|
+
f"Pattern: {match.pattern_name} (severity: {match.severity.value})"
|
|
235
|
+
)
|
|
236
|
+
ui.console.print()
|
|
237
|
+
|
|
238
|
+
ui.console.print("Choose how to handle this secret:")
|
|
239
|
+
ui.console.print(" 1. [dim]Ignore (skip it this time)[/dim]")
|
|
240
|
+
ui.console.print(
|
|
241
|
+
" 2. [yellow]Protect (replace with ***REDACTED*** this time)[/yellow]"
|
|
242
|
+
)
|
|
243
|
+
ui.console.print(
|
|
244
|
+
" 3. [blue]Add to skip list (skip this line every time)[/blue]"
|
|
245
|
+
)
|
|
246
|
+
ui.console.print(" 4. [red]Protect forever (always replace in repo)[/red]")
|
|
247
|
+
ui.console.print()
|
|
248
|
+
|
|
249
|
+
choices = ["1", "2", "3", "4"]
|
|
250
|
+
choice = ui.ask("Enter choice", choices=choices, default="2")
|
|
251
|
+
|
|
252
|
+
if choice == "1":
|
|
253
|
+
return "IGNORE"
|
|
254
|
+
elif choice == "2":
|
|
255
|
+
return "REDACT"
|
|
256
|
+
elif choice == "3":
|
|
257
|
+
guard.add_allowed(match.file, match.line_content, match.pattern_name)
|
|
258
|
+
ui.info("Added to skip list.")
|
|
259
|
+
return "IGNORE"
|
|
260
|
+
elif choice == "4":
|
|
261
|
+
permanent_guard.add_permanent_redact(
|
|
262
|
+
match.file, match.line_content, match.pattern_name
|
|
263
|
+
)
|
|
264
|
+
ui.warn("Will always redact this secret.")
|
|
265
|
+
return "REDACT"
|
|
266
|
+
return "REDACT"
|
|
267
|
+
|
|
268
|
+
return handle_secret
|