dotx 2.0.4__tar.gz → 2.1.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 (29) hide show
  1. {dotx-2.0.4/src/dotx.egg-info → dotx-2.1.0}/PKG-INFO +2 -1
  2. {dotx-2.0.4 → dotx-2.1.0}/pyproject.toml +2 -1
  3. {dotx-2.0.4 → dotx-2.1.0}/src/dotx/cli.py +245 -67
  4. {dotx-2.0.4 → dotx-2.1.0/src/dotx.egg-info}/PKG-INFO +2 -1
  5. {dotx-2.0.4 → dotx-2.1.0}/src/dotx.egg-info/requires.txt +1 -0
  6. {dotx-2.0.4 → dotx-2.1.0}/tests/test_cli_database.py +3 -2
  7. {dotx-2.0.4 → dotx-2.1.0}/LICENSE +0 -0
  8. {dotx-2.0.4 → dotx-2.1.0}/MANIFEST.in +0 -0
  9. {dotx-2.0.4 → dotx-2.1.0}/README.md +0 -0
  10. {dotx-2.0.4 → dotx-2.1.0}/setup.cfg +0 -0
  11. {dotx-2.0.4 → dotx-2.1.0}/src/dotx/__init__.py +0 -0
  12. {dotx-2.0.4 → dotx-2.1.0}/src/dotx/database.py +0 -0
  13. {dotx-2.0.4 → dotx-2.1.0}/src/dotx/ignore.py +0 -0
  14. {dotx-2.0.4 → dotx-2.1.0}/src/dotx/install.py +0 -0
  15. {dotx-2.0.4 → dotx-2.1.0}/src/dotx/installed-schema.sql +0 -0
  16. {dotx-2.0.4 → dotx-2.1.0}/src/dotx/options.py +0 -0
  17. {dotx-2.0.4 → dotx-2.1.0}/src/dotx/plan.py +0 -0
  18. {dotx-2.0.4 → dotx-2.1.0}/src/dotx/uninstall.py +0 -0
  19. {dotx-2.0.4 → dotx-2.1.0}/src/dotx.egg-info/SOURCES.txt +0 -0
  20. {dotx-2.0.4 → dotx-2.1.0}/src/dotx.egg-info/dependency_links.txt +0 -0
  21. {dotx-2.0.4 → dotx-2.1.0}/src/dotx.egg-info/entry_points.txt +0 -0
  22. {dotx-2.0.4 → dotx-2.1.0}/src/dotx.egg-info/top_level.txt +0 -0
  23. {dotx-2.0.4 → dotx-2.1.0}/tests/test_cli.py +0 -0
  24. {dotx-2.0.4 → dotx-2.1.0}/tests/test_ignore.py +0 -0
  25. {dotx-2.0.4 → dotx-2.1.0}/tests/test_ignore_rules.py +0 -0
  26. {dotx-2.0.4 → dotx-2.1.0}/tests/test_install.py +0 -0
  27. {dotx-2.0.4 → dotx-2.1.0}/tests/test_options.py +0 -0
  28. {dotx-2.0.4 → dotx-2.1.0}/tests/test_plan.py +0 -0
  29. {dotx-2.0.4 → dotx-2.1.0}/tests/test_uninstall.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dotx
3
- Version: 2.0.4
3
+ Version: 2.1.0
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
@@ -22,6 +22,7 @@ License-File: LICENSE
22
22
  Requires-Dist: click>=8.1.7
23
23
  Requires-Dist: loguru>=0.7.0
24
24
  Requires-Dist: pathspec>=0.12.1
25
+ Requires-Dist: rich>=13.9.4
25
26
  Requires-Dist: typer>=0.15.1
26
27
  Dynamic: license-file
27
28
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dotx"
3
- version = "2.0.4"
3
+ version = "2.1.0"
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" }
@@ -24,6 +24,7 @@ dependencies = [
24
24
  "click>=8.1.7",
25
25
  "loguru>=0.7.0",
26
26
  "pathspec>=0.12.1",
27
+ "rich>=13.9.4",
27
28
  "typer>=0.15.1",
28
29
  ]
29
30
 
@@ -21,11 +21,15 @@ from typing import Annotated, Optional
21
21
  import click
22
22
  import typer
