apm-cli 0.8.3__tar.gz → 0.8.5__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 (175) hide show
  1. {apm_cli-0.8.3/src/apm_cli.egg-info → apm_cli-0.8.5}/PKG-INFO +2 -1
  2. {apm_cli-0.8.3 → apm_cli-0.8.5}/pyproject.toml +2 -1
  3. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/adapters/client/copilot.py +5 -2
  4. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/adapters/client/vscode.py +37 -15
  5. apm_cli-0.8.5/src/apm_cli/bundle/lockfile_enrichment.py +154 -0
  6. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/bundle/packer.py +33 -27
  7. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/bundle/plugin_exporter.py +24 -7
  8. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/bundle/unpacker.py +15 -0
  9. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/commands/_helpers.py +17 -11
  10. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/commands/audit.py +281 -141
  11. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/commands/compile/cli.py +60 -64
  12. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/commands/compile/watcher.py +29 -28
  13. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/commands/config.py +16 -13
  14. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/commands/deps/_utils.py +46 -16
  15. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/commands/deps/cli.py +55 -42
  16. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/commands/init.py +21 -23
  17. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/commands/install.py +542 -228
  18. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/commands/list_cmd.py +7 -8
  19. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/commands/mcp.py +20 -14
  20. apm_cli-0.8.5/src/apm_cli/commands/pack.py +251 -0
  21. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/commands/prune.py +17 -16
  22. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/commands/run.py +32 -28
  23. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/commands/runtime.py +20 -20
  24. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/commands/uninstall/cli.py +28 -29
  25. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/commands/uninstall/engine.py +27 -25
  26. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/commands/update.py +23 -22
  27. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/compilation/agents_compiler.py +34 -57
  28. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/compilation/claude_formatter.py +2 -4
  29. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/compilation/context_optimizer.py +13 -14
  30. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/compilation/distributed_compiler.py +8 -10
  31. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/compilation/template_builder.py +4 -3
  32. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/config.py +26 -9
  33. apm_cli-0.8.5/src/apm_cli/core/__init__.py +5 -0
  34. apm_cli-0.8.5/src/apm_cli/core/auth.py +419 -0
  35. apm_cli-0.8.5/src/apm_cli/core/command_logger.py +330 -0
  36. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/core/safe_installer.py +34 -11
  37. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/core/script_runner.py +7 -8
  38. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/core/target_detection.py +28 -5
  39. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/core/token_manager.py +25 -2
  40. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/deps/aggregator.py +2 -2
  41. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/deps/apm_resolver.py +30 -10
  42. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/deps/dependency_graph.py +15 -0
  43. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/deps/github_downloader.py +121 -84
  44. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/deps/lockfile.py +2 -3
  45. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/deps/plugin_parser.py +4 -2
  46. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/deps/verifier.py +2 -2
  47. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/drift.py +4 -0
  48. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/integration/agent_integrator.py +7 -6
  49. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/integration/command_integrator.py +3 -2
  50. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/integration/hook_integrator.py +2 -1
  51. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/integration/instruction_integrator.py +5 -2
  52. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/integration/mcp_integrator.py +222 -76
  53. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/integration/prompt_integrator.py +4 -2
  54. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/integration/skill_integrator.py +23 -10
  55. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/models/apm_package.py +2 -2
  56. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/models/dependency/reference.py +14 -2
  57. apm_cli-0.8.5/src/apm_cli/policy/__init__.py +47 -0
  58. apm_cli-0.8.5/src/apm_cli/policy/ci_checks.py +322 -0
  59. apm_cli-0.8.5/src/apm_cli/policy/discovery.py +426 -0
  60. apm_cli-0.8.5/src/apm_cli/policy/inheritance.py +260 -0
  61. apm_cli-0.8.5/src/apm_cli/policy/matcher.py +84 -0
  62. apm_cli-0.8.5/src/apm_cli/policy/models.py +143 -0
  63. apm_cli-0.8.5/src/apm_cli/policy/parser.py +269 -0
  64. apm_cli-0.8.5/src/apm_cli/policy/policy_checks.py +788 -0
  65. apm_cli-0.8.5/src/apm_cli/policy/schema.py +110 -0
  66. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/registry/operations.py +9 -3
  67. apm_cli-0.8.5/src/apm_cli/security/file_scanner.py +85 -0
  68. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/security/gate.py +2 -1
  69. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/utils/__init__.py +3 -0
  70. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/utils/diagnostics.py +48 -5
  71. apm_cli-0.8.5/src/apm_cli/utils/paths.py +27 -0
  72. apm_cli-0.8.5/src/apm_cli/utils/yaml_io.py +55 -0
  73. {apm_cli-0.8.3 → apm_cli-0.8.5/src/apm_cli.egg-info}/PKG-INFO +2 -1
  74. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli.egg-info/SOURCES.txt +14 -0
  75. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli.egg-info/requires.txt +1 -0
  76. {apm_cli-0.8.3 → apm_cli-0.8.5}/tests/test_apm_resolver.py +0 -2
  77. {apm_cli-0.8.3 → apm_cli-0.8.5}/tests/test_github_downloader.py +7 -5
  78. {apm_cli-0.8.3 → apm_cli-0.8.5}/tests/test_github_downloader_token_precedence.py +2 -2
  79. {apm_cli-0.8.3 → apm_cli-0.8.5}/tests/test_token_manager.py +31 -0
  80. apm_cli-0.8.3/src/apm_cli/bundle/lockfile_enrichment.py +0 -41
  81. apm_cli-0.8.3/src/apm_cli/commands/pack.py +0 -149
  82. apm_cli-0.8.3/src/apm_cli/core/__init__.py +0 -1
  83. {apm_cli-0.8.3 → apm_cli-0.8.5}/AUTHORS +0 -0
  84. {apm_cli-0.8.3 → apm_cli-0.8.5}/LICENSE +0 -0
  85. {apm_cli-0.8.3 → apm_cli-0.8.5}/README.md +0 -0
  86. {apm_cli-0.8.3 → apm_cli-0.8.5}/setup.cfg +0 -0
  87. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/__init__.py +0 -0
  88. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/adapters/__init__.py +0 -0
  89. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/adapters/client/__init__.py +0 -0
  90. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/adapters/client/base.py +0 -0
  91. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/adapters/client/codex.py +0 -0
  92. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/adapters/client/cursor.py +0 -0
  93. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/adapters/client/opencode.py +0 -0
  94. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/adapters/package_manager/__init__.py +0 -0
  95. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/adapters/package_manager/base.py +0 -0
  96. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/adapters/package_manager/default_manager.py +0 -0
  97. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/bundle/__init__.py +0 -0
  98. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/cli.py +0 -0
  99. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/commands/__init__.py +0 -0
  100. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/commands/compile/__init__.py +0 -0
  101. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/commands/deps/__init__.py +0 -0
  102. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/commands/uninstall/__init__.py +0 -0
  103. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/compilation/__init__.py +0 -0
  104. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/compilation/constants.py +0 -0
  105. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/compilation/constitution.py +0 -0
  106. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/compilation/constitution_block.py +0 -0
  107. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/compilation/injector.py +0 -0
  108. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/compilation/link_resolver.py +0 -0
  109. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/constants.py +0 -0
  110. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/core/conflict_detector.py +0 -0
  111. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/core/docker_args.py +0 -0
  112. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/core/operations.py +0 -0
  113. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/deps/__init__.py +0 -0
  114. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/deps/collection_parser.py +0 -0
  115. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/deps/package_validator.py +0 -0
  116. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/factory.py +0 -0
  117. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/integration/__init__.py +0 -0
  118. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/integration/base_integrator.py +0 -0
  119. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/integration/skill_transformer.py +0 -0
  120. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/integration/targets.py +0 -0
  121. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/integration/utils.py +0 -0
  122. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/models/__init__.py +0 -0
  123. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/models/dependency/__init__.py +0 -0
  124. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/models/dependency/mcp.py +0 -0
  125. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/models/dependency/types.py +0 -0
  126. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/models/plugin.py +0 -0
  127. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/models/results.py +0 -0
  128. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/models/validation.py +0 -0
  129. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/output/__init__.py +0 -0
  130. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/output/formatters.py +0 -0
  131. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/output/models.py +0 -0
  132. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/output/script_formatters.py +0 -0
  133. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/primitives/__init__.py +0 -0
  134. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/primitives/discovery.py +0 -0
  135. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/primitives/models.py +0 -0
  136. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/primitives/parser.py +0 -0
  137. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/registry/__init__.py +0 -0
  138. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/registry/client.py +0 -0
  139. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/registry/integration.py +0 -0
  140. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/runtime/__init__.py +0 -0
  141. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/runtime/base.py +0 -0
  142. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/runtime/codex_runtime.py +0 -0
  143. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/runtime/copilot_runtime.py +0 -0
  144. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/runtime/factory.py +0 -0
  145. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/runtime/llm_runtime.py +0 -0
  146. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/runtime/manager.py +0 -0
  147. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/security/__init__.py +0 -0
  148. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/security/audit_report.py +0 -0
  149. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/security/content_scanner.py +0 -0
  150. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/utils/console.py +0 -0
  151. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/utils/content_hash.py +0 -0
  152. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/utils/github_host.py +0 -0
  153. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/utils/helpers.py +0 -0
  154. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/utils/path_security.py +0 -0
  155. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/utils/version_checker.py +0 -0
  156. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/version.py +0 -0
  157. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/workflow/__init__.py +0 -0
  158. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/workflow/discovery.py +0 -0
  159. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/workflow/parser.py +0 -0
  160. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli/workflow/runner.py +0 -0
  161. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli.egg-info/dependency_links.txt +0 -0
  162. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli.egg-info/entry_points.txt +0 -0
  163. {apm_cli-0.8.3 → apm_cli-0.8.5}/src/apm_cli.egg-info/top_level.txt +0 -0
  164. {apm_cli-0.8.3 → apm_cli-0.8.5}/tests/test_apm_package_models.py +0 -0
  165. {apm_cli-0.8.3 → apm_cli-0.8.5}/tests/test_codex_docker_args_fix.py +0 -0
  166. {apm_cli-0.8.3 → apm_cli-0.8.5}/tests/test_codex_empty_string_and_defaults.py +0 -0
  167. {apm_cli-0.8.3 → apm_cli-0.8.5}/tests/test_collision_integration.py +0 -0
  168. {apm_cli-0.8.3 → apm_cli-0.8.5}/tests/test_console.py +0 -0
  169. {apm_cli-0.8.3 → apm_cli-0.8.5}/tests/test_distributed_compilation.py +0 -0
  170. {apm_cli-0.8.3 → apm_cli-0.8.5}/tests/test_empty_string_and_defaults.py +0 -0
  171. {apm_cli-0.8.3 → apm_cli-0.8.5}/tests/test_enhanced_discovery.py +0 -0
  172. {apm_cli-0.8.3 → apm_cli-0.8.5}/tests/test_lockfile.py +0 -0
  173. {apm_cli-0.8.3 → apm_cli-0.8.5}/tests/test_runnable_prompts.py +0 -0
  174. {apm_cli-0.8.3 → apm_cli-0.8.5}/tests/test_runtime_manager_token_precedence.py +0 -0
  175. {apm_cli-0.8.3 → apm_cli-0.8.5}/tests/test_virtual_package_multi_install.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: apm-cli
