dotman-cli 0.2.9__tar.gz → 0.3.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 (42) hide show
  1. {dotman_cli-0.2.9/src/dotman_cli.egg-info → dotman_cli-0.3.0}/PKG-INFO +152 -1
  2. {dotman_cli-0.2.9 → dotman_cli-0.3.0}/README.md +151 -0
  3. {dotman_cli-0.2.9 → dotman_cli-0.3.0}/pyproject.toml +1 -1
  4. {dotman_cli-0.2.9 → dotman_cli-0.3.0}/src/dotman/cli/__init__.py +2 -0
  5. {dotman_cli-0.2.9 → dotman_cli-0.3.0}/src/dotman/cli/commands/__init__.py +2 -0
  6. dotman_cli-0.3.0/src/dotman/cli/commands/doctor.py +135 -0
  7. {dotman_cli-0.2.9 → dotman_cli-0.3.0}/src/dotman/core/config.py +15 -1
  8. {dotman_cli-0.2.9 → dotman_cli-0.3.0}/src/dotman/core/exceptions.py +12 -0
  9. dotman_cli-0.3.0/src/dotman/services/__init__.py +19 -0
  10. dotman_cli-0.3.0/src/dotman/services/doctor.py +109 -0
  11. {dotman_cli-0.2.9 → dotman_cli-0.3.0/src/dotman_cli.egg-info}/PKG-INFO +152 -1
  12. {dotman_cli-0.2.9 → dotman_cli-0.3.0}/src/dotman_cli.egg-info/SOURCES.txt +2 -0
  13. dotman_cli-0.2.9/src/dotman/services/__init__.py +0 -8
  14. {dotman_cli-0.2.9 → dotman_cli-0.3.0}/LICENSE +0 -0
  15. {dotman_cli-0.2.9 → dotman_cli-0.3.0}/setup.cfg +0 -0
  16. {dotman_cli-0.2.9 → dotman_cli-0.3.0}/src/dotman/__init__.py +0 -0
  17. {dotman_cli-0.2.9 → dotman_cli-0.3.0}/src/dotman/cli/commands/absorb.py +0 -0
  18. {dotman_cli-0.2.9 → dotman_cli-0.3.0}/src/dotman/cli/commands/clean.py +0 -0
  19. {dotman_cli-0.2.9 → dotman_cli-0.3.0}/src/dotman/cli/commands/clone.py +0 -0
  20. {dotman_cli-0.2.9 → dotman_cli-0.3.0}/src/dotman/cli/commands/deploy.py +0 -0
  21. {dotman_cli-0.2.9 → dotman_cli-0.3.0}/src/dotman/cli/commands/history.py +0 -0
  22. {dotman_cli-0.2.9 → dotman_cli-0.3.0}/src/dotman/cli/commands/init.py +0 -0
  23. {dotman_cli-0.2.9 → dotman_cli-0.3.0}/src/dotman/cli/commands/pull.py +0 -0
  24. {dotman_cli-0.2.9 → dotman_cli-0.3.0}/src/dotman/cli/commands/push.py +0 -0
  25. {dotman_cli-0.2.9 → dotman_cli-0.3.0}/src/dotman/cli/commands/repo.py +0 -0
  26. {dotman_cli-0.2.9 → dotman_cli-0.3.0}/src/dotman/cli/commands/status.py +0 -0
  27. {dotman_cli-0.2.9 → dotman_cli-0.3.0}/src/dotman/cli/commands/watch.py +0 -0
  28. {dotman_cli-0.2.9 → dotman_cli-0.3.0}/src/dotman/cli_utils.py +0 -0
  29. {dotman_cli-0.2.9 → dotman_cli-0.3.0}/src/dotman/core/__init__.py +0 -0
  30. {dotman_cli-0.2.9 → dotman_cli-0.3.0}/src/dotman/core/link_manager.py +0 -0
  31. {dotman_cli-0.2.9 → dotman_cli-0.3.0}/src/dotman/core/template_engine.py +0 -0
  32. {dotman_cli-0.2.9 → dotman_cli-0.3.0}/src/dotman/main.py +0 -0
  33. {dotman_cli-0.2.9 → dotman_cli-0.3.0}/src/dotman/managers/__init__.py +0 -0
  34. {dotman_cli-0.2.9 → dotman_cli-0.3.0}/src/dotman/managers/history.py +0 -0
  35. {dotman_cli-0.2.9 → dotman_cli-0.3.0}/src/dotman/managers/remote.py +0 -0
  36. {dotman_cli-0.2.9 → dotman_cli-0.3.0}/src/dotman/managers/repository.py +0 -0
  37. {dotman_cli-0.2.9 → dotman_cli-0.3.0}/src/dotman/managers/watcher.py +0 -0
  38. {dotman_cli-0.2.9 → dotman_cli-0.3.0}/src/dotman/services/hook_executor.py +0 -0
  39. {dotman_cli-0.2.9 → dotman_cli-0.3.0}/src/dotman_cli.egg-info/dependency_links.txt +0 -0
  40. {dotman_cli-0.2.9 → dotman_cli-0.3.0}/src/dotman_cli.egg-info/entry_points.txt +0 -0
  41. {dotman_cli-0.2.9 → dotman_cli-0.3.0}/src/dotman_cli.egg-info/requires.txt +0 -0
  42. {dotman_cli-0.2.9 → dotman_cli-0.3.0}/src/dotman_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dotman-cli
