this repo has no description

feat: sync command now removes skills not in environment

- Install new skills from pixi environment
- Remove skills that were installed but no longer available
- Show summary of changes and final state

Also includes CI workflow fixes and formatting.

+54 -30
-1
.gitignore
··· 385 385 # Build outputs 386 386 output/ 387 387 .pi/ 388 -
+1 -1
.tangled/workflows/build.yml
··· 50 50 exit 1 51 51 fi 52 52 echo "Found package: $PACKAGE" 53 - ls -la "$PACKAGE" 53 + ls -la "$PACKAGE"
+1 -1
.tangled/workflows/conda-release.yml
··· 45 45 46 46 - name: "Upload to prefix.dev" 47 47 command: | 48 - rattler-build upload prefix --channel nandi-testing output/**/*.conda 48 + rattler-build upload prefix --channel nandi-testing output/**/*.conda
+48 -23
pixi_skills/cli.py
··· 290 290 [tool.pixi-skills] 291 291 agent = "pi" 292 292 293 - Then installs all skills from .pixi/envs/{env}/share/agent-skills/ to the 294 - agent's local skills directory. 293 + Installs skills from .pixi/envs/{env}/share/agent-skills/ and removes 294 + skills that are no longer available in the environment. 295 295 """ 296 296 # Find and parse config 297 297 config_path = find_pixi_toml() ··· 330 330 f"Syncing to [cyan]{backend_name}[/cyan] -> [dim]{skills_dir}[/dim]\n" 331 331 ) 332 332 333 - # Discover available skills 334 - skills = discover_local_skills(env) 333 + # Discover available skills from pixi environment 334 + available_skills = discover_local_skills(env) 335 + available_names = {s.name for s in available_skills} 335 336 336 - if not skills: 337 - console.print( 338 - f"[yellow]No skills found in .pixi/envs/{env}/share/agent-skills/[/yellow]" 339 - ) 337 + # Get currently installed skills in backend 338 + installed_skills = backend_instance.get_installed_skills(Scope.LOCAL) 339 + installed_names = {name for name, _ in installed_skills} 340 + 341 + # Determine what to install and remove 342 + to_install = [s for s in available_skills if s.name not in installed_names] 343 + to_remove = [name for name in installed_names if name not in available_names] 344 + 345 + if not to_install and not to_remove: 346 + console.print(f"[dim]Already in sync ({len(available_skills)} skill(s))[/dim]") 340 347 raise typer.Exit(0) 341 348 342 - # Get currently installed skills 343 - installed_names = { 344 - name for name, _ in backend_instance.get_installed_skills(Scope.LOCAL) 345 - } 349 + # Show summary of changes 350 + if available_skills: 351 + console.print(f"[dim]Available in env: {len(available_skills)} skill(s)[/dim]") 352 + else: 353 + console.print(f"[dim]No skills in .pixi/envs/{env}/share/agent-skills/[/dim]") 346 354 347 - # Determine what to install 348 - to_install = [s for s in skills if s.name not in installed_names] 355 + if installed_names: 356 + console.print(f"[dim]Installed: {len(installed_names)} skill(s)[/dim]") 357 + else: 358 + console.print("[dim]Installed: 0 skill(s)[/dim]") 349 359 350 - if not to_install: 351 - console.print(f"[dim]All {len(skills)} skill(s) already synced.[/dim]") 352 - raise typer.Exit(0) 353 - 354 - # Install skills 360 + # Install new skills 355 361 installed_count = 0 356 - for skill in sorted(to_install): 362 + for skill in sorted(to_install, key=lambda s: s.name): 357 363 try: 358 364 symlink_path = backend_instance.install(skill) 359 365 console.print(f"[green]+[/green] {skill.name} -> {symlink_path}") ··· 361 367 except ValueError as e: 362 368 console.print(f"[red]✗ {skill.name}: {e}[/red]") 363 369 364 - console.print( 365 - f"\n[green]Synced {installed_count}/{len(to_install)} skill(s)[/green]" 366 - ) 370 + # Remove skills no longer available 371 + removed_count = 0 372 + for name in sorted(to_remove): 373 + if backend_instance.uninstall(name, Scope.LOCAL): 374 + console.print(f"[red]-[/red] {name}") 375 + removed_count += 1 376 + else: 377 + console.print(f"[yellow]?[/yellow] {name} (not found)") 378 + 379 + # Summary 380 + changes = [] 381 + if installed_count: 382 + changes.append(f"installed {installed_count}") 383 + if removed_count: 384 + changes.append(f"removed {removed_count}") 385 + 386 + if changes: 387 + console.print(f"\n[green]Synced: {', '.join(changes)} skill(s)[/green]") 388 + 389 + # Show final state 390 + final_count = len(available_names) 391 + console.print(f"[dim]Result: {final_count} skill(s) in {skills_dir}[/dim]") 367 392 368 393 369 394 if __name__ == "__main__":
+1 -1
pixi_skills/config.py
··· 61 61 parent = current.parent 62 62 if parent == current: # Reached filesystem root 63 63 return None 64 - current = parent 64 + current = parent
+1 -1
recipe/recipe.release.yaml
··· 38 38 summary: Manage coding agent skills using pixi 39 39 homepage: https://github.com/pavelzw/pixi-skills 40 40 license: MIT 41 - license_file: LICENSE 41 + license_file: LICENSE
+1 -1
recipe/recipe.yaml
··· 34 34 summary: Manage coding agent skills using pixi 35 35 homepage: https://github.com/pavelzw/pixi-skills 36 36 license: MIT 37 - license_file: LICENSE 37 + license_file: LICENSE
+1 -1
tests/test_config.py
··· 92 92 deep_dir = tmp_path / "a" / "b" / "c" 93 93 deep_dir.mkdir(parents=True) 94 94 result = find_pixi_toml(deep_dir) 95 - assert result == config_file 95 + assert result == config_file