dotman-git 1.0.0__py3-none-any.whl
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.
- dot_man/__init__.py +4 -0
- dot_man/backups.py +211 -0
- dot_man/branch_ops.py +347 -0
- dot_man/cli/__init__.py +113 -0
- dot_man/cli/add_cmd.py +167 -0
- dot_man/cli/audit_cmd.py +141 -0
- dot_man/cli/backup_cmd.py +105 -0
- dot_man/cli/branch_cmd.py +103 -0
- dot_man/cli/clean_cmd.py +97 -0
- dot_man/cli/common.py +548 -0
- dot_man/cli/completions_cmd.py +127 -0
- dot_man/cli/config_cmd.py +979 -0
- dot_man/cli/deploy_cmd.py +169 -0
- dot_man/cli/discover_cmd.py +105 -0
- dot_man/cli/doctor_cmd.py +229 -0
- dot_man/cli/edit_cmd.py +177 -0
- dot_man/cli/encrypt_cmd.py +205 -0
- dot_man/cli/export_cmd.py +146 -0
- dot_man/cli/import_cmd.py +315 -0
- dot_man/cli/init_cmd.py +532 -0
- dot_man/cli/interface.py +56 -0
- dot_man/cli/log_cmd.py +339 -0
- dot_man/cli/main.py +36 -0
- dot_man/cli/navigate_cmd.py +903 -0
- dot_man/cli/onboarding.py +546 -0
- dot_man/cli/profile_cmd.py +313 -0
- dot_man/cli/remote_cmd.py +454 -0
- dot_man/cli/restore_cmd.py +82 -0
- dot_man/cli/revert_cmd.py +86 -0
- dot_man/cli/show_cmd.py +29 -0
- dot_man/cli/status_cmd.py +185 -0
- dot_man/cli/switch_cmd.py +387 -0
- dot_man/cli/tag_cmd.py +164 -0
- dot_man/cli/template_cmd.py +244 -0
- dot_man/cli/tui_cmd.py +44 -0
- dot_man/cli/verify_cmd.py +156 -0
- dot_man/completions/_dot-man.zsh +28 -0
- dot_man/completions/dot-man.bash +15 -0
- dot_man/completions/dot-man.fish +58 -0
- dot_man/completions/install.sh +26 -0
- dot_man/config.py +23 -0
- dot_man/config_detector.py +426 -0
- dot_man/constants.py +109 -0
- dot_man/core.py +614 -0
- dot_man/dotman_config.py +516 -0
- dot_man/encryption.py +173 -0
- dot_man/exceptions.py +255 -0
- dot_man/files.py +443 -0
- dot_man/global_config.py +305 -0
- dot_man/hooks.py +232 -0
- dot_man/interactive.py +460 -0
- dot_man/lock.py +64 -0
- dot_man/merge.py +440 -0
- dot_man/operations.py +212 -0
- dot_man/py.typed +1 -0
- dot_man/save_deploy_ops.py +466 -0
- dot_man/secrets.py +473 -0
- dot_man/section.py +207 -0
- dot_man/status_ops.py +229 -0
- dot_man/tui_log.py +91 -0
- dot_man/ui.py +127 -0
- dot_man/utils.py +132 -0
- dot_man/vault.py +317 -0
- dotman_git-1.0.0.dist-info/METADATA +678 -0
- dotman_git-1.0.0.dist-info/RECORD +69 -0
- dotman_git-1.0.0.dist-info/WHEEL +5 -0
- dotman_git-1.0.0.dist-info/entry_points.txt +3 -0
- dotman_git-1.0.0.dist-info/licenses/LICENSE +21 -0
- dotman_git-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Deploy command for dot-man CLI."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
|
|
8
|
+
from .. import ui
|
|
9
|
+
from ..exceptions import DotManError
|
|
10
|
+
from .common import (
|
|
11
|
+
complete_branches,
|
|
12
|
+
error,
|
|
13
|
+
handle_exception,
|
|
14
|
+
require_init,
|
|
15
|
+
success,
|
|
16
|
+
warn,
|
|
17
|
+
)
|
|
18
|
+
from .interface import cli as main
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@main.command()
|
|
22
|
+
@click.argument("branch", shell_complete=complete_branches)
|
|
23
|
+
@click.option("--force", is_flag=True, help="Skip confirmation prompt")
|
|
24
|
+
@click.option("--dry-run", is_flag=True, help="Show what would be deployed")
|
|
25
|
+
@require_init
|
|
26
|
+
def deploy(branch: str, force: bool, dry_run: bool):
|
|
27
|
+
"""One-way deployment of a branch configuration.
|
|
28
|
+
|
|
29
|
+
Deploys files from the specified branch to your home directory.
|
|
30
|
+
Unlike 'switch', this does NOT save current local changes first.
|
|
31
|
+
Typically used for setting up a new machine.
|
|
32
|
+
|
|
33
|
+
Example: dot-man deploy main
|
|
34
|
+
"""
|
|
35
|
+
try:
|
|
36
|
+
from ..operations import get_operations
|
|
37
|
+
|
|
38
|
+
ops = get_operations()
|
|
39
|
+
git = ops.git
|
|
40
|
+
|
|
41
|
+
# Check branch exists
|
|
42
|
+
if not git.branch_exists(branch):
|
|
43
|
+
available = ", ".join(git.list_branches())
|
|
44
|
+
error(f"Branch '{branch}' not found. Available: {available}")
|
|
45
|
+
|
|
46
|
+
if not force and not dry_run:
|
|
47
|
+
ui.console.print(
|
|
48
|
+
Panel(
|
|
49
|
+
"[yellow]WARNING: Deploy will OVERWRITE local files![/yellow]\n\n"
|
|
50
|
+
"This will:\n"
|
|
51
|
+
f"• Deploy '{branch}' configuration\n"
|
|
52
|
+
"• Overwrite existing dotfiles\n"
|
|
53
|
+
"• Local changes will be LOST\n\n"
|
|
54
|
+
"[dim]Typical use: Setting up a new machine[/dim]",
|
|
55
|
+
title="⚠️ Destructive Operation",
|
|
56
|
+
border_style="yellow",
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if not ui.confirm("Continue?"):
|
|
61
|
+
ui.info("Aborted.")
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
# Checkout branch
|
|
65
|
+
if not dry_run:
|
|
66
|
+
git.checkout(branch)
|
|
67
|
+
ops.reload_config()
|
|
68
|
+
|
|
69
|
+
# Get sections
|
|
70
|
+
section_names = ops.get_sections()
|
|
71
|
+
if not section_names:
|
|
72
|
+
warn("No files configured in this branch")
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
sections = [ops.get_section(name) for name in section_names]
|
|
76
|
+
|
|
77
|
+
# Phase 1: Scan
|
|
78
|
+
if not dry_run:
|
|
79
|
+
ui.console.print("Scanning for changes...")
|
|
80
|
+
|
|
81
|
+
plan = ops.scan_deployable_changes(sections)
|
|
82
|
+
|
|
83
|
+
sections_to_process = plan["sections_to_deploy"]
|
|
84
|
+
pre_hooks = list(dict.fromkeys(plan["pre_hooks"]))
|
|
85
|
+
post_hooks = list(dict.fromkeys(plan["post_hooks"]))
|
|
86
|
+
scan_errors = plan["errors"]
|
|
87
|
+
|
|
88
|
+
for err in scan_errors:
|
|
89
|
+
warn(err)
|
|
90
|
+
|
|
91
|
+
if not sections_to_process:
|
|
92
|
+
ui.console.print("[yellow]No changes detected.[/yellow]")
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
# Display Plan / Dry Run
|
|
96
|
+
if dry_run:
|
|
97
|
+
ui.console.print(
|
|
98
|
+
f"\n[bold]Dry Run Summary - {len(sections_to_process)} files to deploy:[/bold]"
|
|
99
|
+
)
|
|
100
|
+
for section, local_path, repo_path in sections_to_process:
|
|
101
|
+
action = "OVERWRITE" if local_path.exists() else "CREATE"
|
|
102
|
+
ui.console.print(f" {action}: {local_path}")
|
|
103
|
+
|
|
104
|
+
if pre_hooks:
|
|
105
|
+
ui.console.print("\n[bold]Pre-Hooks:[/bold]")
|
|
106
|
+
for cmd in pre_hooks:
|
|
107
|
+
ui.console.print(f" [dim]{cmd}[/dim]")
|
|
108
|
+
|
|
109
|
+
if post_hooks:
|
|
110
|
+
ui.console.print("\n[bold]Post-Hooks:[/bold]")
|
|
111
|
+
for cmd in post_hooks:
|
|
112
|
+
ui.console.print(f" [dim]{cmd}[/dim]")
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
# Confirm
|
|
116
|
+
ui.console.print(f"Found {len(sections_to_process)} files to deploy.")
|
|
117
|
+
if not force:
|
|
118
|
+
if not ui.confirm(f"Deploy {len(sections_to_process)} files?"):
|
|
119
|
+
ui.info("Aborted.")
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
# Execute Pre-Hooks
|
|
123
|
+
if pre_hooks:
|
|
124
|
+
ui.console.print("\n[bold]Running pre-deploy hooks...[/bold]")
|
|
125
|
+
for cmd in pre_hooks:
|
|
126
|
+
ui.console.print(f" Exec: [cyan]{cmd}[/cyan]")
|
|
127
|
+
try:
|
|
128
|
+
subprocess.run(cmd, shell=True, check=False)
|
|
129
|
+
except Exception as e:
|
|
130
|
+
warn(f"Failed to run command '{cmd}': {e}")
|
|
131
|
+
|
|
132
|
+
# Phase 2: Execute Deployment (Parallel)
|
|
133
|
+
ui.console.print("\n[bold]Deploying files...[/bold]")
|
|
134
|
+
result = ops.execute_deployment_plan(plan)
|
|
135
|
+
|
|
136
|
+
deployed = result["deployed"]
|
|
137
|
+
exec_errors = result["errors"]
|
|
138
|
+
|
|
139
|
+
if exec_errors:
|
|
140
|
+
for err in exec_errors:
|
|
141
|
+
ui.console.print(f" [red]Error:[/red] {err}")
|
|
142
|
+
import logging
|
|
143
|
+
|
|
144
|
+
logging.error(f"Deployment error: {err}")
|
|
145
|
+
|
|
146
|
+
ui.console.print(f"\nDeployed: {deployed}/{len(sections_to_process)} files.")
|
|
147
|
+
|
|
148
|
+
# Execute Post-Hooks
|
|
149
|
+
if post_hooks:
|
|
150
|
+
ui.console.print("\n[bold]Running post-deploy hooks...[/bold]")
|
|
151
|
+
for cmd in post_hooks:
|
|
152
|
+
ui.console.print(f" Exec: [cyan]{cmd}[/cyan]")
|
|
153
|
+
try:
|
|
154
|
+
subprocess.run(cmd, shell=True, check=False)
|
|
155
|
+
except Exception as e:
|
|
156
|
+
warn(f"Failed to run command '{cmd}': {e}")
|
|
157
|
+
|
|
158
|
+
# Update global config
|
|
159
|
+
ops.global_config.current_branch = branch
|
|
160
|
+
ops.global_config.save()
|
|
161
|
+
|
|
162
|
+
success(f"Deployment complete! ({deployed} files)")
|
|
163
|
+
|
|
164
|
+
except DotManError as e:
|
|
165
|
+
error(str(e), e.exit_code)
|
|
166
|
+
except KeyboardInterrupt:
|
|
167
|
+
handle_exception(KeyboardInterrupt())
|
|
168
|
+
except Exception as e:
|
|
169
|
+
handle_exception(e, "Deployment")
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Discover command for auto-detecting existing dotfiles."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from .. import ui
|
|
6
|
+
from ..config_detector import ConfigDetector
|
|
7
|
+
from .common import require_init
|
|
8
|
+
from .interface import cli as main
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@main.command("discover")
|
|
12
|
+
@click.option(
|
|
13
|
+
"--include-extended/--no-extended",
|
|
14
|
+
default=True,
|
|
15
|
+
help="Include extended configs (VS Code, Sublime, etc.)",
|
|
16
|
+
)
|
|
17
|
+
@click.option(
|
|
18
|
+
"--add",
|
|
19
|
+
is_flag=True,
|
|
20
|
+
help="Automatically add detected configs to dot-man.toml",
|
|
21
|
+
)
|
|
22
|
+
@require_init
|
|
23
|
+
def discover_cmd(include_extended: bool, add: bool):
|
|
24
|
+
"""Auto-detect existing dotfiles on your system.
|
|
25
|
+
|
|
26
|
+
Scans common locations for popular configuration files and directories,
|
|
27
|
+
then offers to add them to dot-man for tracking.
|
|
28
|
+
|
|
29
|
+
Examples:
|
|
30
|
+
dot-man discover # Show all detected dotfiles
|
|
31
|
+
dot-man discover --no-extended # Only common configs
|
|
32
|
+
dot-man discover --add # Auto-add to config
|
|
33
|
+
"""
|
|
34
|
+
ui.console.print("[bold]🔍 Scanning for dotfiles...[/bold]")
|
|
35
|
+
ui.console.print()
|
|
36
|
+
|
|
37
|
+
detected = ConfigDetector.detect_popular_configs(include_extended=include_extended)
|
|
38
|
+
detected.extend(ConfigDetector.detect_quickshell_configs())
|
|
39
|
+
|
|
40
|
+
if not detected:
|
|
41
|
+
ui.console.print("[yellow]No dotfiles detected on this system.[/yellow]")
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
ui.console.print(f"[bold]Found {len(detected)} configurations:[/bold]")
|
|
45
|
+
ui.console.print()
|
|
46
|
+
|
|
47
|
+
for config in detected:
|
|
48
|
+
exists = "✓" if config["paths"] else "?"
|
|
49
|
+
hook = f" ({config['default_hook']})" if config["default_hook"] else ""
|
|
50
|
+
ui.console.print(f" [{exists}] {config['display_name']}")
|
|
51
|
+
ui.console.print(f" [{config['section_name']}]{hook}")
|
|
52
|
+
for path in config["paths"]:
|
|
53
|
+
ui.console.print(f" {path}")
|
|
54
|
+
|
|
55
|
+
if add:
|
|
56
|
+
_add_detected_configs(detected)
|
|
57
|
+
else:
|
|
58
|
+
ui.console.print()
|
|
59
|
+
ui.console.print(
|
|
60
|
+
"[dim]Tip: Use --add to automatically add these to dot-man.toml[/dim]"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _add_detected_configs(detected: list):
|
|
65
|
+
"""Add detected configs to dot-man.toml."""
|
|
66
|
+
from ..dotman_config import DotManConfig
|
|
67
|
+
from ..operations import get_operations
|
|
68
|
+
|
|
69
|
+
config = DotManConfig()
|
|
70
|
+
ops = get_operations()
|
|
71
|
+
added_count = 0
|
|
72
|
+
|
|
73
|
+
for conf in detected:
|
|
74
|
+
section_name = conf["section_name"]
|
|
75
|
+
|
|
76
|
+
if section_name in ops.get_sections():
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
config.add_section(
|
|
81
|
+
name=section_name,
|
|
82
|
+
paths=conf["paths"],
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if conf["default_hook"]:
|
|
86
|
+
config.update_section(
|
|
87
|
+
section_name,
|
|
88
|
+
post_deploy=conf["default_hook"],
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
added_count += 1
|
|
92
|
+
ui.console.print(f" [green]✓[/green] Added: {section_name}")
|
|
93
|
+
except Exception as e:
|
|
94
|
+
ui.console.print(f" [red]✗[/red] Failed to add {section_name}: {e}")
|
|
95
|
+
|
|
96
|
+
config.save()
|
|
97
|
+
|
|
98
|
+
if added_count > 0:
|
|
99
|
+
ui.console.print()
|
|
100
|
+
ui.success(f"Added {added_count} sections to dot-man.toml")
|
|
101
|
+
ui.console.print("[dim]Run 'dot-man status' to see your tracked files[/dim]")
|
|
102
|
+
else:
|
|
103
|
+
ui.console.print(
|
|
104
|
+
"[dim]No new sections to add (already tracked or duplicates)[/dim]"
|
|
105
|
+
)
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""Doctor command for dot-man CLI - diagnostics and health checks."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
|
|
6
|
+
from .. import ui
|
|
7
|
+
from ..constants import DOT_MAN_DIR, DOT_MAN_TOML, GLOBAL_TOML, REPO_DIR
|
|
8
|
+
from .common import error, require_init, success
|
|
9
|
+
from .interface import cli as main
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@main.command("doctor")
|
|
13
|
+
@require_init
|
|
14
|
+
def doctor():
|
|
15
|
+
"""Run diagnostics and health checks.
|
|
16
|
+
|
|
17
|
+
Checks the dot-man installation for common issues:
|
|
18
|
+
git availability, repository integrity, configuration validity,
|
|
19
|
+
file permissions, and remote connectivity.
|
|
20
|
+
"""
|
|
21
|
+
from ..operations import get_operations
|
|
22
|
+
|
|
23
|
+
checks_passed = 0
|
|
24
|
+
checks_failed = 0
|
|
25
|
+
checks_warned = 0
|
|
26
|
+
|
|
27
|
+
def check_pass(label: str, detail: str = "") -> None:
|
|
28
|
+
nonlocal checks_passed
|
|
29
|
+
checks_passed += 1
|
|
30
|
+
msg = f"[success]✓[/success] {label}"
|
|
31
|
+
if detail:
|
|
32
|
+
msg += f" [dim]({detail})[/dim]"
|
|
33
|
+
ui.console.print(msg)
|
|
34
|
+
|
|
35
|
+
def check_fail(label: str, detail: str = "") -> None:
|
|
36
|
+
nonlocal checks_failed
|
|
37
|
+
checks_failed += 1
|
|
38
|
+
msg = f"[error]✗[/error] {label}"
|
|
39
|
+
if detail:
|
|
40
|
+
msg += f" [dim]({detail})[/dim]"
|
|
41
|
+
ui.console.print(msg)
|
|
42
|
+
|
|
43
|
+
def check_warn(label: str, detail: str = "") -> None:
|
|
44
|
+
nonlocal checks_warned
|
|
45
|
+
checks_warned += 1
|
|
46
|
+
msg = f"[warning]⚠[/warning] {label}"
|
|
47
|
+
if detail:
|
|
48
|
+
msg += f" [dim]({detail})[/dim]"
|
|
49
|
+
ui.console.print(msg)
|
|
50
|
+
|
|
51
|
+
ui.console.print("[bold]dot-man doctor[/bold]")
|
|
52
|
+
ui.console.print()
|
|
53
|
+
|
|
54
|
+
# 1. Git availability
|
|
55
|
+
ui.console.print("[bold]System[/bold]")
|
|
56
|
+
git_path = shutil.which("git")
|
|
57
|
+
if git_path:
|
|
58
|
+
import subprocess
|
|
59
|
+
|
|
60
|
+
result = subprocess.run(
|
|
61
|
+
["git", "--version"], capture_output=True, text=True, timeout=5
|
|
62
|
+
)
|
|
63
|
+
version = result.stdout.strip() if result.returncode == 0 else "unknown"
|
|
64
|
+
check_pass("Git installed", version)
|
|
65
|
+
else:
|
|
66
|
+
check_fail("Git not found", "Install git and ensure it's in PATH")
|
|
67
|
+
|
|
68
|
+
# 2. Python version
|
|
69
|
+
import sys
|
|
70
|
+
|
|
71
|
+
py_version = (
|
|
72
|
+
f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
73
|
+
)
|
|
74
|
+
if sys.version_info >= (3, 9):
|
|
75
|
+
check_pass("Python version", py_version)
|
|
76
|
+
else:
|
|
77
|
+
check_fail("Python version too old", f"{py_version} (requires 3.9+)")
|
|
78
|
+
|
|
79
|
+
ui.console.print()
|
|
80
|
+
|
|
81
|
+
# 3. Repository checks
|
|
82
|
+
ui.console.print("[bold]Repository[/bold]")
|
|
83
|
+
if DOT_MAN_DIR.exists():
|
|
84
|
+
check_pass("Config directory exists", str(DOT_MAN_DIR))
|
|
85
|
+
else:
|
|
86
|
+
check_fail("Config directory missing", "Run 'dot-man init'")
|
|
87
|
+
|
|
88
|
+
if REPO_DIR.exists():
|
|
89
|
+
check_pass("Repository directory exists", str(REPO_DIR))
|
|
90
|
+
else:
|
|
91
|
+
check_fail("Repository directory missing", "Run 'dot-man init'")
|
|
92
|
+
|
|
93
|
+
# Check .git directory
|
|
94
|
+
git_dir = REPO_DIR / ".git"
|
|
95
|
+
if git_dir.exists():
|
|
96
|
+
check_pass("Git repository initialized")
|
|
97
|
+
else:
|
|
98
|
+
check_fail("No .git directory in repo", "Repository may be corrupted")
|
|
99
|
+
|
|
100
|
+
# Check repo permissions
|
|
101
|
+
if REPO_DIR.exists():
|
|
102
|
+
if os.access(REPO_DIR, os.R_OK | os.W_OK):
|
|
103
|
+
check_pass("Repository permissions", "read/write OK")
|
|
104
|
+
else:
|
|
105
|
+
check_fail("Repository permissions", "Cannot read/write to repo directory")
|
|
106
|
+
|
|
107
|
+
ui.console.print()
|
|
108
|
+
|
|
109
|
+
# 4. Configuration checks
|
|
110
|
+
ui.console.print("[bold]Configuration[/bold]")
|
|
111
|
+
|
|
112
|
+
if GLOBAL_TOML.exists():
|
|
113
|
+
check_pass("Global config exists", str(GLOBAL_TOML))
|
|
114
|
+
else:
|
|
115
|
+
check_warn("No global config", "Will use defaults")
|
|
116
|
+
|
|
117
|
+
if DOT_MAN_TOML.exists():
|
|
118
|
+
check_pass("dot-man.toml exists", str(DOT_MAN_TOML))
|
|
119
|
+
# Try to parse it
|
|
120
|
+
try:
|
|
121
|
+
ops = get_operations()
|
|
122
|
+
sections = ops.get_sections()
|
|
123
|
+
check_pass("Config is valid", f"{len(sections)} section(s)")
|
|
124
|
+
except Exception as e:
|
|
125
|
+
check_fail("Config parse error", str(e))
|
|
126
|
+
else:
|
|
127
|
+
check_warn("No dot-man.toml", "Run 'dot-man config create' or 'dot-man edit'")
|
|
128
|
+
|
|
129
|
+
ui.console.print()
|
|
130
|
+
|
|
131
|
+
# 5. Branch checks
|
|
132
|
+
ui.console.print("[bold]Branches[/bold]")
|
|
133
|
+
try:
|
|
134
|
+
ops = get_operations()
|
|
135
|
+
current = ops.current_branch
|
|
136
|
+
check_pass("Current branch", current)
|
|
137
|
+
|
|
138
|
+
branches = ops.git.list_branches()
|
|
139
|
+
check_pass("Available branches", ", ".join(branches) if branches else "none")
|
|
140
|
+
except Exception as e:
|
|
141
|
+
check_fail("Branch check failed", str(e))
|
|
142
|
+
|
|
143
|
+
ui.console.print()
|
|
144
|
+
|
|
145
|
+
# 6. Remote checks
|
|
146
|
+
ui.console.print("[bold]Remote[/bold]")
|
|
147
|
+
try:
|
|
148
|
+
ops = get_operations()
|
|
149
|
+
remote_url = ops.global_config.remote_url
|
|
150
|
+
if remote_url:
|
|
151
|
+
check_pass("Remote configured", remote_url)
|
|
152
|
+
else:
|
|
153
|
+
check_warn("No remote configured", "Run 'dot-man setup' to configure")
|
|
154
|
+
except Exception as e:
|
|
155
|
+
check_warn("Could not check remote", str(e))
|
|
156
|
+
|
|
157
|
+
ui.console.print()
|
|
158
|
+
|
|
159
|
+
# 7. Tracked files check
|
|
160
|
+
ui.console.print("[bold]Tracked Files[/bold]")
|
|
161
|
+
try:
|
|
162
|
+
ops = get_operations()
|
|
163
|
+
missing_count = 0
|
|
164
|
+
total_paths = 0
|
|
165
|
+
for section_name in ops.get_sections():
|
|
166
|
+
section = ops.get_section(section_name)
|
|
167
|
+
for p in section.paths:
|
|
168
|
+
total_paths += 1
|
|
169
|
+
if not p.exists():
|
|
170
|
+
missing_count += 1
|
|
171
|
+
check_warn(f"Missing: {p}", f"section '{section_name}'")
|
|
172
|
+
|
|
173
|
+
if missing_count == 0 and total_paths > 0:
|
|
174
|
+
check_pass("All tracked paths exist", f"{total_paths} path(s)")
|
|
175
|
+
elif total_paths == 0:
|
|
176
|
+
check_warn("No paths tracked", "Add files with 'dot-man add'")
|
|
177
|
+
except Exception as e:
|
|
178
|
+
check_fail("File check failed", str(e))
|
|
179
|
+
|
|
180
|
+
# 8. Orphaned files
|
|
181
|
+
try:
|
|
182
|
+
ops = get_operations()
|
|
183
|
+
orphans = ops.get_orphaned_files()
|
|
184
|
+
if orphans:
|
|
185
|
+
check_warn(
|
|
186
|
+
f"{len(orphans)} orphaned file(s) in repo",
|
|
187
|
+
"Run 'dot-man clean --orphans' to remove",
|
|
188
|
+
)
|
|
189
|
+
else:
|
|
190
|
+
check_pass("No orphaned files")
|
|
191
|
+
except Exception as e:
|
|
192
|
+
check_warn("Could not check orphans", str(e))
|
|
193
|
+
|
|
194
|
+
# 9. Backup check
|
|
195
|
+
try:
|
|
196
|
+
ops = get_operations()
|
|
197
|
+
backup_list = ops.backups.list_backups()
|
|
198
|
+
if backup_list:
|
|
199
|
+
check_pass("Backups available", f"{len(backup_list)} backup(s)")
|
|
200
|
+
else:
|
|
201
|
+
check_warn(
|
|
202
|
+
"No backups",
|
|
203
|
+
"Backups are created automatically before destructive operations",
|
|
204
|
+
)
|
|
205
|
+
except Exception as e:
|
|
206
|
+
check_warn("Could not check backups", str(e))
|
|
207
|
+
|
|
208
|
+
ui.console.print()
|
|
209
|
+
|
|
210
|
+
# Summary
|
|
211
|
+
ui.console.print("[bold]Summary[/bold]")
|
|
212
|
+
total = checks_passed + checks_failed + checks_warned
|
|
213
|
+
ui.console.print(
|
|
214
|
+
f" [success]{checks_passed}[/success] passed, "
|
|
215
|
+
f"[warning]{checks_warned}[/warning] warnings, "
|
|
216
|
+
f"[error]{checks_failed}[/error] failed "
|
|
217
|
+
f"[dim]({total} checks)[/dim]"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
if checks_failed == 0:
|
|
221
|
+
ui.console.print()
|
|
222
|
+
success("dot-man is healthy!")
|
|
223
|
+
else:
|
|
224
|
+
ui.console.print()
|
|
225
|
+
error(
|
|
226
|
+
f"{checks_failed} check(s) failed. See above for details.",
|
|
227
|
+
exit_code=0,
|
|
228
|
+
)
|
|
229
|
+
raise SystemExit(1)
|
dot_man/cli/edit_cmd.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Edit command for dot-man CLI."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
import questionary
|
|
7
|
+
|
|
8
|
+
from .. import ui
|
|
9
|
+
from ..config import GlobalConfig
|
|
10
|
+
from ..constants import DOT_MAN_TOML, GLOBAL_TOML, REPO_DIR
|
|
11
|
+
from ..exceptions import DotManError
|
|
12
|
+
from .common import error, require_init, success, warn
|
|
13
|
+
from .interface import cli as main
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@main.command()
|
|
17
|
+
@click.option("--editor", help="Editor to use (default: config or $VISUAL or $EDITOR)")
|
|
18
|
+
@click.option("--global", "edit_global", is_flag=True, help="Edit global configuration")
|
|
19
|
+
@click.option(
|
|
20
|
+
"--raw", is_flag=True, help="Use raw text editor instead of interactive TUI"
|
|
21
|
+
)
|
|
22
|
+
@require_init
|
|
23
|
+
def edit(editor: str | None, edit_global: bool, raw: bool):
|
|
24
|
+
"""Open the configuration file in your text editor.
|
|
25
|
+
|
|
26
|
+
By default, opens the dot-man.toml file for the current branch.
|
|
27
|
+
Use --global to edit the global configuration.
|
|
28
|
+
"""
|
|
29
|
+
try:
|
|
30
|
+
from ..interactive import (
|
|
31
|
+
custom_style,
|
|
32
|
+
run_global_wizard,
|
|
33
|
+
run_section_wizard,
|
|
34
|
+
run_templates_wizard,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Determine target file path
|
|
38
|
+
if edit_global:
|
|
39
|
+
target = GLOBAL_TOML
|
|
40
|
+
desc = "global configuration"
|
|
41
|
+
else:
|
|
42
|
+
target = REPO_DIR / DOT_MAN_TOML
|
|
43
|
+
desc = "dot-man.toml"
|
|
44
|
+
|
|
45
|
+
# If --raw flag, skip interactive mode
|
|
46
|
+
if raw:
|
|
47
|
+
if not target.exists():
|
|
48
|
+
error(f"Configuration file not found: {target}")
|
|
49
|
+
_open_raw_editor(target, desc, editor)
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
# Interactive Mode
|
|
53
|
+
from ..operations import get_operations
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
ops = get_operations()
|
|
57
|
+
|
|
58
|
+
while True:
|
|
59
|
+
ops.reload_config()
|
|
60
|
+
sections = ops.get_sections()
|
|
61
|
+
|
|
62
|
+
choices = []
|
|
63
|
+
choices.append(
|
|
64
|
+
questionary.Choice("⚙️ Global Configuration", value="global")
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if sections:
|
|
68
|
+
choices.append(questionary.Separator("--- Sections ---"))
|
|
69
|
+
for name in sections:
|
|
70
|
+
choices.append(
|
|
71
|
+
questionary.Choice(f"📄 {name}", value=f"section:{name}")
|
|
72
|
+
)
|
|
73
|
+
else:
|
|
74
|
+
choices.append(questionary.Separator("--- No Sections ---"))
|
|
75
|
+
|
|
76
|
+
choices.append(questionary.Separator("--- Actions ---"))
|
|
77
|
+
choices.append(
|
|
78
|
+
questionary.Choice("➕ Add New Section", value="add_new")
|
|
79
|
+
)
|
|
80
|
+
choices.append(
|
|
81
|
+
questionary.Choice("📝 Edit Templates", value="templates")
|
|
82
|
+
)
|
|
83
|
+
choices.append(
|
|
84
|
+
questionary.Choice("📝 Open Raw File (Advanced)", value="raw")
|
|
85
|
+
)
|
|
86
|
+
choices.append(
|
|
87
|
+
questionary.Choice("🚪 Quit", value="quit", shortcut_key="q")
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
selection = questionary.select(
|
|
91
|
+
"What would you like to configure?",
|
|
92
|
+
choices=choices,
|
|
93
|
+
use_shortcuts=True,
|
|
94
|
+
style=custom_style,
|
|
95
|
+
).ask()
|
|
96
|
+
|
|
97
|
+
if not selection or selection == "quit":
|
|
98
|
+
break
|
|
99
|
+
|
|
100
|
+
if selection == "global":
|
|
101
|
+
run_global_wizard(ops.global_config)
|
|
102
|
+
|
|
103
|
+
elif selection == "raw":
|
|
104
|
+
_open_raw_editor(target, desc, editor)
|
|
105
|
+
break
|
|
106
|
+
|
|
107
|
+
elif selection == "add_new":
|
|
108
|
+
path_str = questionary.path(
|
|
109
|
+
"Path to file or directory:", style=custom_style
|
|
110
|
+
).ask()
|
|
111
|
+
if path_str:
|
|
112
|
+
try:
|
|
113
|
+
path = Path(path_str).expanduser()
|
|
114
|
+
if not path.exists():
|
|
115
|
+
warn(f"Path does not exist: {path}")
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
section_name = questionary.text(
|
|
119
|
+
"Section Name:", default=path.stem, style=custom_style
|
|
120
|
+
).ask()
|
|
121
|
+
if section_name:
|
|
122
|
+
from .add_cmd import add
|
|
123
|
+
|
|
124
|
+
ctx = click.get_current_context()
|
|
125
|
+
ctx.invoke(
|
|
126
|
+
add,
|
|
127
|
+
path=str(path),
|
|
128
|
+
section=section_name,
|
|
129
|
+
repo_base=None,
|
|
130
|
+
exclude=(),
|
|
131
|
+
include=(),
|
|
132
|
+
inherits=(),
|
|
133
|
+
post_deploy=None,
|
|
134
|
+
pre_deploy=None,
|
|
135
|
+
)
|
|
136
|
+
ui.console.print()
|
|
137
|
+
ui.console.print("Press Enter to continue...")
|
|
138
|
+
input()
|
|
139
|
+
except Exception as e:
|
|
140
|
+
warn(f"Error adding section: {e}")
|
|
141
|
+
|
|
142
|
+
elif selection == "templates":
|
|
143
|
+
run_templates_wizard(ops.dotman_config)
|
|
144
|
+
|
|
145
|
+
elif selection.startswith("section:"):
|
|
146
|
+
section_name = selection.split(":", 1)[1]
|
|
147
|
+
run_section_wizard(ops.dotman_config, section_name)
|
|
148
|
+
|
|
149
|
+
except KeyboardInterrupt:
|
|
150
|
+
return
|
|
151
|
+
except Exception as e:
|
|
152
|
+
warn(f"Interactive menu error: {e}")
|
|
153
|
+
ui.console.print("Falling back to raw editor...")
|
|
154
|
+
_open_raw_editor(target, desc, editor)
|
|
155
|
+
|
|
156
|
+
except DotManError as e:
|
|
157
|
+
error(str(e), e.exit_code)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _open_raw_editor(target: Path, desc: str, editor: str | None = None):
|
|
161
|
+
"""Helper to open raw editor."""
|
|
162
|
+
from ..utils import get_editor, open_in_editor
|
|
163
|
+
|
|
164
|
+
global_config = GlobalConfig()
|
|
165
|
+
try:
|
|
166
|
+
global_config.load()
|
|
167
|
+
config_editor = global_config.editor
|
|
168
|
+
except (FileNotFoundError, DotManError):
|
|
169
|
+
config_editor = None
|
|
170
|
+
|
|
171
|
+
editor_cmd = editor or config_editor or get_editor()
|
|
172
|
+
ui.console.print(f"Opening {desc} in [cyan]{editor_cmd}[/cyan]...")
|
|
173
|
+
|
|
174
|
+
if not open_in_editor(target, editor_cmd):
|
|
175
|
+
error(f"Editor '{editor_cmd}' exited with error")
|
|
176
|
+
|
|
177
|
+
success(f"Edited {desc}")
|