3
- Version: 0.2.9
3
+ Version: 0.3.0
4
4
  Summary: One more dot file manager but an interesting and easy to use one.
5
5
  Author: Nimit Bhardwaj
6
6
  License: MIT
@@ -165,6 +165,7 @@ packages:
165
165
  | `dotman rollback [id]` | Rollback a deployment by ID |
166
166
  | `dotman repo add <name>` | Register current directory as a repository |
167
167
  | `dotman repo list` | List all registered repositories |
168
+ | `dotman doctor [packages]` | Check required executables are present |
168
169
 
169
170
  ### Options
170
171
 
@@ -221,6 +222,156 @@ packages:
221
222
 
222
223
  The `absorb_ignore` field accepts a list of regex patterns that are matched against the full file path. If a file matches any pattern, it will be skipped during absorption.
223
224
 
225
+ ## Doctor Configuration
226
+
227
+ Dotman can check if required executables are present on your system before deploying packages. This helps identify missing dependencies early, preventing partial deployments and confusing errors.
228
+
229
+ ### Basic Configuration
230
+
231
+ Define executable requirements in your package configuration:
232
+
233
+ ```yaml
234
+ packages:
235
+ nvim:
236
+ depends: []
237
+ files:
238
+ - source: "nvim"
239
+ target: "~/.config/nvim"
240
+ doctor:
241
+ executables:
242
+ - name: nvim
243
+ severity: error
244
+ - name: git
245
+ severity: error
246
+ ```
247
+
248
+ ### Severity Levels
249
+
250
+ The `severity` field determines how missing executables are treated:
251
+
252
+ - **`error`**: The executable is required for the package to work. If missing, `dotman deploy` may fail or produce unexpected results. The doctor command returns exit code 1.
253
+ - **`warning`**: The executable is optional or only needed for certain features. If missing, the package may still work with reduced functionality. The doctor command returns exit code 0 but shows a warning.
254
+
255
+ ### Example Configuration
256
+
257
+ ```yaml
258
+ packages:
259
+ nvim-base:
260
+ description: "Neovim configuration with LazyVim for base"
261
+ files:
262
+ - source: "nvim/base/"
263
+ target: "~/.config/nvim"
264
+ doctor:
265
+ executables:
266
+ - name: nvim
267
+ severity: error
268
+ - name: git
269
+ severity: error
270
+
271
+ opencode:
272
+ description: "OpenCode AI assistant configuration"
273
+ files:
274
+ - source: "opencode"
275
+ target: "~/.config/opencode"
276
+ doctor:
277
+ executables:
278
+ - name: node
279
+ severity: warning
280
+ - name: bun
281
+ severity: warning
282
+
283
+ zsh:
284
+ description: "Zsh Config for the system"
285
+ files:
286
+ - source: "zsh/.zshrc"
287
+ target: "~/.zshrc"
288
+ doctor:
289
+ executables:
290
+ - name: zsh
291
+ severity: error
292
+ ```
293
+
294
+ ### Using the Doctor Command
295
+
296
+ ```bash
297
+ # Check all enabled packages
298
+ dotman doctor
299
+
300
+ # Check specific packages
301
+ dotman doctor nvim zsh
302
+
303
+ # Check with custom config directory
304
+ dotman doctor --config-dir ~/.dotfiles
305
+ ```
306
+
307
+ ### Example Output
308
+
309
+ ```
310
+ $ dotman doctor
311
+
312
+ ╭───────────────────────────────────────────────────────────────────────────╮
313
+ │ Doctor Check Results │
314
+ ├───────────────────────────────────────────────────────────────────────────┤
315
+ │ Package: nvim-base │
316
+ ├───────────────────────────────────────────────────────────────────────────┤
317
+ │ Executable Status Severity Path │
318
+ │ nvim ✓ Found error /usr/bin/nvim │
319
+ │ git ✓ Found error /usr/bin/git │
320
+ ├───────────────────────────────────────────────────────────────────────────┤
321
+ │ Package: opencode │
322
+ ├───────────────────────────────────────────────────────────────────────────┤
323
+ │ Executable Status Severity Path │
324
+ │ node ✓ Found warning /usr/bin/node │
325
+ │ bun ✗ Missing warning Not in PATH │
326
+ ├───────────────────────────────────────────────────────────────────────────┤
327
+ │ Package: zsh │
328
+ ├───────────────────────────────────────────────────────────────────────────┤
329
+ │ Executable Status Severity Path │
330
+ │ zsh ✓ Found error /usr/bin/zsh │
331
+ ╰───────────────────────────────────────────────────────────────────────────╯
332
+
333
+ Summary: 0 errors, 1 warning, 4 passed
334
+ ```
335
+
336
+ ### Exit Codes
337
+
338
+ - **Exit code 0**: No error-severity executables are missing (warnings are acceptable)
339
+ - **Exit code 1**: One or more error-severity executables are missing
340
+
341
+ ### How It Works
342
+
343
+ 1. The doctor command loads your configuration and resolves the package dependency tree
344
+ 2. For each package with `doctor.executables` defined, it checks if each executable exists in your system's PATH
345
+ 3. Executable lookup uses `shutil.which()` for cross-platform compatibility (Linux, macOS, Windows)
346
+ 4. Results are displayed in a formatted table grouped by package
347
+ 5. A summary shows totals for errors, warnings, and passed checks
348
+
349
+ ### Packages Without Doctor Config
350
+
351
+ Packages without a `doctor` configuration are still shown in the output with "No executable requirements":
352
+
353
+ ```
354
+ $ dotman doctor
355
+
356
+ ╭───────────────────────────────────────────────────────────────────────────╮
357
+ │ Doctor Check Results │
358
+ ├───────────────────────────────────────────────────────────────────────────┤
359
+ │ Package: simple-config │
360
+ ├───────────────────────────────────────────────────────────────────────────┤
361
+ │ Executable Status Severity Path │
362
+ │ No executable requirements │
363
+ ╰───────────────────────────────────────────────────────────────────────────╯
364
+
365
+ Summary: 0 errors, 0 warnings, 0 passed
366
+ ```
367
+
368
+ ### Best Practices
369
+
370
+ - Set `severity: error` for executables that are strictly required (e.g., `nvim`, `git`)
371
+ - Set `severity: warning` for executables that are optional or only needed for specific features
372
+ - Run `dotman doctor` before `dotman deploy` to catch missing dependencies early
373
+ - Use `--dry-run` with deploy to preview changes after fixing any doctor issues
374
+
224
375
  ### Overlapping Targets