3
- Version: 0.8.3
3
+ Version: 0.8.5
4
4
  Summary: MCP configuration tool
5
5
  Author-email: Daniel Meppiel <user@example.com>
6
6
  License: MIT License
@@ -52,6 +52,7 @@ Requires-Dist: GitPython>=3.1.0
52
52
  Provides-Extra: dev
53
53
  Requires-Dist: pytest>=7.0.0; extra == "dev"
54
54
  Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
55
+ Requires-Dist: pytest-xdist>=3.0.0; extra == "dev"
55
56
  Requires-Dist: black>=26.3.1; python_version >= "3.10" and extra == "dev"
56
57
  Requires-Dist: isort>=5.0.0; extra == "dev"
57
58
  Requires-Dist: mypy>=1.0.0; extra == "dev"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "apm-cli"
7
- version = "0.8.3"
7
+ version = "0.8.5"
8
8
  description = "MCP configuration tool"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -41,6 +41,7 @@ dependencies = [
41
41
  dev = [
42
42
  "pytest>=7.0.0",
43
43
  "pytest-cov>=4.0.0",
44
+ "pytest-xdist>=3.0.0",
44
45
  "black>=26.3.1; python_version>='3.10'",
45
46
  "isort>=5.0.0",
46
47
  "mypy>=1.0.0",
@@ -12,6 +12,7 @@ from .base import MCPClientAdapter
12
12
  from ...registry.client import SimpleRegistryClient
13
13
  from ...registry.integration import RegistryIntegration
14
14
  from ...core.docker_args import DockerArgsProcessor
15
+ from ...core.token_manager import GitHubTokenManager
15
16
  from ...utils.github_host import is_github_hostname
16
17
 
17
18
 
@@ -199,8 +200,10 @@ class CopilotClientAdapter(MCPClientAdapter):
199
200
  is_github_server = self._is_github_server(server_name, remote.get("url", ""))
200
201
 
201
202
  if is_github_server:
202
- # Check for GitHub Personal Access Token
203
- github_token = os.getenv("GITHUB_PERSONAL_ACCESS_TOKEN")
203
+ # Use centralized token manager (copilot chain: GITHUB_COPILOT_PAT → GITHUB_TOKEN → GITHUB_APM_PAT),
204
+ # falling back to GITHUB_PERSONAL_ACCESS_TOKEN for Copilot CLI compat.
205
+ _tm = GitHubTokenManager()
206
+ github_token = _tm.get_token_for_purpose('copilot') or os.getenv("GITHUB_PERSONAL_ACCESS_TOKEN")
204
207
  if github_token:
205
208
  config["headers"] = {
206
209
  "Authorization": f"Bearer {github_token}"
@@ -32,7 +32,7 @@ class VSCodeClientAdapter(MCPClientAdapter):
32
32
  self.registry_client = SimpleRegistryClient(registry_url)
33
33
  self.registry_integration = RegistryIntegration(registry_url)
34
34
 
35
- def get_config_path(self):
35
+ def get_config_path(self, logger=None):
36
36
  """Get the path to the VSCode MCP configuration file in the repository.
37
37
 
38
38
  Returns:
@@ -50,11 +50,14 @@ class VSCodeClientAdapter(MCPClientAdapter):
50
50
  if not vscode_dir.exists():
51
51
  vscode_dir.mkdir(parents=True, exist_ok=True)
52
52
  except Exception as e:
53
- print(f"Warning: Could not create .vscode directory: {e}")
53
+ if logger:
54
+ logger.warning(f"Could not create .vscode directory: {e}")
55
+ else:
56
+ print(f"Warning: Could not create .vscode directory: {e}")
54
57
 
55
58
  return str(mcp_config_path)
56
59
 
57
- def update_config(self, new_config):
60
+ def update_config(self, new_config, logger=None):
58
61
  """Update the VSCode MCP configuration with new values.
59
62
 
60
63
  Args:
@@ -63,7 +66,7 @@ class VSCodeClientAdapter(MCPClientAdapter):
63
66
  Returns:
64
67
  bool: True if successful, False otherwise.
65
68
  """
66
- config_path = self.get_config_path()
69
+ config_path = self.get_config_path(logger=logger)
67
70
 
68
71
  try:
69
72
  # Write the updated config
@@ -72,16 +75,19 @@ class VSCodeClientAdapter(MCPClientAdapter):
72
75
 
73
76
  return True
74
77
  except Exception as e:
75
- print(f"Error updating VSCode MCP configuration: {e}")
78
+ if logger:
79
+ logger.error(f"Error updating VSCode MCP configuration: {e}")
80
+ else:
81
+ print(f"Error updating VSCode MCP configuration: {e}")
76
82
  return False
77
83
 
78
- def get_current_config(self):
84
+ def get_current_config(self, logger=None):
79
85
  """Get the current VSCode MCP configuration.
80
86
 
81
87
  Returns:
82
88
  dict: Current VSCode MCP configuration from the local .vscode/mcp.json file.
83
89
  """
84
- config_path = self.get_config_path()
90
+ config_path = self.get_config_path(logger=logger)
85
91
 
86
92
  try:
87
93
  try:
@@ -90,10 +96,13 @@ class VSCodeClientAdapter(MCPClientAdapter):
90
96
  except (FileNotFoundError, json.JSONDecodeError):
91
97
  return {}
92
98
  except Exception as e:
93
- print(f"Error reading VSCode MCP configuration: {e}")
99
+ if logger:
100
+ logger.error(f"Error reading VSCode MCP configuration: {e}")
101
+ else:
102
+ print(f"Error reading VSCode MCP configuration: {e}")
94
103
  return {}
95
104
 
96
- def configure_mcp_server(self, server_url, server_name=None, enabled=True, env_overrides=None, server_info_cache=None, runtime_vars=None):
105
+ def configure_mcp_server(self, server_url, server_name=None, enabled=True, env_overrides=None, server_info_cache=None, runtime_vars=None, logger=None):
97
106
  """Configure an MCP server in VS Code mcp.json file.
98
107
 
99
108
  This method updates the .vscode/mcp.json file to add or update
@@ -105,6 +114,7 @@ class VSCodeClientAdapter(MCPClientAdapter):
105
114
  enabled (bool, optional): Whether to enable the server. Defaults to True.
106
115
  env_overrides (dict, optional): Environment variable overrides. Defaults to None.
107
116
  server_info_cache (dict, optional): Pre-fetched server info to avoid duplicate registry calls.
117
+ logger: Optional CommandLogger for structured output.
108
118
 
109
119
  Returns:
110
120
  bool: True if successful, False otherwise.
@@ -113,7 +123,10 @@ class VSCodeClientAdapter(MCPClientAdapter):
113
123
  ValueError: If server is not found in registry.
114
124
  """
115
125
  if not server_url:
116
- print("Error: server_url cannot be empty")
126
+ if logger:
127
+ logger.error("server_url cannot be empty")
128
+ else:
129
+ print("Error: server_url cannot be empty")
117
130
  return False
118
131
 
119
132
  try:
@@ -133,14 +146,17 @@ class VSCodeClientAdapter(MCPClientAdapter):
133
146
  server_config, input_vars = self._format_server_config(server_info)
134
147
 
135
148
  if not server_config:
136
- print(f"Unable to configure server: {server_url}")
149
+ if logger:
150
+ logger.error(f"Unable to configure server: {server_url}")
151
+ else:
152
+ print(f"Unable to configure server: {server_url}")
137
153
  return False
138
154
 
139
155
  # Use provided server name or fallback to server_url
140
156
  config_key = server_name or server_url
141
157
 
142
158
  # Get current config
143
- current_config = self.get_current_config()
159
+ current_config = self.get_current_config(logger=logger)
144
160
 
145
161
  # Ensure servers and inputs sections exist
146
162
  if "servers" not in current_config:
@@ -159,17 +175,23 @@ class VSCodeClientAdapter(MCPClientAdapter):
159
175
  existing_input_ids.add(var.get("id"))
160
176
 
161
177
  # Update the configuration
162
- result = self.update_config(current_config)
178
+ result = self.update_config(current_config, logger=logger)
163
179
 
164
180
  if result:
165
- print(f"Successfully configured MCP server '{config_key}' for VS Code")
181
+ if logger:
182
+ logger.verbose_detail(f"Configured MCP server '{config_key}' for VS Code")
183
+ else:
184
+ print(f"Successfully configured MCP server '{config_key}' for VS Code")
166
185
  return result
167
186
 
168
187
  except ValueError:
169
188
  # Re-raise ValueError for registry errors
170
189
  raise
171
190
  except Exception as e:
172
- print(f"Error configuring MCP server: {e}")
191
+ if logger:
192
+ logger.error(f"Error configuring MCP server: {e}")
193
+ else:
194
+ print(f"Error configuring MCP server: {e}")
173
195
  return False
174
196
 
175
197
  def _format_server_config(self, server_info):
@@ -0,0 +1,154 @@
1
+ """Lockfile enrichment for pack-time metadata."""
2
+
3
+ from datetime import datetime, timezone
4
+ from typing import Dict, List, Tuple
5
+
6
+ from ..deps.lockfile import LockFile
7
+
8
+
9
+ # Authoritative mapping of target names to deployed-file path prefixes.
10
+ _TARGET_PREFIXES = {
11
+ "copilot": [".github/"],
12
+ "vscode": [".github/"],
13
+ "claude": [".claude/"],
14
+ "cursor": [".cursor/"],
15
+ "opencode": [".opencode/"],
16
+ "all": [".github/", ".claude/", ".cursor/", ".opencode/"],
17
+ }
18
+
19
+ # Cross-target path equivalences for skills/ and agents/ directories.
20
+ # Only these two directory types are semantically identical across targets;
21
+ # commands, instructions, hooks are target-specific and are NOT mapped.
22
+ #
23
+ # .github/ is the canonical interop prefix -- install always creates it, so
24
+ # all non-github targets map FROM .github/. The copilot target additionally
25
+ # maps FROM .claude/ for the common case of Claude-first projects packing
26
+ # for Copilot. Cursor/opencode sources are niche; if someone publishes
27
+ # skills exclusively under .cursor/, they must pack with --target cursor.
28
+ _CROSS_TARGET_MAPS: Dict[str, Dict[str, str]] = {
29
+ "claude": {
30
+ ".github/skills/": ".claude/skills/",
31
+ ".github/agents/": ".claude/agents/",
32
+ },
33
+ "vscode": {
34
+ ".claude/skills/": ".github/skills/",
35
+ ".claude/agents/": ".github/agents/",
36
+ },
37
+ "copilot": {
38
+ ".claude/skills/": ".github/skills/",
39
+ ".claude/agents/": ".github/agents/",
40
+ },
41
+ "cursor": {
42
+ ".github/skills/": ".cursor/skills/",
43
+ ".github/agents/": ".cursor/agents/",
44
+ },
45
+ "opencode": {
46
+ ".github/skills/": ".opencode/skills/",
47
+ ".github/agents/": ".opencode/agents/",
48
+ },
49
+ }
50
+
51
+
52
+ def _filter_files_by_target(
53
+ deployed_files: List[str], target: str
54
+ ) -> Tuple[List[str], Dict[str, str]]:
55
+ """Filter deployed file paths by target prefix, with cross-target mapping.
56
+
57
+ When files are deployed under one target prefix (e.g. ``.github/skills/``)
58
+ but the pack target is different (e.g. ``claude``), skills and agents are
59
+ remapped to the equivalent target path. Commands, instructions, and hooks
60
+ are NOT remapped -- they are target-specific.
61
+
62
+ Returns:
63
+ A tuple of ``(filtered_files, path_mappings)`` where *path_mappings*
64
+ maps ``bundle_path -> disk_path`` for any file that was cross-target
65
+ remapped. Direct matches have no entry in the dict.
66
+ """
67
+ prefixes = _TARGET_PREFIXES.get(target, _TARGET_PREFIXES["all"])
68
+ direct = [f for f in deployed_files if any(f.startswith(p) for p in prefixes)]
69
+
70
+ path_mappings: Dict[str, str] = {}
71
+ cross_map = _CROSS_TARGET_MAPS.get(target, {})
72
+ if cross_map:
73
+ direct_set = set(direct)
74
+ for f in deployed_files:
75
+ if f in direct_set:
76
+ continue
77
+ for src_prefix, dst_prefix in cross_map.items():
78
+ if f.startswith(src_prefix):
79
+ mapped = dst_prefix + f[len(src_prefix):]
80
+ if mapped not in direct_set:
81
+ direct.append(mapped)
82
+ direct_set.add(mapped)
83
+ path_mappings[mapped] = f
84
+ break
85
+
86
+ return direct, path_mappings
87
+
88
+
89
+ def enrich_lockfile_for_pack(
90
+ lockfile: LockFile,
91
+ fmt: str,
92
+ target: str,
93
+ ) -> str:
94
+ """Create an enriched copy of the lockfile YAML with a ``pack:`` section.
95
+
96
+ Filters each dependency's ``deployed_files`` to only include paths
97
+ matching the pack *target*, so the bundle lockfile is consistent with
98
+ the files actually shipped in the bundle.
99
+
100
+ Does NOT mutate the original *lockfile* object -- serialises a copy and
101
+ prepends the pack metadata.
102
+
103
+ Args:
104
+ lockfile: The resolved lockfile to enrich.
105
+ fmt: Bundle format (``"apm"`` or ``"plugin"``).
106
+ target: Effective target used for packing (e.g. ``"copilot"``, ``"claude"``,
107
+ ``"all"``). The internal alias ``"vscode"`` is also accepted.
108
+
109
+ Returns:
110
+ A YAML string with the ``pack:`` block followed by the original
111
+ lockfile content.
112
+ """
113
+ import yaml
114
+
115
+ # Build a filtered lockfile YAML: each dep's deployed_files is narrowed
116
+ # to only the paths matching the pack target (with cross-target mapping).
117
+ all_mappings: Dict[str, str] = {}
118
+ data = yaml.safe_load(lockfile.to_yaml())
119
+ if data and "dependencies" in data:
120
+ for dep in data["dependencies"]:
121
+ if "deployed_files" in dep:
122
+ filtered, mappings = _filter_files_by_target(
123
+ dep["deployed_files"], target
124
+ )
125
+ dep["deployed_files"] = filtered
126
+ all_mappings.update(mappings)
127
+
128
+ # Build the pack: metadata section (after filtering so we know if mapping
129
+ # occurred).
130
+ pack_meta: Dict = {
131
+ "format": fmt,
132
+ "target": target,
133
+ "packed_at": datetime.now(timezone.utc).isoformat(),
134
+ }
135
+ if all_mappings:
136
+ # Record the source prefixes that were remapped so consumers know the
137
+ # bundle paths differ from the original lockfile. Use the canonical
138
+ # prefix keys from _CROSS_TARGET_MAPS rather than reverse-engineering
139
+ # them from file paths.
140
+ cross_map = _CROSS_TARGET_MAPS.get(target, {})
141
+ used_src_prefixes = set()
142
+ for original in all_mappings.values():
143
+ for src_prefix in cross_map:
144
+ if original.startswith(src_prefix):
145
+ used_src_prefixes.add(src_prefix)
146
+ break
147
+ pack_meta["mapped_from"] = sorted(used_src_prefixes)
148
+
149
+ from ..utils.yaml_io import yaml_to_str
150
+
151
+ pack_section = yaml_to_str({"pack": pack_meta})
152
+
153
+ lockfile_yaml = yaml_to_str(data)
154
+ return pack_section + lockfile_yaml
@@ -5,23 +5,12 @@ import shutil
5
5
  import tarfile
6
6
  from dataclasses import dataclass, field
7
7
  from pathlib import Path
8
- from typing import List, Optional
8
+ from typing import Dict, List, Optional
9
9
 
10
10
  from ..deps.lockfile import LockFile, get_lockfile_path, migrate_lockfile_if_needed
11
11
  from ..models.apm_package import APMPackage
12
12
  from ..core.target_detection import detect_target
13
- from .lockfile_enrichment import enrich_lockfile_for_pack
14
-
15
-
16
- # Target prefix mapping ("copilot" and "vscode" both map to .github/)
17
- _TARGET_PREFIXES = {
18
- "copilot": [".github/"],
19
- "vscode": [".github/"],
20
- "claude": [".claude/"],
21
- "cursor": [".cursor/"],
22
- "opencode": [".opencode/"],
23
- "all": [".github/", ".claude/", ".cursor/", ".opencode/"],
24
- }
13
+ from .lockfile_enrichment import enrich_lockfile_for_pack, _filter_files_by_target
25
14
 
26
15
 
27
16
  @dataclass
@@ -31,12 +20,8 @@ class PackResult:
31
20
  bundle_path: Path
32
21
  files: List[str] = field(default_factory=list)
33
22
  lockfile_enriched: bool = False
34
-
35
-
36
- def _filter_files_by_target(deployed_files: List[str], target: str) -> List[str]:
37
- """Filter deployed file paths by target prefix."""
38
- prefixes = _TARGET_PREFIXES.get(target, _TARGET_PREFIXES["all"])
39
- return [f for f in deployed_files if any(f.startswith(p) for p in prefixes)]
23
+ mapped_count: int = 0
24
+ path_mappings: Dict[str, str] = field(default_factory=dict)
40
25
 
41
26
 
42
27
  def pack_bundle(
@@ -47,6 +32,7 @@ def pack_bundle(
47
32
  archive: bool = False,
48
33
  dry_run: bool = False,
49
34
  force: bool = False,
35
+ logger=None,
50
36
  ) -> PackResult:
51
37
  """Create a self-contained bundle from installed APM dependencies.
52
38
 
@@ -54,7 +40,7 @@ def pack_bundle(
54
40
  project_root: Root of the project containing ``apm.lock.yaml`` and ``apm.yml``.
55
41
  output_dir: Directory where the bundle will be created.
56
42
  fmt: Bundle format -- ``"apm"`` (default) or ``"plugin"``.
57
- target: Target filter -- ``"vscode"``, ``"claude"``, ``"all"``, or *None*
43
+ target: Target filter -- ``"copilot"``, ``"claude"``, ``"all"``, or *None*
58
44
  (auto-detect from apm.yml / project structure).
59
45
  archive: If *True*, produce a ``.tar.gz`` and remove the directory.
60
46
  dry_run: If *True*, resolve the file list but write nothing to disk.
@@ -81,6 +67,7 @@ def pack_bundle(
81
67
  archive=archive,
82
68
  dry_run=dry_run,
83
69
  force=force,
70
+ logger=logger,
84
71
  )
85
72
 
86
73
  lockfile_path = get_lockfile_path(project_root)
@@ -129,7 +116,7 @@ def pack_bundle(
129
116
  for dep in lockfile.get_all_dependencies():
130
117
  all_deployed.extend(dep.deployed_files)
131
118
 
132
- filtered_files = _filter_files_by_target(all_deployed, effective_target)
119
+ filtered_files, path_mappings = _filter_files_by_target(all_deployed, effective_target)
133
120
  # Deduplicate while preserving order
134
121
  seen = set()
135
122
  unique_files: List[str] = []
@@ -148,14 +135,16 @@ def pack_bundle(
148
135
  raise ValueError(
149
136
  f"Refusing to pack unsafe path from lockfile: {rel_path!r}"
150
137
  )
151
- abs_path = project_root / rel_path
138
+ # For cross-target mapped files, verify the original (on-disk) path
139
+ disk_path = path_mappings.get(rel_path, rel_path)
140
+ abs_path = project_root / disk_path
152
141
  if not abs_path.resolve().is_relative_to(project_root_resolved):
153
142
  raise ValueError(
154
- f"Refusing to pack path that escapes project root: {rel_path!r}"
143
+ f"Refusing to pack path that escapes project root: {disk_path!r}"
155
144
  )
156
145
  # deployed_files may reference directories (ending with /)
157
146
  if not abs_path.exists():
158
- missing.append(rel_path)
147
+ missing.append(disk_path)
159
148
  if missing:
160
149
  raise ValueError(
161
150
  f"The following deployed files are missing on disk -- "
@@ -170,6 +159,8 @@ def pack_bundle(
170
159
  bundle_path=bundle_dir,
171
160
  files=unique_files,
172
161
  lockfile_enriched=True,
162
+ mapped_count=len(path_mappings),
163
+ path_mappings=path_mappings,
173
164
  )
174
165
 
175
166
  # 5b. Scan files for hidden characters before bundling.
@@ -183,7 +174,8 @@ def pack_bundle(
183
174
 
184
175
  _scan_findings_total = 0
185
176
  for rel_path in unique_files:
186
- src = project_root / rel_path
177
+ disk_path = path_mappings.get(rel_path, rel_path)
178
+ src = project_root / disk_path
187
179
  if src.is_symlink():
188
180
  continue
189
181
  if src.is_dir():
@@ -196,21 +188,33 @@ def pack_bundle(
196
188
  )
197
189
  _scan_findings_total += len(verdict.all_findings)
198
190
  if _scan_findings_total:
199
- _rich_warning(
191
+ _warn_msg = (
200
192
  f"Bundle contains {_scan_findings_total} hidden character(s) across source files "
201
193
  f"— run 'apm audit' to inspect before publishing"
202
194
  )
195
+ if logger:
196
+ logger.warning(_warn_msg)
197
+ else:
198
+ _rich_warning(_warn_msg)
203
199
 
204
200
  # 6. Build output directory
205
201
  bundle_dir = output_dir / f"{pkg_name}-{pkg_version}"
206
202
  bundle_dir.mkdir(parents=True, exist_ok=True)
203
+ bundle_dir_resolved = bundle_dir.resolve()
207
204
 
208
205
  # 7. Copy files preserving directory structure
209
206
  for rel_path in unique_files:
210
- src = project_root / rel_path
207
+ # For cross-target mapped files, read from the original disk path
208
+ disk_path = path_mappings.get(rel_path, rel_path)
209
+ src = project_root / disk_path
211
210
  if src.is_symlink():
212
211
  continue # Never bundle symlinks
213
212
  dest = bundle_dir / rel_path
213
+ # Defense-in-depth: verify mapped destination stays inside the bundle
214
+ if not dest.resolve().is_relative_to(bundle_dir_resolved):
215
+ raise ValueError(
216
+ f"Refusing to write outside bundle directory: {rel_path!r}"
217
+ )
214
218
  if src.is_dir():
215
219
  from ..security.gate import ignore_symlinks
216
220
  shutil.copytree(src, dest, dirs_exist_ok=True, ignore=ignore_symlinks)
@@ -226,6 +230,8 @@ def pack_bundle(
226
230
  bundle_path=bundle_dir,
227
231
  files=unique_files,
228
232
  lockfile_enriched=True,
233
+ mapped_count=len(path_mappings),
234
+ path_mappings=path_mappings,
229
235
  )
230
236
 
231
237
  # 10. Archive if requested
@@ -285,7 +285,8 @@ def _get_dev_dependency_urls(apm_yml_path: Path) -> Set[Tuple[str, str]]:
285
285
  ``github/awesome-copilot``).
286
286
  """
287
287
  try:
288
- data = yaml.safe_load(apm_yml_path.read_text(encoding="utf-8"))
288
+ from ..utils.yaml_io import load_yaml
289
+ data = load_yaml(apm_yml_path)
289
290
  except (yaml.YAMLError, OSError, ValueError):
290
291
  return set()
291
292
  if not isinstance(data, dict):
@@ -319,7 +320,7 @@ def _get_dev_dependency_urls(apm_yml_path: Path) -> Set[Tuple[str, str]]:
319
320
 
320
321
 
321
322
  def _find_or_synthesize_plugin_json(
322
- project_root: Path, apm_yml_path: Path
323
+ project_root: Path, apm_yml_path: Path, logger=None,
323
324
  ) -> dict:
324
325
  """Locate an existing ``plugin.json`` or synthesise one from ``apm.yml``."""
325
326
  from ..deps.plugin_parser import synthesize_plugin_json_from_apm_yml
@@ -330,16 +331,24 @@ def _find_or_synthesize_plugin_json(
330
331
  try:
331
332
  return json.loads(plugin_json_path.read_text(encoding="utf-8"))
332
333
  except (json.JSONDecodeError, OSError) as exc:
333
- _rich_warning(
334
+ _warn_msg = (
334
335
  f"Found plugin.json at {plugin_json_path} but could not parse it: {exc}. "
335
336
  "Falling back to synthesis from apm.yml."
336
337
  )
338
+ if logger:
339
+ logger.warning(_warn_msg)
340
+ else:
341
+ _rich_warning(_warn_msg)
337
342
 
338
343
  else:
339
- _rich_warning(
344
+ _warn_msg = (
340
345
  "No plugin.json found. Synthesizing from apm.yml. "
341
346
  "Consider running 'apm init --plugin'."
342
347
  )
348
+ if logger:
349
+ logger.warning(_warn_msg)
350
+ else:
351
+ _rich_warning(_warn_msg)
343
352
  return synthesize_plugin_json_from_apm_yml(apm_yml_path)
344
353
 
345
354
 
@@ -400,6 +409,7 @@ def export_plugin_bundle(
400
409
  archive: bool = False,
401
410
  dry_run: bool = False,
402
411
  force: bool = False,
412
+ logger=None,
403
413
  ) -> PackResult:
404
414
  """Export the project as a plugin-native directory.
405
415
 
@@ -439,7 +449,7 @@ def export_plugin_bundle(
439
449
  )
440
450
 
441
451
  # 3. Find or synthesize plugin.json
442
- plugin_json = _find_or_synthesize_plugin_json(project_root, apm_yml_path)
452
+ plugin_json = _find_or_synthesize_plugin_json(project_root, apm_yml_path, logger=logger)
443
453
 
444
454
  # 4. devDependencies filtering
445
455
  dev_dep_urls = _get_dev_dependency_urls(apm_yml_path)
@@ -510,7 +520,10 @@ def export_plugin_bundle(
510
520
 
511
521
  # 7. Emit collision warnings
512
522
  for msg in collisions:
513
- _rich_warning(msg)
523
+ if logger:
524
+ logger.warning(msg)
525
+ else:
526
+ _rich_warning(msg)
514
527
 
515
528
  # 8. Build output file list (sorted for determinism)
516
529
  output_files = sorted(file_map.keys())
@@ -548,10 +561,14 @@ def export_plugin_bundle(
548
561
  verdict = SecurityGate.scan_text(text, str(src), policy=WARN_POLICY)
549
562
  scan_findings_total += len(verdict.all_findings)
550
563
  if scan_findings_total:
551
- _rich_warning(
564
+ _warn_msg = (
552
565
  f"Bundle contains {scan_findings_total} hidden character(s) across "
553
566
  f"source files — run 'apm audit' to inspect before publishing"
554
567
  )
568
+ if logger:
569
+ logger.warning(_warn_msg)
570
+ else:
571
+ _rich_warning(_warn_msg)
555
572
 
556
573
  # 11. Write files to output directory (clean slate to prevent symlink attacks)
557
574
  if bundle_dir.exists():
@@ -22,6 +22,7 @@ class UnpackResult:
22
22
  skipped_count: int = 0
23
23
  security_warnings: int = 0
24
24
  security_critical: int = 0
25
+ pack_meta: Dict = field(default_factory=dict)
25
26
 
26
27
 
27
28
  def unpack_bundle(
@@ -98,6 +99,18 @@ def unpack_bundle(
98
99
  legacy_lockfile_path = source_dir / LEGACY_LOCKFILE_NAME
99
100
  if legacy_lockfile_path.exists():
100
101
  lockfile_path = legacy_lockfile_path
102
+
103
+ # Extract pack: metadata (written by apm pack) before structured parse
104
+ pack_meta: Dict = {}
105
+ try:
106
+ import yaml
107
+ raw = yaml.safe_load(lockfile_path.read_text(encoding="utf-8"))
108
+ if isinstance(raw, dict):
109
+ val = raw.get("pack", {})
110
+ pack_meta = val if isinstance(val, dict) else {}
111
+ except Exception:
112
+ pass # non-critical -- proceed without metadata
113
+
101
114
  lockfile = LockFile.read(lockfile_path)
102
115
  if lockfile is None:
103
116
  if not lockfile_path.exists():
@@ -176,6 +189,7 @@ def unpack_bundle(
176
189
  dependency_files=dep_file_map,
177
190
  security_warnings=security_warnings,
178
191
  security_critical=security_critical,
192
+ pack_meta=pack_meta,
179
193
  )
180
194
 
181
195
  # 4. Copy target files to output_dir (additive, no deletes)
@@ -219,6 +233,7 @@ def unpack_bundle(
219
233
  skipped_count=skipped,
220
234
  security_warnings=security_warnings,
221
235
  security_critical=security_critical,
236
+ pack_meta=pack_meta,
222
237
  )
223
238
  finally:
224
239
  # Clean up temp dir if we created one