dotman-cli 0.2.9__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. {dotman_cli-0.2.9/src/dotman_cli.egg-info → dotman_cli-0.4.0}/PKG-INFO +156 -2
  2. {dotman_cli-0.2.9 → dotman_cli-0.4.0}/README.md +155 -1
  3. {dotman_cli-0.2.9 → dotman_cli-0.4.0}/pyproject.toml +3 -1
  4. {dotman_cli-0.2.9 → dotman_cli-0.4.0}/src/dotman/cli/__init__.py +4 -2
  5. {dotman_cli-0.2.9 → dotman_cli-0.4.0}/src/dotman/cli/commands/__init__.py +4 -0
  6. {dotman_cli-0.2.9 → dotman_cli-0.4.0}/src/dotman/cli/commands/absorb.py +16 -4
  7. {dotman_cli-0.2.9 → dotman_cli-0.4.0}/src/dotman/cli/commands/deploy.py +37 -55
  8. dotman_cli-0.4.0/src/dotman/cli/commands/diff.py +93 -0
  9. dotman_cli-0.4.0/src/dotman/cli/commands/doctor.py +135 -0
  10. {dotman_cli-0.2.9 → dotman_cli-0.4.0}/src/dotman/cli/commands/history.py +32 -42
  11. {dotman_cli-0.2.9 → dotman_cli-0.4.0}/src/dotman/cli/commands/init.py +8 -4
  12. {dotman_cli-0.2.9 → dotman_cli-0.4.0}/src/dotman/cli/commands/push.py +1 -5
  13. {dotman_cli-0.2.9 → dotman_cli-0.4.0}/src/dotman/cli/commands/repo.py +4 -3
  14. {dotman_cli-0.2.9 → dotman_cli-0.4.0}/src/dotman/cli/commands/status.py +11 -39
  15. {dotman_cli-0.2.9 → dotman_cli-0.4.0}/src/dotman/cli/commands/watch.py +2 -2
  16. {dotman_cli-0.2.9 → dotman_cli-0.4.0}/src/dotman/cli_utils.py +15 -23
  17. {dotman_cli-0.2.9 → dotman_cli-0.4.0}/src/dotman/core/config.py +68 -5
  18. {dotman_cli-0.2.9 → dotman_cli-0.4.0}/src/dotman/core/exceptions.py +12 -0
  19. {dotman_cli-0.2.9 → dotman_cli-0.4.0}/src/dotman/core/link_manager.py +39 -23
  20. dotman_cli-0.4.0/src/dotman/main.py +20 -0
  21. {dotman_cli-0.2.9 → dotman_cli-0.4.0}/src/dotman/managers/history.py +2 -5
  22. {dotman_cli-0.2.9 → dotman_cli-0.4.0}/src/dotman/managers/remote.py +16 -1
  23. {dotman_cli-0.2.9 → dotman_cli-0.4.0}/src/dotman/managers/watcher.py +4 -0
  24. dotman_cli-0.4.0/src/dotman/services/__init__.py +19 -0
  25. dotman_cli-0.4.0/src/dotman/services/doctor.py +109 -0
  26. {dotman_cli-0.2.9 → dotman_cli-0.4.0/src/dotman_cli.egg-info}/PKG-INFO +156 -2
  27. {dotman_cli-0.2.9 → dotman_cli-0.4.0}/src/dotman_cli.egg-info/SOURCES.txt +3 -0
  28. dotman_cli-0.2.9/src/dotman/main.py +0 -13
  29. dotman_cli-0.2.9/src/dotman/services/__init__.py +0 -8
  30. {dotman_cli-0.2.9 → dotman_cli-0.4.0}/LICENSE +0 -0
  31. {dotman_cli-0.2.9 → dotman_cli-0.4.0}/setup.cfg +0 -0
  32. {dotman_cli-0.2.9 → dotman_cli-0.4.0}/src/dotman/__init__.py +0 -0
  33. {dotman_cli-0.2.9 → dotman_cli-0.4.0}/src/dotman/cli/commands/clean.py +0 -0
  34. {dotman_cli-0.2.9 → dotman_cli-0.4.0}/src/dotman/cli/commands/clone.py +0 -0
  35. {dotman_cli-0.2.9 → dotman_cli-0.4.0}/src/dotman/cli/commands/pull.py +0 -0
  36. {dotman_cli-0.2.9 → dotman_cli-0.4.0}/src/dotman/core/__init__.py +0 -0
  37. {dotman_cli-0.2.9 → dotman_cli-0.4.0}/src/dotman/core/template_engine.py +0 -0
  38. {dotman_cli-0.2.9 → dotman_cli-0.4.0}/src/dotman/managers/__init__.py +0 -0
  39. {dotman_cli-0.2.9 → dotman_cli-0.4.0}/src/dotman/managers/repository.py +0 -0
  40. {dotman_cli-0.2.9 → dotman_cli-0.4.0}/src/dotman/services/hook_executor.py +0 -0
  41. {dotman_cli-0.2.9 → dotman_cli-0.4.0}/src/dotman_cli.egg-info/dependency_links.txt +0 -0
  42. {dotman_cli-0.2.9 → dotman_cli-0.4.0}/src/dotman_cli.egg-info/entry_points.txt +0 -0
  43. {dotman_cli-0.2.9 → dotman_cli-0.4.0}/src/dotman_cli.egg-info/requires.txt +0 -0
  44. {dotman_cli-0.2.9 → dotman_cli-0.4.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.4.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
@@ -155,6 +155,7 @@ packages:
155
155
  | `dotman deploy [packages]` | Deploy dotfiles (create symlinks) |
156
156
  | `dotman undeploy [packages]` | Remove deployed symlinks |
157
157
  | `dotman status [packages]` | Show status of deployed dotfiles |
158
+ | `dotman diff [packages]` | Diff deployed files against what deploy would write (exit 1 on drift) |
158
159
  | `dotman list` | List all available packages |
159
160
  | `dotman absorb [packages]` | Absorb unmanaged files from target directories |
160
161
  | `dotman watch` | Watch for file changes and deploy automatically |
@@ -165,10 +166,13 @@ packages:
165
166
  | `dotman rollback [id]` | Rollback a deployment by ID |
166
167
  | `dotman repo add <name>` | Register current directory as a repository |
167
168
  | `dotman repo list` | List all registered repositories |
169
+ | `dotman doctor [packages]` | Check required executables are present |
168
170
 
169
171
  ### Options
170
172
 
171
173
  - `--config-dir, -c` - Override the config directory (default: `.dotman/` in current directory or `DOTMAN_CONFIG_DIR` env var)
174
+ - `--repo, -r` - Use a registered repository. With no flag and no dotman repo in the current directory, the default registered repository is used, so `dotman status` works from anywhere
175
+ - `--backup-dir` - Override `settings.backup_dir` (relative paths are relative to the repo)
172
176
  - `--dry-run, -n` - Preview changes without applying them
173
177
  - `--force, -f` - Overwrite existing files (with backup)
174
178
  - Specific packages can be passed to commands for targeted operations
@@ -219,7 +223,157 @@ packages:
219
223
  - ".git/**" # Ignore git directories
220
224
  ```
221
225
 
222
- 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.
226
+ Each `absorb_ignore` entry is tried as a glob against the path relative to the target (and the file name), then as a regex against the full path. If a file matches any pattern, it is skipped during absorption.
227
+
228
+ ## Doctor Configuration
229
+
230
+ 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.
231
+
232
+ ### Basic Configuration
233
+
234
+ Define executable requirements in your package configuration:
235
+
236
+ ```yaml
237
+ packages:
238
+ nvim:
239
+ depends: []
240
+ files:
241
+ - source: "nvim"
242
+ target: "~/.config/nvim"
243
+ doctor:
244
+ executables:
245
+ - name: nvim
246
+ severity: error
247
+ - name: git
248
+ severity: error
249
+ ```
250
+
251
+ ### Severity Levels
252
+
253
+ The `severity` field determines how missing executables are treated:
254
+
255
+ - **`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.
256
+ - **`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.
257
+
258
+ ### Example Configuration
259
+
260
+ ```yaml
261
+ packages:
262
+ nvim-base:
263
+ description: "Neovim configuration with LazyVim for base"
264
+ files:
265
+ - source: "nvim/base/"
266
+ target: "~/.config/nvim"
267
+ doctor:
268
+ executables:
269
+ - name: nvim
270
+ severity: error
271
+ - name: git
272
+ severity: error
273
+
274
+ opencode:
275
+ description: "OpenCode AI assistant configuration"
276
+ files:
277
+ - source: "opencode"
278
+ target: "~/.config/opencode"
279
+ doctor:
280
+ executables:
281
+ - name: node
282
+ severity: warning
283
+ - name: bun
284
+ severity: warning
285
+
286
+ zsh:
287
+ description: "Zsh Config for the system"
288
+ files:
289
+ - source: "zsh/.zshrc"
290
+ target: "~/.zshrc"
291
+ doctor:
292
+ executables:
293
+ - name: zsh
294
+ severity: error
295
+ ```
296
+
297
+ ### Using the Doctor Command
298
+
299
+ ```bash
300
+ # Check all enabled packages
301
+ dotman doctor
302
+
303
+ # Check specific packages
304
+ dotman doctor nvim zsh
305
+
306
+ # Check with custom config directory
307
+ dotman doctor --config-dir ~/.dotfiles
308
+ ```
309
+
310
+ ### Example Output
311
+
312
+ ```
313
+ $ dotman doctor
314
+
315
+ ╭───────────────────────────────────────────────────────────────────────────╮
316
+ │ Doctor Check Results │
317
+ ├───────────────────────────────────────────────────────────────────────────┤
318
+ │ Package: nvim-base │
319
+ ├───────────────────────────────────────────────────────────────────────────┤
320
+ │ Executable Status Severity Path │
321
+ │ nvim ✓ Found error /usr/bin/nvim │
322
+ │ git ✓ Found error /usr/bin/git │
323
+ ├───────────────────────────────────────────────────────────────────────────┤
324
+ │ Package: opencode │
325
+ ├───────────────────────────────────────────────────────────────────────────┤
326
+ │ Executable Status Severity Path │
327
+ │ node ✓ Found warning /usr/bin/node │
328
+ │ bun ✗ Missing warning Not in PATH │
329
+ ├───────────────────────────────────────────────────────────────────────────┤
330
+ │ Package: zsh │
331
+ ├───────────────────────────────────────────────────────────────────────────┤
332
+ │ Executable Status Severity Path │
333
+ │ zsh ✓ Found error /usr/bin/zsh │
334
+ ╰───────────────────────────────────────────────────────────────────────────╯
335
+
336
+ Summary: 0 errors, 1 warning, 4 passed
337
+ ```
338
+
339
+ ### Exit Codes
340
+
341
+ - **Exit code 0**: No error-severity executables are missing (warnings are acceptable)
342
+ - **Exit code 1**: One or more error-severity executables are missing
343
+
344
+ ### How It Works
345
+
346
+ 1. The doctor command loads your configuration and resolves the package dependency tree
347
+ 2. For each package with `doctor.executables` defined, it checks if each executable exists in your system's PATH
348
+ 3. Executable lookup uses `shutil.which()` for cross-platform compatibility (Linux, macOS, Windows)
349
+ 4. Results are displayed in a formatted table grouped by package
350
+ 5. A summary shows totals for errors, warnings, and passed checks
351
+
352
+ ### Packages Without Doctor Config
353
+
354
+ Packages without a `doctor` configuration are still shown in the output with "No executable requirements":
355
+
356
+ ```
357
+ $ dotman doctor
358
+
359
+ ╭───────────────────────────────────────────────────────────────────────────╮
360
+ │ Doctor Check Results │
361
+ ├───────────────────────────────────────────────────────────────────────────┤
362
+ │ Package: simple-config │
363
+ ├───────────────────────────────────────────────────────────────────────────┤
364
+ │ Executable Status Severity Path │
365
+ │ No executable requirements │
366
+ ╰───────────────────────────────────────────────────────────────────────────╯
367
+
368
+ Summary: 0 errors, 0 warnings, 0 passed
369
+ ```
370
+
371
+ ### Best Practices
372
+
373
+ - Set `severity: error` for executables that are strictly required (e.g., `nvim`, `git`)
374
+ - Set `severity: warning` for executables that are optional or only needed for specific features
375
+ - Run `dotman doctor` before `dotman deploy` to catch missing dependencies early
376
+ - Use `--dry-run` with deploy to preview changes after fixing any doctor issues
223
377
 
224
378
  ### Overlapping Targets
225
379
 
@@ -134,6 +134,7 @@ packages:
134
134
  | `dotman deploy [packages]` | Deploy dotfiles (create symlinks) |
135
135
  | `dotman undeploy [packages]` | Remove deployed symlinks |
136
136
  | `dotman status [packages]` | Show status of deployed dotfiles |
137
+ | `dotman diff [packages]` | Diff deployed files against what deploy would write (exit 1 on drift) |
137
138
  | `dotman list` | List all available packages |
138
139
  | `dotman absorb [packages]` | Absorb unmanaged files from target directories |
139
140
  | `dotman watch` | Watch for file changes and deploy automatically |
@@ -144,10 +145,13 @@ packages:
144
145
  | `dotman rollback [id]` | Rollback a deployment by ID |
145
146
  | `dotman repo add <name>` | Register current directory as a repository |
146
147
  | `dotman repo list` | List all registered repositories |
148
+ | `dotman doctor [packages]` | Check required executables are present |
147
149
 
148
150
  ### Options
149
151
 
150
152
  - `--config-dir, -c` - Override the config directory (default: `.dotman/` in current directory or `DOTMAN_CONFIG_DIR` env var)
153
+ - `--repo, -r` - Use a registered repository. With no flag and no dotman repo in the current directory, the default registered repository is used, so `dotman status` works from anywhere
154
+ - `--backup-dir` - Override `settings.backup_dir` (relative paths are relative to the repo)
151
155
  - `--dry-run, -n` - Preview changes without applying them
152
156
  - `--force, -f` - Overwrite existing files (with backup)
153
157
  - Specific packages can be passed to commands for targeted operations
@@ -198,7 +202,157 @@ packages:
198
202
  - ".git/**" # Ignore git directories
199
203
  ```
200
204
 
201
- 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.
205
+ Each `absorb_ignore` entry is tried as a glob against the path relative to the target (and the file name), then as a regex against the full path. If a file matches any pattern, it is skipped during absorption.
206
+
207
+ ## Doctor Configuration
208
+
209
+ 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.
210
+
211
+ ### Basic Configuration
212
+
213
+ Define executable requirements in your package configuration:
214
+
215
+ ```yaml
216
+ packages:
217
+ nvim:
218
+ depends: []
219
+ files:
220
+ - source: "nvim"
221
+ target: "~/.config/nvim"
222
+ doctor:
223
+ executables:
224
+ - name: nvim
225
+ severity: error
226
+ - name: git
227
+ severity: error
228
+ ```
229
+
230
+ ### Severity Levels
231
+
232
+ The `severity` field determines how missing executables are treated:
233
+
234
+ - **`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.
235
+ - **`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.
236
+
237
+ ### Example Configuration
238
+
239
+ ```yaml
240
+ packages:
241
+ nvim-base:
242
+ description: "Neovim configuration with LazyVim for base"
243
+ files:
244
+ - source: "nvim/base/"
245
+ target: "~/.config/nvim"
246
+ doctor:
247
+ executables:
248
+ - name: nvim
249
+ severity: error
250
+ - name: git
251
+ severity: error
252
+
253
+ opencode:
254
+ description: "OpenCode AI assistant configuration"
255
+ files:
256
+ - source: "opencode"
257
+ target: "~/.config/opencode"
258
+ doctor:
259
+ executables:
260
+ - name: node
261
+ severity: warning
262
+ - name: bun
263
+ severity: warning
264
+
265
+ zsh:
266
+ description: "Zsh Config for the system"
267
+ files:
268
+ - source: "zsh/.zshrc"
269
+ target: "~/.zshrc"
270
+ doctor:
271
+ executables:
272
+ - name: zsh
273
+ severity: error
274
+ ```
275
+
276
+ ### Using the Doctor Command
277
+
278
+ ```bash
279
+ # Check all enabled packages
280
+ dotman doctor
281
+
282
+ # Check specific packages
283
+ dotman doctor nvim zsh
284
+
285
+ # Check with custom config directory
286
+ dotman doctor --config-dir ~/.dotfiles
287
+ ```
288
+
289
+ ### Example Output
290
+
291
+ ```
292
+ $ dotman doctor
293
+
294
+ ╭───────────────────────────────────────────────────────────────────────────╮
295
+ │ Doctor Check Results │
296
+ ├───────────────────────────────────────────────────────────────────────────┤
297
+ │ Package: nvim-base │
298
+ ├───────────────────────────────────────────────────────────────────────────┤
299
+ │ Executable Status Severity Path │
300
+ │ nvim ✓ Found error /usr/bin/nvim │
301
+ │ git ✓ Found error /usr/bin/git │
302
+ ├───────────────────────────────────────────────────────────────────────────┤
303
+ │ Package: opencode │
304
+ ├───────────────────────────────────────────────────────────────────────────┤
305
+ │ Executable Status Severity Path │
306
+ │ node ✓ Found warning /usr/bin/node │
307
+ │ bun ✗ Missing warning Not in PATH │
308
+ ├───────────────────────────────────────────────────────────────────────────┤
309
+ │ Package: zsh │
310
+ ├───────────────────────────────────────────────────────────────────────────┤
311
+ │ Executable Status Severity Path │
312
+ │ zsh ✓ Found error /usr/bin/zsh │
313
+ ╰───────────────────────────────────────────────────────────────────────────╯
314
+
315
+ Summary: 0 errors, 1 warning, 4 passed
316
+ ```
317
+
318
+ ### Exit Codes
319
+
320
+ - **Exit code 0**: No error-severity executables are missing (warnings are acceptable)
321
+ - **Exit code 1**: One or more error-severity executables are missing
322
+
323
+ ### How It Works
324
+
325
+ 1. The doctor command loads your configuration and resolves the package dependency tree
326
+ 2. For each package with `doctor.executables` defined, it checks if each executable exists in your system's PATH
327
+ 3. Executable lookup uses `shutil.which()` for cross-platform compatibility (Linux, macOS, Windows)
328
+ 4. Results are displayed in a formatted table grouped by package
329
+ 5. A summary shows totals for errors, warnings, and passed checks
330
+
331
+ ### Packages Without Doctor Config
332
+
333
+ Packages without a `doctor` configuration are still shown in the output with "No executable requirements":
334
+
335
+ ```
336
+ $ dotman doctor
337
+
338
+ ╭───────────────────────────────────────────────────────────────────────────╮
339
+ │ Doctor Check Results │
340
+ ├───────────────────────────────────────────────────────────────────────────┤
341
+ │ Package: simple-config │
342
+ ├───────────────────────────────────────────────────────────────────────────┤
343
+ │ Executable Status Severity Path │
344
+ │ No executable requirements │
345
+ ╰───────────────────────────────────────────────────────────────────────────╯
346
+
347
+ Summary: 0 errors, 0 warnings, 0 passed
348
+ ```
349
+
350
+ ### Best Practices
351
+
352
+ - Set `severity: error` for executables that are strictly required (e.g., `nvim`, `git`)
353
+ - Set `severity: warning` for executables that are optional or only needed for specific features
354
+ - Run `dotman doctor` before `dotman deploy` to catch missing dependencies early
355
+ - Use `--dry-run` with deploy to preview changes after fixing any doctor issues
202
356
 
203
357
  ### Overlapping Targets
204
358
 
@@ -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.4.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"
@@ -74,6 +74,8 @@ section-order = ["future", "standard-library", "third-party", "first-party", "lo
74
74
  combine-as-imports = true
75
75
 
76
76
  [tool.mypy]
77
+ # select.kqueue only exists in the darwin stubs; check as darwin everywhere
78
+ platform = "darwin"
77
79
  warn_return_any = true
78
80
  warn_unused_configs = true
79
81
  warn_redundant_casts = true
@@ -5,6 +5,8 @@ from dotman.cli.commands import (
5
5
  add_repository,
6
6
  clone,
7
7
  deploy,
8
+ diff,
9
+ doctor,
8
10
  history,
9
11
  init,
10
12
  list_packages,
@@ -23,7 +25,6 @@ from dotman.cli_utils import (
23
25
  app,
24
26
  console,
25
27
  get_config,
26
- get_repository_option,
27
28
  repo_app,
28
29
  )
29
30
 
@@ -32,14 +33,15 @@ __all__ = [
32
33
  "add_repository",
33
34
  "app",
34
35
  "console",
36
+ "doctor",
35
37
  "get_config",
36
- "get_repository_option",
37
38
  "repo_app",
38
39
  "init",
39
40
  "clone",
40
41
  "push",
41
42
  "pull",
42
43
  "deploy",
44
+ "diff",
43
45
  "undeploy",
44
46
  "status",
45
47
  "list_packages",
@@ -5,6 +5,8 @@ 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.diff import diff
9
+ from dotman.cli.commands.doctor import doctor
8
10
  from dotman.cli.commands.init import init
9
11
  from dotman.cli.commands.pull import pull
10
12
  from dotman.cli.commands.push import push
@@ -27,6 +29,8 @@ __all__ = [
27
29
  "clean",
28
30
  "clone",
29
31
  "deploy",
32
+ "diff",
33
+ "doctor",
30
34
  "history",
31
35
  "init",
32
36
  "list_packages",
@@ -1,5 +1,6 @@
1
1
  """Absorb command for dotman CLI."""
2
2
 
3
+ import fnmatch
3
4
  import re
4
5
  from pathlib import Path
5
6
  from typing import Annotated
@@ -10,10 +11,21 @@ from dotman.cli_utils import app, console, get_config
10
11
  from dotman.core.link_manager import LinkManager
11
12
 
12
13
 
14
+ def _is_ignored(pattern: str, path: Path, relative: Path) -> bool:
15
+ """Match a pattern as a glob on the relative path, or a regex on the full path."""
16
+ if fnmatch.fnmatch(str(relative), pattern) or fnmatch.fnmatch(path.name, pattern):
17
+ return True
18
+ try:
19
+ return re.search(pattern, str(path)) is not None
20
+ except re.error:
21
+ return False
22
+
23
+
13
24
  def _should_skip_file(
14
25
  target_file: Path,
15
26
  absorb_ignore: list[str] | None,
16
27
  dest_file: Path,
28
+ relative: Path | None = None,
17
29
  ) -> tuple[bool, str]:
18
30
  """Check if a file should be skipped during absorption.
19
31
 
@@ -24,8 +36,9 @@ def _should_skip_file(
24
36
  return True, "symlink"
25
37
  if target_file.name.endswith(".j2"):
26
38
  return True, "template"
39
+ relative = relative or Path(target_file.name)
27
40
  if absorb_ignore and any(
28
- re.search(pattern, str(target_file)) for pattern in absorb_ignore
41
+ _is_ignored(pattern, target_file, relative) for pattern in absorb_ignore
29
42
  ):
30
43
  return True, "ignored"
31
44
  if dest_file.exists():
@@ -94,9 +107,8 @@ def absorb_changes(
94
107
  console.print("[red]Dotman is not initialized. Run 'dotman init' first.[/red]")
95
108
  raise typer.Exit(1)
96
109
 
97
- packages_to_absorb = packages or config.get_enabled_packages()
98
110
  # Sort for deterministic behavior (first package wins conflicts)
99
- packages_to_absorb.sort()
111
+ packages_to_absorb = sorted(packages or config.get_enabled_packages())
100
112
 
101
113
  package_objs = []
102
114
  for package_name in packages_to_absorb:
@@ -138,7 +150,7 @@ def absorb_changes(
138
150
  dest_file = source / relative_path
139
151
 
140
152
  should_skip, _ = _should_skip_file(
141
- target_file, absorb_ignore, dest_file
153
+ target_file, absorb_ignore, dest_file, relative_path
142
154
  )
143
155
  if should_skip:
144
156
  continue
@@ -10,8 +10,6 @@ from dotman.cli_utils import app, console, get_config
10
10
  from dotman.core.exceptions import (
11
11
  DotmanError,
12
12
  HookExecutionError,
13
- LinkExistsError,
14
- LinkTargetMissingError,
15
13
  MissingDependencyError,
16
14
  )
17
15
  from dotman.core.link_manager import LinkManager, LinkStatus
@@ -44,17 +42,13 @@ def deploy(
44
42
  str | None,
45
43
  typer.Option("--backup-dir", help="Override backup directory"),
46
44
  ] = None,
47
- template_suffix: Annotated[
48
- str | None,
49
- typer.Option("--template-suffix", help="Override template suffix"),
50
- ] = None,
51
45
  repo_name: Annotated[
52
46
  str | None,
53
47
  typer.Option("--repo", "-r", help="Repository name"),
54
48
  ] = None,
55
49
  ) -> None:
56
50
  """Deploy dotfiles by creating symlinks."""
57
- config = get_config(config_dir, backup_dir, template_suffix, repo_name)
51
+ config = get_config(config_dir, backup_dir, repo_name)
58
52
 
59
53
  if repo_name:
60
54
  console.print(f"[cyan]Using repository: {repo_name}[/cyan]")
@@ -86,6 +80,7 @@ def deploy(
86
80
 
87
81
  deployment_id = str(uuid.uuid4())[:8]
88
82
  deployed_files: list[DeployedFile] = []
83
+ failed = False
89
84
 
90
85
  if dry_run:
91
86
  console.print("[cyan]Dry run mode - no changes will be made[/cyan]")
@@ -126,6 +121,7 @@ def deploy(
126
121
  f" [yellow]Skipping package '{pkg_name}'...[/yellow]"
127
122
  )
128
123
  skip_package = True
124
+ failed = True
129
125
  break
130
126
 
131
127
  if skip_package:
@@ -136,57 +132,39 @@ def deploy(
136
132
  target = Path(file_mapping.target).expanduser()
137
133
 
138
134
  try:
139
- is_template = link_manager.is_template_file(source)
140
-
141
- if is_template:
142
- if not dry_run:
143
- template_engine.render_file(source, variables, target)
144
- console.print(f" [green]Rendered:[/green] {target}")
145
- else:
146
- console.print(f" [cyan]Would render:[/cyan] {target}")
135
+ results = link_manager.create_link(
136
+ source, target, force, dry_run, template_engine, variables
137
+ )
138
+ except DotmanError as e:
139
+ console.print(f" [red]Error:[/red] {e}")
140
+ failed = True
141
+ continue
147
142
 
148
- deployed_files.append(
149
- DeployedFile(
150
- source=str(source),
151
- target=str(target),
152
- is_template=True,
153
- )
154
- )
143
+ for result in results:
144
+ if result.status != LinkStatus.LINKED:
145
+ continue
146
+ is_template = link_manager.is_template_file(result.source)
147
+ if dry_run:
148
+ console.print(f" [cyan]{result.message}[/cyan]")
149
+ elif is_template:
150
+ console.print(f" [green]Rendered:[/green] {result.target}")
155
151
  else:
156
- results = link_manager.create_link(
157
- source, target, force, dry_run, template_engine, variables
152
+ console.print(
153
+ f" [green]Linked:[/green] {result.target} -> {result.source}"
154
+ )
155
+ if result.backed_up:
156
+ console.print(
157
+ f" [yellow]Backed up to:[/yellow] {result.backed_up}"
158
158
  )
159
- for result in results:
160
- if result.status == LinkStatus.LINKED:
161
- if dry_run:
162
- console.print(f" [cyan]{result.message}[/cyan]")
163
- else:
164
- console.print(
165
- f" [green]Linked:[/green]"
166
- f" {result.target} -> {result.source}"
167
- )
168
- if result.backed_up:
169
- console.print(
170
- f" [yellow]Backed"
171
- f" up to:[/yellow] {result.backed_up}"
172
- )
173
-
174
- deployed_files.append(
175
- DeployedFile(
176
- source=str(result.source),
177
- target=str(result.target),
178
- is_template=False,
179
- backup_path=str(result.backed_up)
180
- if result.backed_up
181
- else None,
182
- )
183
- )
184
- except LinkExistsError as e:
185
- console.print(f" [red]Error:[/red] {e}")
186
- except LinkTargetMissingError as e:
187
- console.print(f" [red]Error:[/red] {e}")
188
- except DotmanError as e:
189
- console.print(f" [red]Error:[/red] {e}")
159
+
160
+ deployed_files.append(
161
+ DeployedFile(
162
+ source=str(result.source),
163
+ target=str(result.target),
164
+ is_template=is_template,
165
+ backup_path=str(result.backed_up) if result.backed_up else None,
166
+ )
167
+ )
190
168
 
191
169
  if pkg.hooks.post_deploy:
192
170
  target_dir = None
@@ -232,6 +210,10 @@ def deploy(
232
210
  else:
233
211
  console.print("\n[green]Deploy complete![/green]")
234
212
 
213
+ if failed:
214
+ console.print("[red]Some files failed to deploy (see errors above).[/red]")
215
+ raise typer.Exit(1)
216
+
235
217
 
236
218
  @app.command()
237
219
  def undeploy(