225
376
 
226
377
  If multiple packages target the same directory, the first package (alphabetically) processes the files, and subsequent packages are skipped with a warning. This ensures predictable behavior:
@@ -144,6 +144,7 @@ packages:
144
144
  | `dotman rollback [id]` | Rollback a deployment by ID |
145
145
  | `dotman repo add <name>` | Register current directory as a repository |
146
146
  | `dotman repo list` | List all registered repositories |
147
+ | `dotman doctor [packages]` | Check required executables are present |
147
148
 
148
149
  ### Options
149
150
 
@@ -200,6 +201,156 @@ packages:
200
201
 
201
202
  The `absorb_ignore` field accepts a list of regex patterns that are matched against the full file path. If a file matches any pattern, it will be skipped during absorption.
202
203
 
204
+ ## Doctor Configuration
205
+
206
+ Dotman can check if required executables are present on your system before deploying packages. This helps identify missing dependencies early, preventing partial deployments and confusing errors.
207
+
208
+ ### Basic Configuration
209
+
210
+ Define executable requirements in your package configuration:
211
+
212
+ ```yaml
213
+ packages:
214
+ nvim:
215
+ depends: []
216
+ files:
217
+ - source: "nvim"
218
+ target: "~/.config/nvim"
219
+ doctor:
220
+ executables:
221
+ - name: nvim
222
+ severity: error
223
+ - name: git
224
+ severity: error
225
+ ```
226
+
227
+ ### Severity Levels
228
+
229
+ The `severity` field determines how missing executables are treated:
230
+
231
+ - **`error`**: The executable is required for the package to work. If missing, `dotman deploy` may fail or produce unexpected results. The doctor command returns exit code 1.
232
+ - **`warning`**: The executable is optional or only needed for certain features. If missing, the package may still work with reduced functionality. The doctor command returns exit code 0 but shows a warning.
233
+
234
+ ### Example Configuration
235
+
236
+ ```yaml
237
+ packages:
238
+ nvim-base:
239
+ description: "Neovim configuration with LazyVim for base"
240
+ files:
241
+ - source: "nvim/base/"
242
+ target: "~/.config/nvim"
243
+ doctor:
244
+ executables:
245
+ - name: nvim
246
+ severity: error
247
+ - name: git
248
+ severity: error
249
+
250
+ opencode:
251
+ description: "OpenCode AI assistant configuration"
252
+ files:
253
+ - source: "opencode"
254
+ target: "~/.config/opencode"
255
+ doctor:
256
+ executables:
257
+ - name: node
258
+ severity: warning
259
+ - name: bun
260
+ severity: warning
261
+
262
+ zsh:
263
+ description: "Zsh Config for the system"
264
+ files:
265
+ - source: "zsh/.zshrc"
266
+ target: "~/.zshrc"
267
+ doctor:
268
+ executables:
269
+ - name: zsh
270
+ severity: error
271
+ ```
272
+
273
+ ### Using the Doctor Command
274
+
275
+ ```bash
276
+ # Check all enabled packages
277
+ dotman doctor
278
+
279
+ # Check specific packages
280
+ dotman doctor nvim zsh
281
+
282
+ # Check with custom config directory
283
+ dotman doctor --config-dir ~/.dotfiles
284
+ ```
285
+
286
+ ### Example Output
287
+
288
+ ```
289
+ $ dotman doctor
290
+
291
+ ╭───────────────────────────────────────────────────────────────────────────╮
292
+ │ Doctor Check Results │
293
+ ├───────────────────────────────────────────────────────────────────────────┤
294
+ │ Package: nvim-base │
295
+ ├───────────────────────────────────────────────────────────────────────────┤
296
+ │ Executable Status Severity Path │
297
+ │ nvim ✓ Found error /usr/bin/nvim │
298
+ │ git ✓ Found error /usr/bin/git │
299
+ ├───────────────────────────────────────────────────────────────────────────┤
300
+ │ Package: opencode │
301
+ ├───────────────────────────────────────────────────────────────────────────┤
302
+ │ Executable Status Severity Path │
303
+ │ node ✓ Found warning /usr/bin/node │
304
+ │ bun ✗ Missing warning Not in PATH │
305
+ ├───────────────────────────────────────────────────────────────────────────┤
306
+ │ Package: zsh │
307
+ ├───────────────────────────────────────────────────────────────────────────┤
308
+ │ Executable Status Severity Path │
309
+ │ zsh ✓ Found error /usr/bin/zsh │
310
+ ╰───────────────────────────────────────────────────────────────────────────╯
311
+
312
+ Summary: 0 errors, 1 warning, 4 passed
313
+ ```
314
+
315
+ ### Exit Codes
316
+
317
+ - **Exit code 0**: No error-severity executables are missing (warnings are acceptable)
318
+ - **Exit code 1**: One or more error-severity executables are missing
319
+
320
+ ### How It Works
321
+
322
+ 1. The doctor command loads your configuration and resolves the package dependency tree
323
+ 2. For each package with `doctor.executables` defined, it checks if each executable exists in your system's PATH
324
+ 3. Executable lookup uses `shutil.which()` for cross-platform compatibility (Linux, macOS, Windows)
325
+ 4. Results are displayed in a formatted table grouped by package
326
+ 5. A summary shows totals for errors, warnings, and passed checks
327
+
328
+ ### Packages Without Doctor Config
329
+
330
+ Packages without a `doctor` configuration are still shown in the output with "No executable requirements":
331
+
332
+ ```
333
+ $ dotman doctor
334
+
335
+ ╭───────────────────────────────────────────────────────────────────────────╮
336
+ │ Doctor Check Results │
337
+ ├───────────────────────────────────────────────────────────────────────────┤
338
+ │ Package: simple-config │
339
+ ├───────────────────────────────────────────────────────────────────────────┤
340
+ │ Executable Status Severity Path │
341
+ │ No executable requirements │
342
+ ╰───────────────────────────────────────────────────────────────────────────╯
343
+
344
+ Summary: 0 errors, 0 warnings, 0 passed
345
+ ```
346
+
347
+ ### Best Practices
348
+
349
+ - Set `severity: error` for executables that are strictly required (e.g., `nvim`, `git`)
350
+ - Set `severity: warning` for executables that are optional or only needed for specific features
351
+ - Run `dotman doctor` before `dotman deploy` to catch missing dependencies early
352
+ - Use `--dry-run` with deploy to preview changes after fixing any doctor issues
353
+
203
354
  ### Overlapping Targets
