dotman-cli 0.2.7__tar.gz → 0.2.9__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 (40) hide show
  1. {dotman_cli-0.2.7 → dotman_cli-0.2.9}/PKG-INFO +1 -1
  2. {dotman_cli-0.2.7 → dotman_cli-0.2.9}/pyproject.toml +1 -1
  3. dotman_cli-0.2.9/src/dotman/__init__.py +5 -0
  4. {dotman_cli-0.2.7 → dotman_cli-0.2.9}/src/dotman/cli/commands/__init__.py +2 -0
  5. dotman_cli-0.2.9/src/dotman/cli/commands/clean.py +195 -0
  6. {dotman_cli-0.2.7 → dotman_cli-0.2.9}/src/dotman/cli_utils.py +11 -0
  7. {dotman_cli-0.2.7 → dotman_cli-0.2.9}/src/dotman_cli.egg-info/PKG-INFO +1 -1
  8. {dotman_cli-0.2.7 → dotman_cli-0.2.9}/src/dotman_cli.egg-info/SOURCES.txt +1 -0
  9. dotman_cli-0.2.7/src/dotman/__init__.py +0 -3
  10. {dotman_cli-0.2.7 → dotman_cli-0.2.9}/LICENSE +0 -0
  11. {dotman_cli-0.2.7 → dotman_cli-0.2.9}/README.md +0 -0
  12. {dotman_cli-0.2.7 → dotman_cli-0.2.9}/setup.cfg +0 -0
  13. {dotman_cli-0.2.7 → dotman_cli-0.2.9}/src/dotman/cli/__init__.py +0 -0
  14. {dotman_cli-0.2.7 → dotman_cli-0.2.9}/src/dotman/cli/commands/absorb.py +0 -0
  15. {dotman_cli-0.2.7 → dotman_cli-0.2.9}/src/dotman/cli/commands/clone.py +0 -0
  16. {dotman_cli-0.2.7 → dotman_cli-0.2.9}/src/dotman/cli/commands/deploy.py +0 -0
  17. {dotman_cli-0.2.7 → dotman_cli-0.2.9}/src/dotman/cli/commands/history.py +0 -0
  18. {dotman_cli-0.2.7 → dotman_cli-0.2.9}/src/dotman/cli/commands/init.py +0 -0
  19. {dotman_cli-0.2.7 → dotman_cli-0.2.9}/src/dotman/cli/commands/pull.py +0 -0
  20. {dotman_cli-0.2.7 → dotman_cli-0.2.9}/src/dotman/cli/commands/push.py +0 -0
  21. {dotman_cli-0.2.7 → dotman_cli-0.2.9}/src/dotman/cli/commands/repo.py +0 -0
  22. {dotman_cli-0.2.7 → dotman_cli-0.2.9}/src/dotman/cli/commands/status.py +0 -0
  23. {dotman_cli-0.2.7 → dotman_cli-0.2.9}/src/dotman/cli/commands/watch.py +0 -0
  24. {dotman_cli-0.2.7 → dotman_cli-0.2.9}/src/dotman/core/__init__.py +0 -0
  25. {dotman_cli-0.2.7 → dotman_cli-0.2.9}/src/dotman/core/config.py +0 -0
  26. {dotman_cli-0.2.7 → dotman_cli-0.2.9}/src/dotman/core/exceptions.py +0 -0
  27. {dotman_cli-0.2.7 → dotman_cli-0.2.9}/src/dotman/core/link_manager.py +0 -0
  28. {dotman_cli-0.2.7 → dotman_cli-0.2.9}/src/dotman/core/template_engine.py +0 -0
  29. {dotman_cli-0.2.7 → dotman_cli-0.2.9}/src/dotman/main.py +0 -0
  30. {dotman_cli-0.2.7 → dotman_cli-0.2.9}/src/dotman/managers/__init__.py +0 -0
  31. {dotman_cli-0.2.7 → dotman_cli-0.2.9}/src/dotman/managers/history.py +0 -0
  32. {dotman_cli-0.2.7 → dotman_cli-0.2.9}/src/dotman/managers/remote.py +0 -0
  33. {dotman_cli-0.2.7 → dotman_cli-0.2.9}/src/dotman/managers/repository.py +0 -0
  34. {dotman_cli-0.2.7 → dotman_cli-0.2.9}/src/dotman/managers/watcher.py +0 -0
  35. {dotman_cli-0.2.7 → dotman_cli-0.2.9}/src/dotman/services/__init__.py +0 -0
  36. {dotman_cli-0.2.7 → dotman_cli-0.2.9}/src/dotman/services/hook_executor.py +0 -0
  37. {dotman_cli-0.2.7 → dotman_cli-0.2.9}/src/dotman_cli.egg-info/dependency_links.txt +0 -0
  38. {dotman_cli-0.2.7 → dotman_cli-0.2.9}/src/dotman_cli.egg-info/entry_points.txt +0 -0
  39. {dotman_cli-0.2.7 → dotman_cli-0.2.9}/src/dotman_cli.egg-info/requires.txt +0 -0
  40. {dotman_cli-0.2.7 → dotman_cli-0.2.9}/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.7