23
23
  from loguru import logger
24
+ from rich.console import Console
25
+ from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn
26
+ from rich.table import Table
27
+ from rich.panel import Panel
24
28
 
25
29
  from dotx import __version__, __homepage__
26
30
  from dotx.database import InstallationDB
27
31
  from dotx.install import plan_install
28
- from dotx.options import set_option
32
+ from dotx.options import set_option, is_verbose_mode
29
33
  from dotx.plan import Action, Plan, execute_plan, extract_plan, log_extracted_plan
30
34
  from dotx.uninstall import plan_uninstall
31
35
 
@@ -128,6 +132,8 @@ def install(
128
132
  ):
129
133
  """Install source packages to target directory."""
130
134
  logger.info("install starting")
135
+ console = Console()
136
+ verbose = is_verbose_mode(ctx)
131
137
 
132
138
  # Get target from options
133
139
  target_path = Path(ctx.obj.get("TARGET", Path.home())) if ctx.obj else Path.home()
@@ -148,20 +154,64 @@ def install(
148
154
  failures = extract_plan(plan, {Action.FAIL})
149
155
  if failures:
150
156
  can_install = False
151
- typer.echo(
152
- f"Error: can't install {source_package} because it would overwrite:"
157
+ console.print(
158
+ f"[red]✗ Error: can't install {source_package.name} - would overwrite:[/red]"
153
159
  )
154
160
  for plan_node in failures:
155
- typer.echo(f"{target_path / plan_node.relative_destination_path}")
156
- typer.echo()
161
+ console.print(f" {target_path / plan_node.relative_destination_path}")
162
+ console.print()
157
163
 
158
164
  if can_install:
159
- # Open database and execute all plans
165
+ # Count total actions
166
+ total_actions = sum(
167
+ len(extract_plan(plan, {Action.LINK, Action.CREATE}))
168
+ for _, plan in plans
169
+ )
170
+
171
+ # Open database and execute all plans with progress
160
172
  with InstallationDB() as db:
161
- for source_package, plan in plans:
162
- execute_plan(source_package, target_path, plan, db)
173
+ if verbose:
174
+ # Verbose: show each file
175
+ for source_package, plan in plans:
176
+ console.print(f"[cyan]Installing {source_package.name}...[/cyan]")
177
+ for node in extract_plan(plan, {Action.LINK, Action.CREATE}):
178
+ console.print(f" {node.relative_destination_path}")
179
+ execute_plan(source_package, target_path, plan, db)
180
+ else:
181
+ # Default: show progress bar
182
+ with Progress(
183
+ SpinnerColumn(),
184
+ TextColumn("[progress.description]{task.description}"),
185
+ BarColumn(),
186
+ TaskProgressColumn(),
187
+ console=console,
188
+ ) as progress:
189
+ task = progress.add_task("Installing...", total=total_actions)
190
+ for source_package, plan in plans:
191
+ progress.update(task, description=f"Installing {source_package.name}...")
192
+ execute_plan(source_package, target_path, plan, db)
193
+ progress.advance(task, len(extract_plan(plan, {Action.LINK, Action.CREATE})))
194
+
195
+ # Show summary
196
+ total_files = sum(
197
+ len(extract_plan(plan, {Action.LINK}))
198
+ for _, plan in plans
199
+ )
200
+ total_dirs = sum(
201
+ len(extract_plan(plan, {Action.CREATE}))
202
+ for _, plan in plans
203
+ )
204
+
205
+ summary_parts = []
206
+ if total_files:
207
+ summary_parts.append(f"{total_files} file(s)")
208
+ if total_dirs:
209
+ summary_parts.append(f"{total_dirs} dir(s)")
210
+
211
+ summary = " and ".join(summary_parts) if summary_parts else "nothing"
212
+ console.print(f"\n[green]✓ Installed {summary} from {len(sources)} package(s)[/green]")
163
213
  else:
164
- typer.echo("Refusing to install anything because of previous failures.")
214
+ console.print("[red]✗ Refusing to install - conflicts detected[/red]")
165
215
 
166
216
  logger.info("install finished")
167
217
 
@@ -182,6 +232,8 @@ def uninstall(
182
232
  ):
183
233
  """Uninstall source packages from target directory."""
184
234
  logger.info("uninstall starting")
235
+ console = Console()
236
+ verbose = is_verbose_mode(ctx)
185
237
 
186
238
  # Get target from options
187
239
  target_path = Path(ctx.obj.get("TARGET", Path.home())) if ctx.obj else Path.home()
@@ -197,10 +249,42 @@ def uninstall(
197
249
  )
198
250
  plans.append((source_package, plan))
199
251
 
200
- # Open database and execute all plans
252
+ # Count total actions
253
+ total_actions = sum(
254
+ len(extract_plan(plan, {Action.UNLINK}))
255
+ for _, plan in plans
256
+ )
257
+
258
+ # Open database and execute all plans with progress
201
259
  with InstallationDB() as db:
202
- for source_package, plan in plans:
203
- execute_plan(source_package, target_path, plan, db)
260
+ if verbose:
261
+ # Verbose: show each file
262
+ for source_package, plan in plans:
263
+ console.print(f"[cyan]Uninstalling {source_package.name}...[/cyan]")
264
+ for node in extract_plan(plan, {Action.UNLINK}):
265
+ console.print(f" {node.relative_destination_path}")
266
+ execute_plan(source_package, target_path, plan, db)
267
+ else:
268
+ # Default: show progress bar
269
+ with Progress(
270
+ SpinnerColumn(),
271
+ TextColumn("[progress.description]{task.description}"),
272
+ BarColumn(),
273
+ TaskProgressColumn(),
274
+ console=console,
275
+ ) as progress:
276
+ task = progress.add_task("Uninstalling...", total=total_actions)
277
+ for source_package, plan in plans:
278
+ progress.update(task, description=f"Uninstalling {source_package.name}...")
279
+ execute_plan(source_package, target_path, plan, db)
280
+ progress.advance(task, len(extract_plan(plan, {Action.UNLINK})))
281
+
282
+ # Show summary
283
+ total_removed = sum(
284
+ len(extract_plan(plan, {Action.UNLINK}))
285
+ for _, plan in plans
286
+ )
287
+ console.print(f"\n[green]✓ Removed {total_removed} symlink(s) from {len(sources)} package(s)[/green]")
204
288
 
205
289
  logger.info("uninstall finished")
206
290
 
@@ -214,35 +298,39 @@ def list_installed(
214
298
  ):
215
299
  """List all installed packages."""
216
300
  logger.info("list starting")
301
+ console = Console()
217
302
 
218
303
  with InstallationDB() as db:
219
304
  packages = db.get_all_packages()
220
305
 
221
306
  if not packages:
222
- typer.echo("No packages installed.")
307
+ console.print("[yellow]No packages installed.[/yellow]")
223
308
  return
224
309
 
225
310
  if as_commands:
226
- # Output as dotx install commands
311
+ # Output as dotx install commands (plain text, no formatting)
227
312
  for pkg in packages:
228
313
  typer.echo(f"dotx install {pkg['package_name']}")
229
314
  else:
230
- # Output as table
231
- typer.echo("\nInstalled Packages:")
232
- typer.echo("-" * 80)
233
- typer.echo(f"{'Package':<50} {'Files':<10} {'Last Install':<20}")
234
- typer.echo("-" * 80)
315
+ # Output as rich table
316
+ table = Table(title="Installed Packages", show_header=True, header_style="bold cyan")
317
+ table.add_column("Package", style="cyan", no_wrap=True)
318
+ table.add_column("Files", justify="right", style="magenta")
319
+ table.add_column("Last Install", style="green")
320
+
235
321
  for pkg in packages:
236
322
  package_name = Path(pkg["package_name"]).name
237
- file_count = pkg["file_count"]
323
+ file_count = str(pkg["file_count"])
238
324
  latest = (
239
325
  pkg["latest_install"][:19]
240
326
  if pkg["latest_install"]
241
- else "unknown"
327
+ else "[dim]unknown[/dim]"
242
328
  )
243
- typer.echo(f"{package_name:<50} {file_count:<10} {latest:<20}")
244
- typer.echo("-" * 80)
245
- typer.echo(f"Total: {len(packages)} package(s)\n")
329
+ table.add_row(package_name, file_count, latest)
330
+
331
+ console.print()
332
+ console.print(table)
333
+ console.print(f"\n[bold]Total: {len(packages)} package(s)[/bold]\n")
246
334
 
247
335
  logger.info("list finished")
248
336
 
@@ -262,6 +350,7 @@ def verify(
262
350
  ):
263
351
  """Verify installations against filesystem."""
264
352
  logger.info("verify starting")
353
+ console = Console()
265
354
 
266
355
  with InstallationDB() as db:
267
356
  if package:
@@ -275,24 +364,24 @@ def verify(
275
364
  ]
276
365
 
277
366
  if not packages_to_verify:
278
- typer.echo("No packages to verify.")
367
+ console.print("[yellow]No packages to verify.[/yellow]")
279
368
  return
280
369
 
281
370
  total_issues = 0
282
371
  for pkg in packages_to_verify:
283
372
  issues = db.verify_installations(pkg)
284
373
  if issues:
285
- typer.echo(f"\n{pkg}:")
374
+ console.print(f"\n[bold cyan]{pkg.name}:[/bold cyan]")
286
375
  for issue in issues:
287
- typer.echo(f" {issue['target_path']}")
288
- typer.echo(f" Issue: {issue['issue']}")
289
- typer.echo(f" Expected type: {issue['link_type']}")
376
+ console.print(f" [red]✗[/red] {issue['target_path']}")
377
+ console.print(f" [dim]Issue: {issue['issue']}[/dim]")
378
+ console.print(f" [dim]Expected: {issue['link_type']}[/dim]")
290
379
  total_issues += len(issues)
291
380
 
292
381
  if total_issues == 0:
293
- typer.echo("✓ All installations verified successfully.")
382
+ console.print("[green]✓ All installations verified successfully.[/green]")
294
383
  else:
295
- typer.echo(f"\n⚠ Found {total_issues} issue(s).")
384
+ console.print(f"\n[yellow]⚠ Found {total_issues} issue(s).[/yellow]")
296
385
 
297
386
  logger.info("verify finished")
298
387
 
@@ -312,29 +401,78 @@ def show(
312
401
  ):
313
402
  """Show detailed installation information for a package."""
314
403
  logger.info("show starting")
404
+ console = Console()
315
405
 
316
406
  with InstallationDB() as db:
317
407
  installations = db.get_installations(package)
318
408
 
319
409
  if not installations:
320
- typer.echo(f"No installations found for {package}")
410
+ console.print(f"[yellow]No installations found for {package.name}[/yellow]")
321
411
  return
322
412
 
323
- typer.echo(f"\nPackage: {package}")
324
- typer.echo(f"Installed files: {len(installations)}")
325
- typer.echo("\nInstallations:")
326
- typer.echo("-" * 80)
413
+ # Create info panel
414
+ info = f"[bold cyan]Package:[/bold cyan] {package}\n"
415
+ info += f"[bold cyan]Installed files:[/bold cyan] {len(installations)}"
416
+
417
+ panel = Panel(info, title="Package Information", border_style="cyan")
418
+ console.print()
419
+ console.print(panel)
420
+
421
+ # Create table for installations
422
+ table = Table(show_header=True, header_style="bold magenta", box=None)
423
+ table.add_column("Target Path", style="cyan", no_wrap=False, overflow="fold")
424
+ table.add_column("Type", style="yellow")
425
+ table.add_column("Installed At", style="green")
327
426
 
328
427
  for install in installations:
329
- typer.echo(f"\n Target: {install['target_path']}")
330
- typer.echo(f" Type: {install['link_type']}")
331
- typer.echo(f" When: {install['installed_at']}")
428
+ table.add_row(
429
+ str(install['target_path']),
430
+ install['link_type'],
431
+ install['installed_at'][:19] if install['installed_at'] else "unknown"
432
+ )
332
433
 
333
- typer.echo("-" * 80)
434
+ console.print()
435
+ console.print(table)
436
+ console.print()
334
437
 
335
438
  logger.info("show finished")
336
439
 
337
440
 
441
+ def _scan_symlinks(path: Path, max_depth: int, progress: Progress, task_id) -> list[tuple[Path, bool]]:
442
+ """
443
+ Scan a directory for symlinks up to a maximum depth.
444
+
445
+ Returns list of (symlink_path, is_dir) tuples.
446
+ """
447
+ symlinks = []
448
+
449
+ def _walk_limited(directory: Path, current_depth: int):
450
+ """Recursively walk directory up to max_depth."""
451
+ if current_depth > max_depth:
452
+ return
453
+
454
+ try:
455
+ for item in directory.iterdir():
456
+ # Update progress
457
+ progress.update(task_id, advance=1)
458
+
459
+ if item.is_symlink():
460
+ is_dir = item.is_dir() # Check if symlink points to directory
461
+ symlinks.append((item, is_dir))
462
+ # Don't descend into symlinked directories
463
+ continue
464
+
465
+ # Recurse into regular directories
466
+ if item.is_dir() and not item.is_symlink():
467
+ _walk_limited(item, current_depth + 1)
468
+
469
+ except (PermissionError, OSError) as e:
470
+ logger.debug(f"Skipping {directory}: {e}")
471
+
472
+ _walk_limited(path, 0)
473
+ return symlinks
474
+
475
+
338
476
  @app.command()
339
477
  def sync(
340
478
  ctx: click.Context,
@@ -342,37 +480,77 @@ def sync(
342
480
  bool,
343
481
  typer.Option("--dry-run", help="Show what would be added without modifying the database"),
344
482
  ] = False,
483
+ max_depth: Annotated[
484
+ Optional[int],
485
+ typer.Option(help="Maximum depth to scan (default: 1 for home, 3 for config)"),
486
+ ] = None,
487
+ scan_paths: Annotated[
488
+ Optional[list[Path]],
489
+ typer.Option(help="Additional paths to scan"),
490
+ ] = None,
491
+ simple: Annotated[
492
+ bool,
493
+ typer.Option(help="Simple scan: only home directory depth 1, skip ~/.config"),
494
+ ] = False,
345
495
  ):
346
- """Rebuild database from filesystem (scan for existing symlinks)."""
496
+ """
497
+ Rebuild database from filesystem (scan for existing symlinks).
498
+
499
+ By default, scans:
500
+ - Top-level of home directory (depth=1)
501
+ - All of ~/.config (depth=3)
502
+
503
+ Use --simple to only scan home directory at depth 1.
504
+ Use --max-depth to override default depth for all paths.
505
+ Use --scan-paths to add additional directories to scan.
506
+ """
347
507
  logger.info("sync starting")
508
+ console = Console()
348
509
 
349
510
  # Get target from options
350
511
  target_path = Path(ctx.obj.get("TARGET", Path.home())) if ctx.obj else Path.home()
351
512
 
352
- # Scan filesystem for symlinks
513
+ # Determine scan strategy
514
+ scan_configs = []
515
+
516
+ if simple:
517
+ # Simple mode: just top-level of home
518
+ scan_configs.append((target_path, max_depth or 1))
519
+ else:
520
+ # Smart mode: top-level home + full ~/.config
521
+ scan_configs.append((target_path, max_depth or 1))
522
+ config_path = target_path / ".config"
523
+ if config_path.exists() and config_path.is_dir():
524
+ scan_configs.append((config_path, max_depth or 3))
525
+
526
+ # Add any user-specified paths
527
+ if scan_paths:
528
+ for path in scan_paths:
529
+ scan_configs.append((path, max_depth or 3))
530
+
531
+ # Scan filesystem for symlinks with progress
353
532
  symlinks = []
354
- for root, dirs, files in target_path.walk():
355
- # Check if directories are symlinks
356
- for dirname in dirs[:]:
357
- dirpath = root / dirname
358
- if dirpath.is_symlink():
359
- symlinks.append((dirpath, True)) # (path, is_dir)
360
- # Don't descend into symlinked directories
361
- dirs.remove(dirname)
362
-
363
- # Check if files are symlinks
364
- for filename in files:
365
- filepath = root / filename
366
- if filepath.is_symlink():
367
- symlinks.append((filepath, False)) # (path, is_dir)
533
+
534
+ with Progress(
535
+ SpinnerColumn(),
536
+ TextColumn("[progress.description]{task.description}"),
537
+ BarColumn(),
538
+ TaskProgressColumn(),
539
+ console=console,
540
+ ) as progress:
541
+ scan_task = progress.add_task("Scanning for symlinks...", total=None)
542
+
543
+ for scan_path, depth in scan_configs:
544
+ progress.update(scan_task, description=f"Scanning {scan_path.name}...")
545
+ symlinks.extend(_scan_symlinks(scan_path, depth, progress, scan_task))
546
+
547
+ console.print(f"[green]✓[/green] Found {len(symlinks)} symlink(s)")
368
548
 
369
549
  if not symlinks:
370
- typer.echo("No symlinks found in target directory.")
550
+ console.print("[yellow]No symlinks found.[/yellow]")
371
551
  logger.info("sync finished - no symlinks found")
372
552
  return
373
553
 
374
- typer.echo(f"Found {len(symlinks)} symlink(s) in {target_path}")
375
-
376
554
  # Group symlinks by their resolved source parent directory (package)
377
555
  packages = {}
378
556
  unknown = []
@@ -397,23 +575,23 @@ def sync(
397
575
  unknown.append((link_path, None, is_dir))
398
576
 
399
577
  # Show what was found
400
- typer.echo(f"\nDiscovered {len(packages)} potential package(s):")
578
+ console.print(f"\n[bold]Discovered {len(packages)} potential package(s):[/bold]")
401
579
  for package_root, links in packages.items():
402
- typer.echo(f"\n {package_root}")
403
- typer.echo(f" {len(links)} symlink(s)")
580
+ console.print(f" [cyan]{package_root}[/cyan]")
581
+ console.print(f" {len(links)} symlink(s)")
404
582
 
405
583
  if unknown:
406
- typer.echo(f"\n Unknown/broken: {len(unknown)} symlink(s)")
584
+ console.print(f" [yellow]Unknown/broken: {len(unknown)} symlink(s)[/yellow]")
407
585
 
408
586
  if dry_run:
409
- typer.echo("\nDry run - no database changes made.")
587
+ console.print("\n[yellow]Dry run - no database changes made.[/yellow]")
410
588
  logger.info("sync finished - dry run")
411
589
  return
412
590
 
413
591
  # Ask for confirmation
414
- typer.echo("\nThis will rebuild the database with the discovered installations.")
592
+ console.print("\n[bold]This will rebuild the database with the discovered installations.[/bold]")
415
593
  if not typer.confirm("Continue?"):
416
- typer.echo("Cancelled.")
594
+ console.print("[yellow]Cancelled.[/yellow]")
417
595
  logger.info("sync finished - cancelled by user")
418
596
  return
419
597
 
@@ -434,7 +612,7 @@ def sync(
434
612
  total_recorded += 1
435
613
  logger.debug(f"Recorded {link_path} -> {package_root}")
436
614
 
437
- typer.echo(f"\n✓ Recorded {total_recorded} installation(s) in database.")
615
+ console.print(f"\n[green]✓ Recorded {total_recorded} installation(s) in database.[/green]")
438
616
 
439
617
  logger.info("sync finished")
440
618
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dotx
3
- Version: 2.0.4
3
+ Version: 2.1.0
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
@@ -22,6 +22,7 @@ License-File: LICENSE
22
22
  Requires-Dist: click>=8.1.7
23
23
  Requires-Dist: loguru>=0.7.0
24
24
  Requires-Dist: pathspec>=0.12.1
25
+ Requires-Dist: rich>=13.9.4
25
26
  Requires-Dist: typer>=0.15.1
26
27
  Dynamic: license-file
27
28
 
@@ -1,4 +1,5 @@
1
1
  click>=8.1.7
2
2
  loguru>=0.7.0
3
3
  pathspec>=0.12.1
4
+ rich>=13.9.4
4
5
  typer>=0.15.1
@@ -430,8 +430,9 @@ def test_cli_show_package(tmp_path, monkeypatch):
430
430
  assert result.exit_code == 0
431
431
  assert "Package:" in result.output
432
432
  assert "Installed files: 2" in result.output
433
- assert str(target / "file1") in result.output
434
- assert str(target / "file2") in result.output
433
+ # Rich may wrap long paths, so just check for the filenames
434
+ assert "file1" in result.output
435
+ assert "file2" in result.output
435
436
 
436
437
 
437
438
  def test_cli_show_nonexistent_package(tmp_path, monkeypatch):
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