204
355
 
205
356
  If multiple packages target the same directory, the first package (alphabetically) processes the files, and subsequent packages are skipped with a warning. This ensures predictable behavior:
@@ -15,7 +15,7 @@ classifiers = [
15
15
  "Environment :: Console",
16
16
  "License :: OSI Approved :: MIT License",
17
17
  ]
18
- version = "0.2.9"
18
+ version = "0.3.0"
19
19
  description = "One more dot file manager but an interesting and easy to use one."
20
20
  readme = "README.md"
21
21
  requires-python = ">=3.12"
@@ -5,6 +5,7 @@ from dotman.cli.commands import (
5
5
  add_repository,
6
6
  clone,
7
7
  deploy,
8
+ doctor,
8
9
  history,
9
10
  init,
10
11
  list_packages,
@@ -32,6 +33,7 @@ __all__ = [
32
33
  "add_repository",
33
34
  "app",
34
35
  "console",
36
+ "doctor",
35
37
  "get_config",
36
38
  "get_repository_option",
37
39
  "repo_app",
@@ -5,6 +5,7 @@ from dotman.cli.commands.absorb import absorb_changes
5
5
  from dotman.cli.commands.clean import clean
6
6
  from dotman.cli.commands.clone import clone
7
7
  from dotman.cli.commands.deploy import deploy, undeploy
8
+ from dotman.cli.commands.doctor import doctor
8
9
  from dotman.cli.commands.init import init
9
10
  from dotman.cli.commands.pull import pull
10
11
  from dotman.cli.commands.push import push
@@ -27,6 +28,7 @@ __all__ = [
27
28
  "clean",
28
29
  "clone",
29
30
  "deploy",
31
+ "doctor",
30
32
  "history",
31
33
  "init",
32
34
  "list_packages",
@@ -0,0 +1,135 @@
1
+ """Doctor command for checking required executables."""
2
+
3
+ from pathlib import Path
4
+ from typing import Annotated
5
+
6
+ import typer
7
+ from rich.table import Table
8
+
9
+ from dotman.cli_utils import app, console, get_config
10
+ from dotman.core.exceptions import MissingDependencyError
11
+ from dotman.services import DoctorCheckResult, DoctorCommandResult, ExecutableChecker
12
+
13
+
14
+ @app.command(name="doctor")
15
+ def doctor(
16
+ packages: Annotated[
17
+ list[str] | None,
18
+ typer.Argument(help="Packages to check (default: all enabled)"),
19
+ ] = None,
20
+ config_dir: Annotated[
21
+ Path | None,
22
+ typer.Option("--config-dir", "-c", help="The path of config directory"),
23
+ ] = None,
24
+ backup_dir: Annotated[
25
+ str | None,
26
+ typer.Option("--backup-dir", help="Override backup directory"),
27
+ ] = None,
28
+ repo_name: Annotated[
29
+ str | None,
30
+ typer.Option("--repo", "-r", help="Repository name"),
31
+ ] = None,
32
+ ) -> None:
33
+ """Check if required executables are present in PATH."""
34
+ config = get_config(config_dir, backup_dir, repo_name=repo_name)
35
+
36
+ if not config.is_initialized():
37
+ console.print("[red]Dotman is not initialized. Run 'dotman init' first.[/red]")
38
+ raise typer.Exit(1)
39
+
40
+ try:
41
+ packages_to_check = config.get_all_packages_with_dependencies(packages)
42
+ except MissingDependencyError as e:
43
+ console.print(f"[red]Dependency error:[/red] {e}")
44
+ raise typer.Exit(1)
45
+
46
+ if not packages_to_check:
47
+ console.print("[yellow]No packages to check.[/yellow]")
48
+ return
49
+
50
+ checker = ExecutableChecker()
51
+ all_results: list[DoctorCommandResult] = []
52
+
53
+ for pkg_name in packages_to_check:
54
+ pkg = config.get_package(pkg_name)
55
+ if not pkg:
56
+ continue
57
+
58
+ pkg_result = DoctorCommandResult(package_name=pkg_name)
59
+ if pkg.doctor is None or not pkg.doctor.executables:
60
+ all_results.append(pkg_result)
61
+ continue
62
+
63
+ for executable in pkg.doctor.executables:
64
+ found, path = checker.find_executable(executable.name)
65
+ check_result = DoctorCheckResult(
66
+ package_name=pkg_name,
67
+ executable_name=executable.name,
68
+ found=found,
69
+ path=path,
70
+ severity=executable.severity,
71
+ )
72
+ pkg_result.checks.append(check_result)
73
+
74
+ pkg_result.summary = pkg_result.compute_summary()
75
+ all_results.append(pkg_result)
76
+
77
+ table = Table(title="Doctor Check Results")
78
+ table.add_column("Package", style="cyan")
79
+ table.add_column("Executable", style="white")
80
+ table.add_column("Status", style="white")
81
+ table.add_column("Severity", style="white")
82
+ table.add_column("Path", style="dim")
83
+
84
+ for pkg_result in all_results:
85
+ table.add_section()
86
+ table.add_row(
87
+ f"[bold]{pkg_result.package_name}[/bold]",
88
+ "",
89
+ "",
90
+ "",
91
+ "",
92
+ )
93
+ if not pkg_result.checks:
94
+ table.add_row(
95
+ "",
96
+ "[dim]No executable requirements[/dim]",
97
+ "",
98
+ "",
99
+ "",
100
+ )
101
+ else:
102
+ for check in pkg_result.checks:
103
+ if check.found:
104
+ status = "[green]✓ Found[/green]"
105
+ else:
106
+ status = "[red]✗ Missing[/red]"
107
+
108
+ if check.severity == "error":
109
+ severity = "[red]error[/red]"
110
+ else:
111
+ severity = "[yellow]warning[/yellow]"
112
+
113
+ path_str = check.path if check.path else "Not in PATH"
114
+ table.add_row(
115
+ "",
116
+ check.executable_name,
117
+ status,
118
+ severity,
119
+ path_str,
120
+ )
121
+
122
+ console.print(table)
123
+
124
+ total_errors = sum(r.summary.get("errors", 0) for r in all_results)
125
+ total_warnings = sum(r.summary.get("warnings", 0) for r in all_results)
126
+ total_passed = sum(r.summary.get("passed", 0) for r in all_results)
127
+
128
+ console.print(
129
+ f"\nSummary: [red]{total_errors} errors[/red],"
130
+ f" [yellow]{total_warnings} warnings[/yellow],"
131
+ f" [green]{total_passed} passed[/green]"
132
+ )
133
+
134
+ if total_errors > 0:
135
+ raise typer.Exit(1)
@@ -1,7 +1,7 @@
1
1
  """Configuration management for Dotman."""
2
2
 
3
3
  from pathlib import Path
4
- from typing import Any
4
+ from typing import Any, Literal
5
5
 
6
6
  import yaml
7
7
  from pydantic import BaseModel, Field
@@ -33,6 +33,19 @@ class HookConfig(BaseModel):
33
33
  post_deploy: list[str] = Field(default_factory=list)
34
34
 
35
35
 
36
+ class DoctorExecutable(BaseModel):
37
+ """A required or optional executable for a package."""
38
+
39
+ name: str
40
+ severity: Literal["error", "warning"]
41
+
42
+
43
+ class DoctorConfig(BaseModel):
44
+ """Configuration for doctor executable checks."""
45
+
46
+ executables: list[DoctorExecutable] = Field(default_factory=list)
47
+
48
+
36
49
  class PackageConfig(BaseModel):
37
50
  """Configuration for a single package."""
38
51
 
@@ -40,6 +53,7 @@ class PackageConfig(BaseModel):
40
53
  files: list[FileMapping] = Field(default_factory=list)
41
54
  variables: dict[str, Any] = Field(default_factory=dict)
42
55
  hooks: HookConfig = Field(default_factory=HookConfig)
56
+ doctor: DoctorConfig | None = None
43
57
 
44
58
 
45
59
  class GlobalSettings(BaseModel):
@@ -209,3 +209,15 @@ class NothingToCommitError(RepositoryError):
209
209
  """No changes to commit in the repository."""
210
210
 
211
211
  pass
212
+
213
+
214
+ class DoctorError(DotmanError):
215
+ """Doctor-related errors."""
216
+
217
+ pass
218
+
219
+
220
+ class MissingExecutableError(DoctorError):
221
+ """Required executable is not found in PATH."""
222
+
223
+ pass
@@ -0,0 +1,19 @@
1
+ """Execution services for Dotman.
2
+
3
+ Provides services for executing shell command hooks during package deployment
4
+ and checking for required executables.
5
+ """
6
+
7
+ from dotman.services.doctor import (
8
+ DoctorCheckResult,
9
+ DoctorCommandResult,
10
+ ExecutableChecker,
11
+ )
12
+ from dotman.services.hook_executor import HookExecutor
13
+
14
+ __all__ = [
15
+ "DoctorCheckResult",
16
+ "DoctorCommandResult",
17
+ "ExecutableChecker",
18
+ "HookExecutor",
19
+ ]
@@ -0,0 +1,109 @@
1
+ """Executable checking services for the doctor command."""
2
+
3
+ import shutil
4
+ from typing import Literal
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+
9
+ class DoctorCheckResult(BaseModel):
10
+ """Result of a single executable check for a package."""
11
+
12
+ package_name: str
13
+ executable_name: str
14
+ found: bool
15
+ path: str | None
16
+ severity: Literal["error", "warning"]
17
+
18
+
19
+ class DoctorCommandResult(BaseModel):
20
+ """Result of the doctor command for a single package."""
21
+
22
+ package_name: str
23
+ checks: list[DoctorCheckResult] = Field(default_factory=list)
24
+ summary: dict[str, int] = Field(default_factory=dict)
25
+
26
+ def compute_summary(self) -> dict[str, int]:
27
+ """Compute summary statistics from checks.
28
+
29
+ Returns:
30
+ A dictionary with totals for 'errors', 'warnings', and 'passed'.
31
+ """
32
+ errors = 0
33
+ warnings = 0
34
+ passed = 0
35
+
36
+ for check in self.checks:
37
+ if check.found:
38
+ passed += 1
39
+ elif check.severity == "error":
40
+ errors += 1
41
+ else:
42
+ warnings += 1
43
+
44
+ return {"errors": errors, "warnings": warnings, "passed": passed}
45
+
46
+
47
+ class ExecutableChecker:
48
+ """Checks if executables are present in the system PATH."""
49
+
50
+ def __init__(self) -> None:
51
+ """Initialize the executable checker with an empty cache."""
52
+ self._cache: dict[str, tuple[bool, str | None]] = {}
53
+
54
+ def find_executable(self, name: str) -> tuple[bool, str | None]:
55
+ """Find an executable in the system PATH.
56
+
57
+ Args:
58
+ name: The name of the executable to find.
59
+
60
+ Returns:
61
+ A tuple of (found, path) where:
62
+ - found: True if the executable exists in PATH
63
+ - path: The full path to the executable if found, None otherwise
64
+ """
65
+ if not name:
66
+ return (False, None)
67
+
68
+ name = name.strip()
69
+
70
+ if not name:
71
+ return (False, None)
72
+
73
+ if name in self._cache:
74
+ return self._cache[name]
75
+
76
+ try:
77
+ path: str | None = shutil.which(name)
78
+ if path is not None:
79
+ result: tuple[bool, str | None] = (True, path)
80
+ else:
81
+ result = (False, None)
82
+ except (OSError, TypeError, ValueError):
83
+ result = (False, None)
84
+
85
+ self._cache[name] = result
86
+ return result
87
+
88
+ def find_executables(self, names: list[str]) -> dict[str, tuple[bool, str | None]]:
89
+ """Find multiple executables in the system PATH.
90
+
91
+ Args:
92
+ names: List of executable names to find.
93
+
94
+ Returns:
95
+ A dictionary mapping executable names to (found, path) tuples.
96
+ """
97
+ return {name: self.find_executable(name) for name in names}
98
+
99
+ def clear_cache(self) -> None:
100
+ """Clear the executable cache."""
101
+ self._cache.clear()
102
+
103
+ def get_cache_size(self) -> int:
104
+ """Get the number of cached lookups.
105
+
106
+ Returns:
107
+ The number of executables in the cache.
108
+ """
109
+ return len(self._cache)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dotman-cli
3
- Version: 0.2.9
3
+ Version: 0.3.0
4
4
  Summary: One more dot file manager but an interesting and easy to use one.
5
5
  Author: Nimit Bhardwaj
6
6
  License: MIT
@@ -165,6 +165,7 @@ packages:
165
165
  | `dotman rollback [id]` | Rollback a deployment by ID |
166
166
  | `dotman repo add <name>` | Register current directory as a repository |
167
167
  | `dotman repo list` | List all registered repositories |
168
+ | `dotman doctor [packages]` | Check required executables are present |
168
169
 
169
170
  ### Options
170
171
 
@@ -221,6 +222,156 @@ packages:
221
222
 
222
223
  The `absorb_ignore` field accepts a list of regex patterns that are matched against the full file path. If a file matches any pattern, it will be skipped during absorption.
223
224
 
225
+ ## Doctor Configuration
226
+
227
+ Dotman can check if required executables are present on your system before deploying packages. This helps identify missing dependencies early, preventing partial deployments and confusing errors.
228
+
229
+ ### Basic Configuration
230
+
231
+ Define executable requirements in your package configuration:
232
+
233
+ ```yaml
234
+ packages:
235
+ nvim:
236
+ depends: []
237
+ files:
238
+ - source: "nvim"
239
+ target: "~/.config/nvim"
240
+ doctor:
241
+ executables:
242
+ - name: nvim
243
+ severity: error
244
+ - name: git
245
+ severity: error
246
+ ```
247
+
248
+ ### Severity Levels
249
+
250
+ The `severity` field determines how missing executables are treated:
251
+
252
+ - **`error`**: The executable is required for the package to work. If missing, `dotman deploy` may fail or produce unexpected results. The doctor command returns exit code 1.
253
+ - **`warning`**: The executable is optional or only needed for certain features. If missing, the package may still work with reduced functionality. The doctor command returns exit code 0 but shows a warning.
254
+
255
+ ### Example Configuration
256
+
257
+ ```yaml
258
+ packages:
259
+ nvim-base:
260
+ description: "Neovim configuration with LazyVim for base"
261
+ files:
262
+ - source: "nvim/base/"
263
+ target: "~/.config/nvim"
264
+ doctor:
265
+ executables:
266
+ - name: nvim
267
+ severity: error
268
+ - name: git
269
+ severity: error
270
+
271
+ opencode:
272
+ description: "OpenCode AI assistant configuration"
273
+ files:
274
+ - source: "opencode"
275
+ target: "~/.config/opencode"
276
+ doctor:
277
+ executables:
278
+ - name: node
279
+ severity: warning
280
+ - name: bun
281
+ severity: warning
282
+
283
+ zsh:
284
+ description: "Zsh Config for the system"
285
+ files:
286
+ - source: "zsh/.zshrc"
287
+ target: "~/.zshrc"
288
+ doctor:
289
+ executables:
290
+ - name: zsh
291
+ severity: error
292
+ ```
293
+
294
+ ### Using the Doctor Command
295
+
296
+ ```bash
297
+ # Check all enabled packages
298
+ dotman doctor
299
+
300
+ # Check specific packages
301
+ dotman doctor nvim zsh
302
+
303
+ # Check with custom config directory
304
+ dotman doctor --config-dir ~/.dotfiles
305
+ ```
306
+
307
+ ### Example Output
308
+
309
+ ```
310
+ $ dotman doctor
311
+
312
+ ╭───────────────────────────────────────────────────────────────────────────╮
313
+ │ Doctor Check Results │
314
+ ├───────────────────────────────────────────────────────────────────────────┤
315
+ │ Package: nvim-base │
316
+ ├───────────────────────────────────────────────────────────────────────────┤
317
+ │ Executable Status Severity Path │
318
+ │ nvim ✓ Found error /usr/bin/nvim │
319
+ │ git ✓ Found error /usr/bin/git │
320
+ ├───────────────────────────────────────────────────────────────────────────┤
321
+ │ Package: opencode │
322
+ ├───────────────────────────────────────────────────────────────────────────┤
323
+ │ Executable Status Severity Path │
324
+ │ node ✓ Found warning /usr/bin/node │
325
+ │ bun ✗ Missing warning Not in PATH │
326
+ ├───────────────────────────────────────────────────────────────────────────┤
327
+ │ Package: zsh │
328
+ ├───────────────────────────────────────────────────────────────────────────┤
329
+ │ Executable Status Severity Path │
330
+ │ zsh ✓ Found error /usr/bin/zsh │
331
+ ╰───────────────────────────────────────────────────────────────────────────╯
332
+
333
+ Summary: 0 errors, 1 warning, 4 passed
334
+ ```
335
+
336
+ ### Exit Codes
337
+
338
+ - **Exit code 0**: No error-severity executables are missing (warnings are acceptable)
339
+ - **Exit code 1**: One or more error-severity executables are missing
340
+
341
+ ### How It Works
342
+
343
+ 1. The doctor command loads your configuration and resolves the package dependency tree
344
+ 2. For each package with `doctor.executables` defined, it checks if each executable exists in your system's PATH
345
+ 3. Executable lookup uses `shutil.which()` for cross-platform compatibility (Linux, macOS, Windows)
346
+ 4. Results are displayed in a formatted table grouped by package
347
+ 5. A summary shows totals for errors, warnings, and passed checks
348
+
349
+ ### Packages Without Doctor Config
350
+
351
+ Packages without a `doctor` configuration are still shown in the output with "No executable requirements":
352
+
353
+ ```
354
+ $ dotman doctor
355
+
356
+ ╭───────────────────────────────────────────────────────────────────────────╮
357
+ │ Doctor Check Results │
358
+ ├───────────────────────────────────────────────────────────────────────────┤
359
+ │ Package: simple-config │
360
+ ├───────────────────────────────────────────────────────────────────────────┤
361
+ │ Executable Status Severity Path │
362
+ │ No executable requirements │
363
+ ╰───────────────────────────────────────────────────────────────────────────╯
364
+
365
+ Summary: 0 errors, 0 warnings, 0 passed
366
+ ```
367
+
368
+ ### Best Practices
369
+
370
+ - Set `severity: error` for executables that are strictly required (e.g., `nvim`, `git`)
371
+ - Set `severity: warning` for executables that are optional or only needed for specific features
372
+ - Run `dotman doctor` before `dotman deploy` to catch missing dependencies early
373
+ - Use `--dry-run` with deploy to preview changes after fixing any doctor issues
374
+
224
375
  ### Overlapping Targets
225
376
 
226
377
  If multiple packages target the same directory, the first package (alphabetically) processes the files, and subsequent packages are skipped with a warning. This ensures predictable behavior:
@@ -10,6 +10,7 @@ src/dotman/cli/commands/absorb.py
10
10
  src/dotman/cli/commands/clean.py
11
11
  src/dotman/cli/commands/clone.py
12
12
  src/dotman/cli/commands/deploy.py
13
+ src/dotman/cli/commands/doctor.py
13
14
  src/dotman/cli/commands/history.py
14
15
  src/dotman/cli/commands/init.py
15
16
  src/dotman/cli/commands/pull.py
@@ -28,6 +29,7 @@ src/dotman/managers/remote.py
28
29
  src/dotman/managers/repository.py
29
30
  src/dotman/managers/watcher.py
30
31
  src/dotman/services/__init__.py
32
+ src/dotman/services/doctor.py
31
33
  src/dotman/services/hook_executor.py
32
34
  src/dotman_cli.egg-info/PKG-INFO
33
35
  src/dotman_cli.egg-info/SOURCES.txt
@@ -1,8 +0,0 @@
1
- """Execution services for Dotman.
2
-
3
- Provides services for executing shell command hooks during package deployment.
4
- """
5
-
6
- from dotman.services.hook_executor import HookExecutor
7
-
8
- __all__ = ["HookExecutor"]
File without changes
File without changes