Skip to content

fix(plugin): prevent _map_plugin_artifacts from destroying pre-positioned .apm/ content#1416

Open
abi-jey wants to merge 2 commits into
microsoft:mainfrom
abi-jey:fix/plugin-artifacts-self-destruct
Open

fix(plugin): prevent _map_plugin_artifacts from destroying pre-positioned .apm/ content#1416
abi-jey wants to merge 2 commits into
microsoft:mainfrom
abi-jey:fix/plugin-artifacts-self-destruct

Conversation

@abi-jey
Copy link
Copy Markdown
Contributor

@abi-jey abi-jey commented May 20, 2026

Summary

Fixes #1415

When a package has both apm.yml and .claude-plugin/plugin.json with manifest paths pointing into .apm/, apm install silently drops all agents and skills — reporting (files unchanged).

Root Cause

detect_package_type classifies packages with .claude-plugin/plugin.json as MARKETPLACE_PLUGIN (cascade priority — checked before APM_PACKAGE), even when apm.yml is present. This triggers _validate_marketplace_pluginnormalize_plugin_directory_map_plugin_artifacts.

_map_plugin_artifacts is designed for pure plugins where agents/ and skills/ sit at the package root and need to be copied INTO .apm/. It does a shutil.rmtree on the target (.apm/agents/, .apm/skills/) before copying from the source. When the manifest points to paths already inside .apm/, the source and target overlap — the rmtree destroys the source before the copy can read it.

Sequence:

  1. Download copies package to apm_modules/.apm/agents/ and .apm/skills/ are present
  2. validate_apm_package_map_plugin_artifacts runs
  3. shutil.rmtree(.apm/agents/)deletes the source
  4. shutil.copy2(source_file, ...) — source no longer exists, nothing is copied
  5. Integrate phase finds empty .apm/(files unchanged)

Fix

Added _all_inside_apm() helper that detects when all resolved source paths already reside under .apm/. When true, the destructive rmtree+copy cycle is skipped — the artifacts are already correctly positioned. Applied to all four component types: agents, skills, commands, hooks.

Tests

5 new regression tests in TestMapPluginArtifactsPrePositioned:

  • test_agents_inside_apm_are_preserved — manifest agents pointing into .apm/ survive
  • test_skills_inside_apm_are_preserved — manifest skills pointing into .apm/ survive
  • test_commands_inside_apm_are_preserved — manifest commands pointing into .apm/ survive
  • test_hooks_inside_apm_are_preserved — manifest hooks pointing into .apm/ survive
  • test_external_agents_still_copied — normal root-level→.apm/ copy still works (non-regression)

All 4 regression tests fail without the fix, pass with it. All 73 plugin parser tests pass.

@abi-jey abi-jey force-pushed the fix/plugin-artifacts-self-destruct branch from 01e1293 to a8845bf Compare May 20, 2026 09:31
@abi-jey abi-jey marked this pull request as ready for review May 20, 2026 09:32
Copilot AI review requested due to automatic review settings May 20, 2026 09:32
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a destructive edge case in plugin artifact normalization during apm install when plugin.json component paths point into an already-populated .apm/ tree, preventing accidental deletion of pre-positioned agents/skills/prompts/hooks.

Changes:

  • Added a containment guard in _map_plugin_artifacts() to skip the rmtree + copy cycle when sources appear to already live under .apm/.
  • Added regression tests ensuring .apm/ content is preserved for agents, skills, commands (prompts), and hooks.
  • Added a non-regression test confirming root-level agents/ are still copied into .apm/agents/.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/apm_cli/deps/plugin_parser.py Adds _all_inside_apm() and uses it to conditionally skip destructive remapping for agents/skills/commands/hooks.
tests/unit/test_plugin_parser.py Adds regression coverage for pre-positioned .apm/ artifact preservation and one non-regression copy test.

Comment thread src/apm_cli/deps/plugin_parser.py Outdated
Comment thread tests/unit/test_plugin_parser.py
@abi-jey abi-jey marked this pull request as draft May 20, 2026 10:01
@abi-jey abi-jey force-pushed the fix/plugin-artifacts-self-destruct branch from a8845bf to 6cabc1f Compare May 20, 2026 11:45
@abi-jey abi-jey marked this pull request as ready for review May 20, 2026 11:48
…oft#1415)

The `rmtree(target); copytree(source, target)` cycle wiped the source
when a hybrid APM + Claude-plugin manifest pointed paths INTO `.apm/`,
causing `apm install` to silently report `(files unchanged)`.

The rmtree was redundant -- the downloader already clears install_path
before extraction. Replaced with `dirs_exist_ok=True` plus an
`_is_same_path` guard for `copy2` self-copies.

Tests: 6 regression cases (pre-positioned + mixed-source layouts).
@abi-jey abi-jey force-pushed the fix/plugin-artifacts-self-destruct branch from 6cabc1f to 0cf8057 Compare May 20, 2026 13:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG]: cluade Plugin.json + .apm directory in the same package causes apm install to delete the .apm directory

2 participants