dotx 3.3.1__tar.gz → 3.3.3__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. {dotx-3.3.1/src/dotx.egg-info → dotx-3.3.3}/PKG-INFO +1 -1
  2. {dotx-3.3.1 → dotx-3.3.3}/pyproject.toml +1 -1
  3. {dotx-3.3.1 → dotx-3.3.3}/src/dotx/cli.py +1 -1
  4. {dotx-3.3.1 → dotx-3.3.3}/src/dotx/commands/database.py +17 -4
  5. {dotx-3.3.1 → dotx-3.3.3}/src/dotx/commands/install_cmd.py +11 -4
  6. {dotx-3.3.1 → dotx-3.3.3}/src/dotx/options.py +8 -7
  7. {dotx-3.3.1 → dotx-3.3.3/src/dotx.egg-info}/PKG-INFO +1 -1
  8. {dotx-3.3.1 → dotx-3.3.3}/tests/test_cli_database.py +42 -1
  9. {dotx-3.3.1 → dotx-3.3.3}/LICENSE +0 -0
  10. {dotx-3.3.1 → dotx-3.3.3}/MANIFEST.in +0 -0
  11. {dotx-3.3.1 → dotx-3.3.3}/README.md +0 -0
  12. {dotx-3.3.1 → dotx-3.3.3}/setup.cfg +0 -0
  13. {dotx-3.3.1 → dotx-3.3.3}/src/dotx/__init__.py +0 -0
  14. {dotx-3.3.1 → dotx-3.3.3}/src/dotx/always-create +0 -0
  15. {dotx-3.3.1 → dotx-3.3.3}/src/dotx/commands/__init__.py +0 -0
  16. {dotx-3.3.1 → dotx-3.3.3}/src/dotx/commands/path_cmd.py +0 -0
  17. {dotx-3.3.1 → dotx-3.3.3}/src/dotx/commands/progress.py +0 -0
  18. {dotx-3.3.1 → dotx-3.3.3}/src/dotx/commands/uninstall_cmd.py +0 -0
  19. {dotx-3.3.1 → dotx-3.3.3}/src/dotx/database.py +0 -0
  20. {dotx-3.3.1 → dotx-3.3.3}/src/dotx/dotxignore +0 -0
  21. {dotx-3.3.1 → dotx-3.3.3}/src/dotx/hierarchy.py +0 -0
  22. {dotx-3.3.1 → dotx-3.3.3}/src/dotx/ignore.py +0 -0
  23. {dotx-3.3.1 → dotx-3.3.3}/src/dotx/install.py +0 -0
  24. {dotx-3.3.1 → dotx-3.3.3}/src/dotx/installed-schema.sql +0 -0
  25. {dotx-3.3.1 → dotx-3.3.3}/src/dotx/plan.py +0 -0
  26. {dotx-3.3.1 → dotx-3.3.3}/src/dotx/uninstall.py +0 -0
  27. {dotx-3.3.1 → dotx-3.3.3}/src/dotx.egg-info/SOURCES.txt +0 -0
  28. {dotx-3.3.1 → dotx-3.3.3}/src/dotx.egg-info/dependency_links.txt +0 -0
  29. {dotx-3.3.1 → dotx-3.3.3}/src/dotx.egg-info/entry_points.txt +0 -0
  30. {dotx-3.3.1 → dotx-3.3.3}/src/dotx.egg-info/requires.txt +0 -0
  31. {dotx-3.3.1 → dotx-3.3.3}/src/dotx.egg-info/top_level.txt +0 -0
  32. {dotx-3.3.1 → dotx-3.3.3}/tests/test_always_create.py +0 -0
  33. {dotx-3.3.1 → dotx-3.3.3}/tests/test_cli.py +0 -0
  34. {dotx-3.3.1 → dotx-3.3.3}/tests/test_ignore.py +0 -0
  35. {dotx-3.3.1 → dotx-3.3.3}/tests/test_ignore_rules.py +0 -0
  36. {dotx-3.3.1 → dotx-3.3.3}/tests/test_install.py +0 -0
  37. {dotx-3.3.1 → dotx-3.3.3}/tests/test_options.py +0 -0
  38. {dotx-3.3.1 → dotx-3.3.3}/tests/test_path_which.py +0 -0
  39. {dotx-3.3.1 → dotx-3.3.3}/tests/test_plan.py +0 -0
  40. {dotx-3.3.1 → dotx-3.3.3}/tests/test_uninstall.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dotx
3
- Version: 3.3.1
3
+ Version: 3.3.3
4
4
  Summary: A command-line tool to install a link-farm to your dotfiles
5
5
  Author-email: Wolf <Wolf@zv.cx>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dotx"