3
+ Version: 0.2.9
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
@@ -15,7 +15,7 @@ classifiers = [
15
15
  "Environment :: Console",
16
16
  "License :: OSI Approved :: MIT License",
17
17
  ]
18
- version = "0.2.7"
18
+ version = "0.2.9"
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"
@@ -0,0 +1,5 @@
1
+ """dotman - A dotfile manager using symlinks and templates."""
2
+
3
+ from importlib.metadata import version
4
+
5
+ __version__ = version("dotman-cli")
@@ -2,6 +2,7 @@
2
2
 
3
3
  from dotman.cli.commands import history as history_mod
4
4
  from dotman.cli.commands.absorb import absorb_changes
5
+ from dotman.cli.commands.clean import clean
5
6
  from dotman.cli.commands.clone import clone
6
7
  from dotman.cli.commands.deploy import deploy, undeploy
7
8
  from dotman.cli.commands.init import init
@@ -23,6 +24,7 @@ rollback = history_mod.rollback
23
24
  __all__ = [
24
25
  "absorb_changes",
25
26
  "add_repository",
27
+ "clean",
26
28
  "clone",
27
29
  "deploy",
28
30
  "history",
@@ -0,0 +1,195 @@
1
+ """Clean commands for dotman CLI."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Annotated
6
+
7
+ import typer
8
+ from rich.tree import Tree
9
+
10
+ from dotman.cli_utils import app, console, get_config
11
+
12
+
13
+ def find_empty_dirs(root: Path) -> list[Path]:
14
+ """Find all empty directories under the given root directory.
15
+
16
+ Args:
17
+ root: The root directory to search for empty directories.
18
+
19
+ Returns:
20
+ A list of Path objects pointing to empty directories, sorted.
21
+ """
22
+ empty_dirs: list[Path] = []
23
+
24
+ for dirpath, dirnames, filenames in os.walk(root, topdown=True):
25
+ if dirpath != str(root) and not dirnames and not filenames:
26
+ empty_dirs.append(Path(dirpath))
27
+
28
+ empty_dirs.sort()
29
+ return empty_dirs
30
+
31
+
32
+ def find_orphaned_symlinks(root: Path) -> list[Path]:
33
+ """Find all orphaned symlinks under the given root directory.
34
+
35
+ Args:
36
+ root: The root directory to search for orphaned symlinks.
37
+
38
+ Returns:
39
+ A list of Path objects pointing to orphaned symlinks.
40
+ """
41
+ orphaned: list[Path] = []
42
+
43
+ for dirpath, dirnames, filenames in os.walk(root, followlinks=False):
44
+ for name in (*dirnames, *filenames):
45
+ path = Path(dirpath) / name
46
+
47
+ if path.is_symlink():
48
+ try:
49
+ resolved = path.resolve()
50
+ if not resolved.exists():
51
+ orphaned.append(path)
52
+ except (OSError, RuntimeError):
53
+ orphaned.append(path)
54
+
55
+ return orphaned
56
+
57
+
58
+ @app.command(name="clean")
59
+ def clean(
60
+ packages: Annotated[
61
+ list[str] | None,
62
+ typer.Argument(help="Packages to clean (default: all)"),
63
+ ] = None,
64
+ config_dir: Annotated[
65
+ Path | None,
66
+ typer.Option("--config-dir", "-c", help="The path of config directory"),
67
+ ] = None,
68
+ backup_dir: Annotated[
69
+ str | None,
70
+ typer.Option("--backup-dir", help="Override backup directory"),
71
+ ] = None,
72
+ repo_name: Annotated[
73
+ str | None,
74
+ typer.Option("--repo", "-r", help="Repository name"),
75
+ ] = None,
76
+ dry_run: Annotated[
77
+ bool,
78
+ typer.Option(
79
+ "--dry-run", "-n", help="Show what would be removed without removing"
80
+ ),
81
+ ] = False,
82
+ ) -> None:
83
+ """Clean up deployment artifacts and temporary files."""
84
+ config = get_config(config_dir, backup_dir, repo_name=repo_name)
85
+
86
+ if not config.is_initialized():
87
+ console.print("[red]Dotman is not initialized. Run 'dotman init' first.[/red]")
88
+ raise typer.Exit(1)
89
+
90
+ packages_to_clean = packages or config.get_enabled_packages()
91
+
92
+ if not packages_to_clean:
93
+ console.print("[yellow]No packages to clean.[/yellow]")
94
+ return
95
+
96
+ orphaned_symlinks: dict[str, list[Path]] = {}
97
+ empty_dirs: dict[str, list[Path]] = {}
98
+
99
+ for pkg_name in packages_to_clean:
100
+ pkg = config.get_package(pkg_name)
101
+ if not pkg:
102
+ console.print(
103
+ f"[yellow]Package '{pkg_name}' not found in config, skipping.[/yellow]"
104
+ )
105
+ continue
106
+
107
+ for file_mapping in pkg.files:
108
+ target = Path(file_mapping.target).expanduser()
109
+
110
+ if target.is_dir():
111
+ target_dir = target
112
+ if target_dir.exists():
113
+ orphaned = find_orphaned_symlinks(target_dir)
114
+ empty = find_empty_dirs(target_dir)
115
+
116
+ if orphaned:
117
+ orphaned_symlinks[pkg_name] = orphaned
118
+ if empty:
119
+ empty_dirs[pkg_name] = empty
120
+ elif target.is_file() or target.exists() or target.is_symlink():
121
+ # For file targets, check if the file itself is an orphaned symlink
122
+ if target.is_symlink():
123
+ try:
124
+ if not target.resolve().exists():
125
+ orphaned_symlinks.setdefault(pkg_name, []).append(target)
126
+ except (OSError, RuntimeError):
127
+ orphaned_symlinks.setdefault(pkg_name, []).append(target)
128
+
129
+ total_orphaned = sum(len(v) for v in orphaned_symlinks.values())
130
+ total_empty = sum(len(v) for v in empty_dirs.values())
131
+
132
+ if total_orphaned == 0 and total_empty == 0:
133
+ console.print("[green]No orphaned symlinks or empty directories found.[/green]")
134
+ return
135
+
136
+ tree = Tree("Cleanup Summary")
137
+
138
+ if orphaned_symlinks:
139
+ orphaned_tree = tree.add(f"Orphaned Symlinks ({total_orphaned})")
140
+ for pkg_name, paths in orphaned_symlinks.items():
141
+ pkg_tree = orphaned_tree.add(f"[cyan]{pkg_name}[/cyan]")
142
+ for path in paths:
143
+ pkg_tree.add(f"[red]{path}[/red]")
144
+
145
+ if empty_dirs:
146
+ empty_tree = tree.add(f"Empty Directories ({total_empty})")
147
+ for pkg_name, dirs in empty_dirs.items():
148
+ pkg_tree = empty_tree.add(f"[cyan]{pkg_name}[/cyan]")
149
+ for dir_path in dirs:
150
+ pkg_tree.add(f"[yellow]{dir_path}[/yellow]")
151
+
152
+ console.print(tree)
153
+
154
+ if dry_run:
155
+ console.print("[cyan]Dry run mode - no changes will be made[/cyan]")
156
+
157
+ if not dry_run and not typer.confirm("Proceed with cleanup?"):
158
+ console.print("[yellow]Cleanup cancelled.[/yellow]")
159
+ raise typer.Exit(0)
160
+
161
+ removed_orphaned = 0
162
+ removed_empty = 0
163
+
164
+ for pkg_name, paths in orphaned_symlinks.items():
165
+ for path in paths:
166
+ try:
167
+ if dry_run:
168
+ console.print(f" [cyan]Would remove:[/cyan] {path}")
169
+ else:
170
+ path.unlink()
171
+ removed_orphaned += 1
172
+ console.print(f" [green]Removed:[/green] {path}")
173
+ except OSError as e:
174
+ console.print(f" [red]Failed to remove {path}: {e}[/red]")
175
+
176
+ for pkg_name, dirs in empty_dirs.items():
177
+ for dir_path in sorted(dirs, reverse=True):
178
+ try:
179
+ if dry_run:
180
+ console.print(f" [cyan]Would remove:[/cyan] {dir_path}")
181
+ else:
182
+ dir_path.rmdir()
183
+ removed_empty += 1
184
+ console.print(f" [green]Removed:[/green] {dir_path}")
185
+ except OSError as e:
186
+ console.print(f" [yellow]Could not remove {dir_path}: {e}[/yellow]")
187
+
188
+ if dry_run:
189
+ console.print("\n[cyan]Dry run complete - no changes were made[/cyan]")
190
+ else:
191
+ console.print("\n[green]Clean complete![/green]")
192
+ console.print(
193
+ f"[dim]Removed {removed_orphaned} orphaned symlinks"
194
+ f" and {removed_empty} empty directories.[/dim]"
195
+ )
@@ -7,6 +7,7 @@ from typing import Annotated
7
7
  import typer
8
8
  from rich.console import Console
9
9
 
10
+ from dotman import __version__
10
11
  from dotman.core.config import Config, get_repo_manager
11
12
 
12
13
  console = Console()
@@ -54,4 +55,14 @@ def get_repository_option() -> Annotated[
54
55
  return None
55
56
 
56
57
 
58
+ @app.callback(invoke_without_command=True)
59
+ def version_callback(
60
+ ctx: typer.Context,
61
+ version: bool = typer.Option(False, "--version", help="Show version"),
62
+ ) -> None:
63
+ if version:
64
+ console.print(__version__)
65
+ raise typer.Exit(0)
66
+
67
+
57
68
  app.add_typer(repo_app)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dotman-cli
3
- Version: 0.2.7
3
+ Version: 0.2.9
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
@@ -7,6 +7,7 @@ src/dotman/main.py
7
7
  src/dotman/cli/__init__.py
8
8
  src/dotman/cli/commands/__init__.py
9
9
  src/dotman/cli/commands/absorb.py
10
+ src/dotman/cli/commands/clean.py
10
11
  src/dotman/cli/commands/clone.py
11
12
  src/dotman/cli/commands/deploy.py
12
13
  src/dotman/cli/commands/history.py
@@ -1,3 +0,0 @@
1
- """dotman - A dotfile manager using symlinks and templates."""
2
-
3
- __version__ = "0.1.0"
File without changes
File without changes
File without changes