diff --git a/CHANGELOG.md b/CHANGELOG.md index 4708cb45b..31fa5fcea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,10 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Hook integrator now processes the `windows` property in hook JSON files, copying referenced scripts and rewriting paths during install/compile (#311) -- `apm install -g` now correctly deploys to user-scope directories (e.g., `~/.copilot/` instead of `~/.github/`) across all integrators and uninstall paths (#542) -- `apm install -g` no longer deploys instructions to `~/.copilot/instructions/` (unsupported by Copilot CLI) (#542) -- Fixed partition routing for multi-level user directories (e.g., `~/.config/opencode/`) (#542) -- Fixed uninstall re-integration deploying to wrong paths at user scope (#542) +- `apm install -g` now correctly deploys to user-scope directories, skips unsupported primitives, and cleans up on uninstall -- including multi-level paths like `~/.config/opencode/` (#542) ### Added diff --git a/src/apm_cli/commands/install.py b/src/apm_cli/commands/install.py index 4eafbce5e..a3995d8e5 100644 --- a/src/apm_cli/commands/install.py +++ b/src/apm_cli/commands/install.py @@ -2334,13 +2334,18 @@ def _collect_descendants(node, visited=None): _removed_orphan_count = 0 _failed_orphan_count = 0 _deleted_orphan_paths: builtins.list = [] + # Build validation targets that cover both default KNOWN_TARGETS + # and scope-resolved targets so legacy project-scope orphan paths + # (e.g., ".github/...") are also cleaned up at user scope. + _validation_targets = _targets or {} + _default_targets = getattr(BaseIntegrator, "KNOWN_TARGETS", None) + if _default_targets: + _validation_targets = {**_default_targets, **_validation_targets} for _orphan_path in sorted(orphaned_deployed_files): # validate_deploy_path() is the safety gate: it rejects path-traversal, # requires a known integration prefix, and checks the resolved path # stays within project_root -- so rmtree is safe here. - # Use default prefixes (all KNOWN_TARGETS) so legacy deployed - # paths from older buggy installs are also cleaned up. - if BaseIntegrator.validate_deploy_path(_orphan_path, project_root): + if BaseIntegrator.validate_deploy_path(_orphan_path, project_root, targets=_validation_targets): _target = project_root / _orphan_path if _target.exists(): try: diff --git a/src/apm_cli/commands/uninstall/engine.py b/src/apm_cli/commands/uninstall/engine.py index 40cf69a79..5e6439f43 100644 --- a/src/apm_cli/commands/uninstall/engine.py +++ b/src/apm_cli/commands/uninstall/engine.py @@ -247,11 +247,21 @@ def _sync_integrations_after_uninstall(apm_package, project_root, all_deployed_f sync_managed = all_deployed_files if all_deployed_files else None if sync_managed is not None: - # For removal, partition against both resolved AND unresolved targets - # so legacy deployed_files (from older buggy user-scope installs that - # wrote to .github/ instead of .copilot/) are still bucketed and - # cleaned up during uninstall. + # Partition against default KNOWN_TARGETS for legacy/project-scope + # paths, then merge with resolved targets for user-scope paths. + # This ensures both .github/ (legacy) and .copilot/ (resolved) + # prefixes are recognized during uninstall cleanup. _buckets = BaseIntegrator.partition_managed_files(sync_managed) + if user_scope and _resolved_targets: + _scope_buckets = BaseIntegrator.partition_managed_files( + sync_managed, targets=_resolved_targets + ) + for _bname, _bpaths in _scope_buckets.items(): + _existing = _buckets.get(_bname) + if _existing is not None: + _existing.update(_bpaths) + else: + _buckets[_bname] = _bpaths else: _buckets = None diff --git a/src/apm_cli/integration/skill_integrator.py b/src/apm_cli/integration/skill_integrator.py index 9a6f9ffad..e51bd4b3f 100644 --- a/src/apm_cli/integration/skill_integrator.py +++ b/src/apm_cli/integration/skill_integrator.py @@ -292,7 +292,7 @@ def copy_skill_to_target( # Get and validate skill name from folder raw_skill_name = source_path.name - is_valid, error_msg = validate_skill_name(raw_skill_name) + is_valid, _ = validate_skill_name(raw_skill_name) if is_valid: skill_name = raw_skill_name else: @@ -918,7 +918,7 @@ def sync_integration(self, apm_package, project_root: Path, raw_name = dep.repo_url.split('/')[-1] if dep.is_virtual and dep.virtual_path: raw_name = dep.virtual_path.split('/')[-1] - is_valid, error_msg = validate_skill_name(raw_name) + is_valid, _ = validate_skill_name(raw_name) skill_name = raw_name if is_valid else normalize_skill_name(raw_name) installed_skill_names.add(skill_name)