3
- version = "3.3.1"
3
+ version = "3.3.3"
4
4
  description = "A command-line tool to install a link-farm to your dotfiles"
5
5
  authors = [
6
6
  { name = "Wolf", email = "Wolf@zv.cx" }
@@ -52,7 +52,7 @@ def configure_logging(debug: bool, verbose: bool, log: Path | None):
52
52
 
53
53
  @app.callback()
54
54
  def main(
55
- ctx: click.Context,
55
+ ctx: typer.Context,
56
56
  debug: Annotated[
57
57
  bool,
58
58
  typer.Option("--debug/--no-debug", help="Enable debug logging"),
@@ -85,7 +85,7 @@ def register_commands(app: typer.Typer):
85
85
  packages = db.get_all_packages()
86
86
 
87
87
  if not packages:
88
- console.print("[yellow]No packages installed.[/yellow]")
88
+ console.print("[yellow]No packages in database. Run `dotx sync --package-root <dir>` to rebuild from existing symlinks.[/yellow]")
89
89
  return
90
90
 
91
91
  if as_commands:
@@ -100,17 +100,30 @@ def register_commands(app: typer.Typer):
100
100
  table.add_column("Package", style="cyan", no_wrap=True)
101
101
  table.add_column("Files", justify="right", style="magenta")
102
102
  table.add_column("Last Install", style="green")
103
+ table.add_column("Status", style="green")
103
104
 
105
+ missing_count = 0
104
106
  for pkg in packages:
105
107
  # Show full package path (package_root / package_name) as documented
106
- package_path = str(Path(pkg["package_root"]) / pkg["package_name"])
108
+ package_path = Path(pkg["package_root"]) / pkg["package_name"]
107
109
  file_count = str(pkg["file_count"])
108
110
  latest = _format_timestamp(pkg["latest_install"])
109
- table.add_row(package_path, file_count, latest)
111
+
112
+ # Check if source still exists
113
+ if package_path.exists():
114
+ status = "[green]✓[/green]"
115
+ else:
116
+ status = "[yellow]⚠ source missing[/yellow]"
117
+ missing_count += 1
118
+
119
+ table.add_row(str(package_path), file_count, latest, status)
110
120
 
111
121
  console.print()
112
122
  console.print(table)
113
- console.print(f"\n[bold]Total: {len(packages)} package(s)[/bold]\n")
123
+ console.print(f"\n[bold]Total: {len(packages)} package(s)[/bold]")
124
+ if missing_count > 0:
125
+ console.print(f"[yellow]⚠ {missing_count} package(s) have missing source directories. Run `dotx verify` for details.[/yellow]")
126
+ console.print()
114
127
 
115
128
  logger.info("list finished")
116
129
 
@@ -99,11 +99,18 @@ def register_command(app: typer.Typer):
99
99
  if total_dirs:
100
100
  summary_parts.append(f"{total_dirs} dir(s)")
101
101
 
102
- summary = " and ".join(summary_parts) if summary_parts else "nothing"
103
- if is_dry_run(ctx):
104
- console.print(f"\n[yellow][DRY RUN] Would install {summary} from {len(sources)} package(s)[/yellow]")
102
+ if summary_parts:
103
+ summary = " and ".join(summary_parts)
104
+ if is_dry_run(ctx):
105
+ console.print(f"\n[yellow][DRY RUN] Would install {summary} from {len(sources)} package(s)[/yellow]")
106
+ else:
107
+ console.print(f"\n[green]✓ Installed {summary} from {len(sources)} package(s)[/green]")
105
108
  else:
106
- console.print(f"\n[green]✓ Installed {summary} from {len(sources)} package(s)[/green]")
109
+ # Empty package(s) - nothing to install
110
+ if is_dry_run(ctx):
111
+ console.print(f"\n[yellow][DRY RUN] Would install nothing from {len(sources)} package(s) (nothing to record in database)[/yellow]")
112
+ else:
113
+ console.print(f"\n[green]✓ Installed nothing from {len(sources)} package(s) (nothing to record in database)[/green]")
107
114
  else:
108
115
  console.print("[red]✗ Refusing to install - conflicts detected[/red]")
109
116
 
@@ -1,16 +1,17 @@
1
1
  """
2
2
  This module provides convenience functions for accessing user-data on the associated context.
3
3
 
4
- Note: typer is built on click, so we use click.Context for type annotations.
4
+ Note: typer is built on click, so we use typer.Context for type annotations.
5
5
  """
6
6
 
7
7
  from pathlib import Path
8
8
  from typing import Any
9
9
 
10
10
  import click
11
+ import typer
11
12
 
12
13
 
13
- def set_option(option: str, value: Any, ctx: click.Context | None = None) -> bool:
14
+ def set_option(option: str, value: Any, ctx: typer.Context | None = None) -> bool:
14
15
  """
15
16
  Set an option value in the Typer context.
16
17
 
@@ -28,7 +29,7 @@ def set_option(option: str, value: Any, ctx: click.Context | None = None) -> boo
28
29
  return False
29
30
 
30
31
 
31
- def get_option(option: str, default_for_option: Any = None, ctx: click.Context | None = None) -> Any:
32
+ def get_option(option: str, default_for_option: Any = None, ctx: typer.Context | None = None) -> Any:
32
33
  """
33
34
  Get an option value from the context.
34
35
 
@@ -44,22 +45,22 @@ def get_option(option: str, default_for_option: Any = None, ctx: click.Context |
44
45
  return default_for_option
45
46
 
46
47
 
47
- def is_verbose_mode(ctx: click.Context | None = None) -> bool:
48
+ def is_verbose_mode(ctx: typer.Context | None = None) -> bool:
48
49
  """Check if verbose mode is enabled."""
49
50
  return get_option("VERBOSE", False, ctx)
50
51
 
51
52
 
52
- def is_debug_mode(ctx: click.Context | None = None) -> bool:
53
+ def is_debug_mode(ctx: typer.Context | None = None) -> bool:
53
54
  """Check if debug mode is enabled."""
54
55
  return get_option("DEBUG", False, ctx)
55
56
 
56
57
 
57
- def is_dry_run(ctx: click.Context | None = None) -> bool:
58
+ def is_dry_run(ctx: typer.Context | None = None) -> bool:
58
59
  """Check if dry-run mode is enabled."""
59
60
  return get_option("DRYRUN", False, ctx)
60
61
 
61
62
 
62
- def is_xdg_mode(ctx: click.Context | None = None) -> bool:
63
+ def is_xdg_mode(ctx: typer.Context | None = None) -> bool:
63
64
  """Check if XDG mode is enabled."""
64
65
  return get_option("XDG", False, ctx)
65
66
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dotx
3
- Version: 3.3.1
3
+ Version: 3.3.3
4
4
  Summary: A command-line tool to install a link-farm to your dotfiles
5
5
  Author-email: Wolf <Wolf@zv.cx>
6
6
  License-Expression: MIT
@@ -301,7 +301,7 @@ def test_cli_list_no_packages(tmp_path, monkeypatch):
301
301
  result = runner.invoke(app, ["list"])
302
302
 
303
303
  assert result.exit_code == 0
304
- assert "No packages installed" in result.output
304
+ assert "No packages in database" in result.output
305
305
 
306
306
 
307
307
  def test_cli_list_with_packages(tmp_path, monkeypatch):
@@ -866,3 +866,44 @@ def test_list_shows_correct_package_names_after_install(tmp_path, monkeypatch):
866
866
  result = runner.invoke(app, ["list"])
867
867
  assert result.exit_code == 0, f"list failed: {result.output}"
868
868
  assert "2 package(s)" in result.output # Should show 2 packages
869
+
870
+
871
+ def test_cli_list_shows_missing_source_status(tmp_path, monkeypatch):
872
+ """Test 'dotx list' shows status indicator when source package is missing."""
873
+ import shutil
874
+
875
+ # Override XDG_DATA_HOME
876
+ data_dir = tmp_path / "config"
877
+ data_dir.mkdir()
878
+ monkeypatch.setenv("XDG_DATA_HOME", str(data_dir))
879
+
880
+ # Create and install packages
881
+ source1 = tmp_path / "source1"
882
+ source1.mkdir()
883
+ (source1 / "file1").write_text("content")
884
+
885
+ source2 = tmp_path / "source2"
886
+ source2.mkdir()
887
+ (source2 / "file2").write_text("content")
888
+
889
+ target = tmp_path / "target"
890
+ target.mkdir()
891
+
892
+ runner = CliRunner()
893
+
894
+ # Install both packages
895
+ result = runner.invoke(app, [f"--target={target}", "install", str(source1)])
896
+ assert result.exit_code == 0
897
+ result = runner.invoke(app, [f"--target={target}", "install", str(source2)])
898
+ assert result.exit_code == 0
899
+
900
+ # Delete one source package
901
+ shutil.rmtree(source1)
902
+ assert not source1.exists()
903
+
904
+ # List packages - should show warning for missing source
905
+ result = runner.invoke(app, ["list"])
906
+ assert result.exit_code == 0
907
+ # The warning message at the bottom should be visible
908
+ assert "1 package(s) have missing source directories" in result.output
909
+ assert "dotx verify" in result.output
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes