apm-cli 0.12.0__tar.gz → 0.12.2__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 (299) hide show
  1. {apm_cli-0.12.0 → apm_cli-0.12.2}/NOTICE +37 -0
  2. {apm_cli-0.12.0/src/apm_cli.egg-info → apm_cli-0.12.2}/PKG-INFO +3 -1
  3. {apm_cli-0.12.0 → apm_cli-0.12.2}/README.md +1 -0
  4. {apm_cli-0.12.0 → apm_cli-0.12.2}/pyproject.toml +2 -1
  5. apm_cli-0.12.2/src/apm_cli/cache/__init__.py +16 -0
  6. apm_cli-0.12.2/src/apm_cli/cache/git_cache.py +577 -0
  7. apm_cli-0.12.2/src/apm_cli/cache/http_cache.py +358 -0
  8. apm_cli-0.12.2/src/apm_cli/cache/integrity.py +104 -0
  9. apm_cli-0.12.2/src/apm_cli/cache/locking.py +151 -0
  10. apm_cli-0.12.2/src/apm_cli/cache/paths.py +169 -0
  11. apm_cli-0.12.2/src/apm_cli/cache/url_normalize.py +130 -0
  12. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/cli.py +2 -0
  13. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/audit.py +144 -17
  14. apm_cli-0.12.2/src/apm_cli/commands/cache.py +137 -0
  15. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/install.py +50 -30
  16. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/core/command_logger.py +68 -3
  17. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/core/null_logger.py +12 -0
  18. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/deps/apm_resolver.py +218 -71
  19. apm_cli-0.12.2/src/apm_cli/deps/bare_cache.py +545 -0
  20. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/deps/github_downloader.py +473 -170
  21. apm_cli-0.12.2/src/apm_cli/deps/shared_clone_cache.py +157 -0
  22. apm_cli-0.12.2/src/apm_cli/install/cache_pin.py +233 -0
  23. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/install/context.py +11 -0
  24. apm_cli-0.12.2/src/apm_cli/install/drift.py +731 -0
  25. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/install/phases/download.py +28 -41
  26. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/install/phases/finalize.py +34 -5
  27. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/install/phases/integrate.py +85 -92
  28. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/install/phases/lockfile.py +57 -0
  29. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/install/phases/resolve.py +92 -9
  30. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/install/pipeline.py +58 -9
  31. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/install/services.py +138 -38
  32. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/install/sources.py +51 -17
  33. apm_cli-0.12.2/src/apm_cli/install/summary.py +73 -0
  34. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/integration/base_integrator.py +28 -0
  35. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/integration/command_integrator.py +244 -14
  36. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/integration/mcp_integrator.py +4 -1
  37. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/integration/targets.py +12 -0
  38. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/policy/ci_checks.py +94 -1
  39. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/registry/client.py +127 -9
  40. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/registry/operations.py +47 -41
  41. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/utils/console.py +1 -1
  42. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/utils/content_hash.py +11 -0
  43. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/utils/diagnostics.py +84 -32
  44. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/utils/file_ops.py +32 -1
  45. apm_cli-0.12.2/src/apm_cli/utils/git_env.py +97 -0
  46. apm_cli-0.12.2/src/apm_cli/utils/guards.py +123 -0
  47. apm_cli-0.12.2/src/apm_cli/utils/install_tui.py +365 -0
  48. apm_cli-0.12.2/src/apm_cli/utils/normalization.py +57 -0
  49. apm_cli-0.12.2/src/apm_cli/utils/reflink.py +281 -0
  50. apm_cli-0.12.2/src/apm_cli/utils/short_sha.py +45 -0
  51. {apm_cli-0.12.0 → apm_cli-0.12.2/src/apm_cli.egg-info}/PKG-INFO +3 -1
  52. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli.egg-info/SOURCES.txt +19 -0
  53. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli.egg-info/requires.txt +1 -0
  54. {apm_cli-0.12.0 → apm_cli-0.12.2}/AUTHORS +0 -0
  55. {apm_cli-0.12.0 → apm_cli-0.12.2}/LICENSE +0 -0
  56. {apm_cli-0.12.0 → apm_cli-0.12.2}/setup.cfg +0 -0
  57. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/__init__.py +0 -0
  58. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/adapters/__init__.py +0 -0
  59. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/adapters/client/__init__.py +0 -0
  60. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/adapters/client/base.py +0 -0
  61. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/adapters/client/claude.py +0 -0
  62. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/adapters/client/codex.py +0 -0
  63. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/adapters/client/copilot.py +0 -0
  64. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/adapters/client/cursor.py +0 -0
  65. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/adapters/client/gemini.py +0 -0
  66. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/adapters/client/opencode.py +0 -0
  67. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/adapters/client/vscode.py +0 -0
  68. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/adapters/client/windsurf.py +0 -0
  69. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/adapters/package_manager/__init__.py +0 -0
  70. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/adapters/package_manager/base.py +0 -0
  71. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/adapters/package_manager/default_manager.py +0 -0
  72. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/bundle/__init__.py +0 -0
  73. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/bundle/local_bundle.py +0 -0
  74. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/bundle/lockfile_enrichment.py +0 -0
  75. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/bundle/packer.py +0 -0
  76. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/bundle/plugin_exporter.py +0 -0
  77. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/bundle/unpacker.py +0 -0
  78. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/__init__.py +0 -0
  79. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/_apm_yml_writer.py +0 -0
  80. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/_helpers.py +0 -0
  81. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/compile/__init__.py +0 -0
  82. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/compile/cli.py +0 -0
  83. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/compile/watcher.py +0 -0
  84. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/config.py +0 -0
  85. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/deps/__init__.py +0 -0
  86. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/deps/_utils.py +0 -0
  87. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/deps/cli.py +0 -0
  88. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/experimental.py +0 -0
  89. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/init.py +0 -0
  90. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/list_cmd.py +0 -0
  91. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/marketplace/__init__.py +0 -0
  92. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/marketplace/check.py +0 -0
  93. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/marketplace/doctor.py +0 -0
  94. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/marketplace/init.py +0 -0
  95. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/marketplace/migrate.py +0 -0
  96. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/marketplace/outdated.py +0 -0
  97. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/marketplace/plugin/__init__.py +0 -0
  98. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/marketplace/plugin/add.py +0 -0
  99. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/marketplace/plugin/remove.py +0 -0
  100. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/marketplace/plugin/set.py +0 -0
  101. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/marketplace/publish.py +0 -0
  102. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/marketplace/validate.py +0 -0
  103. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/mcp.py +0 -0
  104. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/outdated.py +0 -0
  105. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/pack.py +0 -0
  106. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/policy.py +0 -0
  107. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/prune.py +0 -0
  108. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/run.py +0 -0
  109. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/runtime.py +0 -0
  110. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/uninstall/__init__.py +0 -0
  111. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/uninstall/cli.py +0 -0
  112. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/uninstall/engine.py +0 -0
  113. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/update.py +0 -0
  114. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/commands/view.py +0 -0
  115. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/compilation/__init__.py +0 -0
  116. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/compilation/agents_compiler.py +0 -0
  117. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/compilation/build_id.py +0 -0
  118. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/compilation/claude_formatter.py +0 -0
  119. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/compilation/constants.py +0 -0
  120. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/compilation/constitution.py +0 -0
  121. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/compilation/constitution_block.py +0 -0
  122. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/compilation/context_optimizer.py +0 -0
  123. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/compilation/distributed_compiler.py +0 -0
  124. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/compilation/gemini_formatter.py +0 -0
  125. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/compilation/injector.py +0 -0
  126. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/compilation/link_resolver.py +0 -0
  127. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/compilation/output_writer.py +0 -0
  128. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/compilation/template_builder.py +0 -0
  129. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/config.py +0 -0
  130. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/constants.py +0 -0
  131. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/core/__init__.py +0 -0
  132. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/core/auth.py +0 -0
  133. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/core/azure_cli.py +0 -0
  134. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/core/build_orchestrator.py +0 -0
  135. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/core/conflict_detector.py +0 -0
  136. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/core/docker_args.py +0 -0
  137. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/core/experimental.py +0 -0
  138. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/core/operations.py +0 -0
  139. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/core/safe_installer.py +0 -0
  140. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/core/scope.py +0 -0
  141. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/core/script_runner.py +0 -0
  142. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/core/target_detection.py +0 -0
  143. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/core/token_manager.py +0 -0
  144. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/deps/__init__.py +0 -0
  145. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/deps/aggregator.py +0 -0
  146. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/deps/artifactory_entry.py +0 -0
  147. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/deps/dependency_graph.py +0 -0
  148. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/deps/download_strategies.py +0 -0
  149. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/deps/git_remote_ops.py +0 -0
  150. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/deps/github_downloader_validation.py +0 -0
  151. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/deps/installed_package.py +0 -0
  152. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/deps/lockfile.py +0 -0
  153. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/deps/package_validator.py +0 -0
  154. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/deps/plugin_parser.py +0 -0
  155. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/deps/registry_proxy.py +0 -0
  156. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/deps/transport_selection.py +0 -0
  157. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/deps/verifier.py +0 -0
  158. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/drift.py +0 -0
  159. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/factory.py +0 -0
  160. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/install/__init__.py +0 -0
  161. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/install/errors.py +0 -0
  162. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/install/helpers/__init__.py +0 -0
  163. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/install/helpers/security_scan.py +0 -0
  164. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/install/insecure_policy.py +0 -0
  165. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/install/local_bundle_handler.py +0 -0
  166. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/install/mcp/__init__.py +0 -0
  167. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/install/mcp/args.py +0 -0
  168. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/install/mcp/command.py +0 -0
  169. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/install/mcp/conflicts.py +0 -0
  170. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/install/mcp/entry.py +0 -0
  171. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/install/mcp/registry.py +0 -0
  172. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/install/mcp/warnings.py +0 -0
  173. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/install/mcp/writer.py +0 -0
  174. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/install/phases/__init__.py +0 -0
  175. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/install/phases/cleanup.py +0 -0
  176. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/install/phases/local_content.py +0 -0
  177. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/install/phases/policy_gate.py +0 -0
  178. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/install/phases/policy_target_check.py +0 -0
  179. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/install/phases/post_deps_local.py +0 -0
  180. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/install/phases/targets.py +0 -0
  181. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/install/presentation/__init__.py +0 -0
  182. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/install/presentation/dry_run.py +0 -0
  183. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/install/request.py +0 -0
  184. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/install/service.py +0 -0
  185. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/install/skill_path_migration.py +0 -0
  186. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/install/template.py +0 -0
  187. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/install/validation.py +0 -0
  188. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/integration/__init__.py +0 -0
  189. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/integration/agent_integrator.py +0 -0
  190. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/integration/cleanup.py +0 -0
  191. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/integration/copilot_cowork_paths.py +0 -0
  192. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/integration/coverage.py +0 -0
  193. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/integration/dispatch.py +0 -0
  194. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/integration/hook_integrator.py +0 -0
  195. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/integration/instruction_integrator.py +0 -0
  196. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/integration/prompt_integrator.py +0 -0
  197. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/integration/skill_integrator.py +0 -0
  198. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/integration/skill_transformer.py +0 -0
  199. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/integration/utils.py +0 -0
  200. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/marketplace/__init__.py +0 -0
  201. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/marketplace/_git_utils.py +0 -0
  202. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/marketplace/_io.py +0 -0
  203. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/marketplace/builder.py +0 -0
  204. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/marketplace/client.py +0 -0
  205. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/marketplace/errors.py +0 -0
  206. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/marketplace/git_stderr.py +0 -0
  207. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/marketplace/init_template.py +0 -0
  208. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/marketplace/migration.py +0 -0
  209. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/marketplace/models.py +0 -0
  210. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/marketplace/pr_integration.py +0 -0
  211. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/marketplace/publisher.py +0 -0
  212. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/marketplace/ref_resolver.py +0 -0
  213. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/marketplace/registry.py +0 -0
  214. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/marketplace/resolver.py +0 -0
  215. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/marketplace/semver.py +0 -0
  216. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/marketplace/shadow_detector.py +0 -0
  217. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/marketplace/tag_pattern.py +0 -0
  218. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/marketplace/validator.py +0 -0
  219. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/marketplace/version_pins.py +0 -0
  220. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/marketplace/yml_editor.py +0 -0
  221. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/marketplace/yml_schema.py +0 -0
  222. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/models/__init__.py +0 -0
  223. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/models/apm_package.py +0 -0
  224. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/models/dependency/__init__.py +0 -0
  225. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/models/dependency/mcp.py +0 -0
  226. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/models/dependency/reference.py +0 -0
  227. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/models/dependency/types.py +0 -0
  228. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/models/plugin.py +0 -0
  229. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/models/results.py +0 -0
  230. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/models/validation.py +0 -0
  231. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/output/__init__.py +0 -0
  232. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/output/formatters.py +0 -0
  233. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/output/models.py +0 -0
  234. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/output/script_formatters.py +0 -0
  235. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/policy/__init__.py +0 -0
  236. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/policy/_help_text.py +0 -0
  237. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/policy/discovery.py +0 -0
  238. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/policy/inheritance.py +0 -0
  239. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/policy/install_preflight.py +0 -0
  240. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/policy/matcher.py +0 -0
  241. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/policy/models.py +0 -0
  242. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/policy/outcome_routing.py +0 -0
  243. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/policy/parser.py +0 -0
  244. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/policy/policy_checks.py +0 -0
  245. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/policy/project_config.py +0 -0
  246. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/policy/schema.py +0 -0
  247. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/primitives/__init__.py +0 -0
  248. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/primitives/discovery.py +0 -0
  249. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/primitives/models.py +0 -0
  250. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/primitives/parser.py +0 -0
  251. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/registry/__init__.py +0 -0
  252. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/registry/integration.py +0 -0
  253. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/runtime/__init__.py +0 -0
  254. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/runtime/base.py +0 -0
  255. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/runtime/codex_runtime.py +0 -0
  256. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/runtime/copilot_runtime.py +0 -0
  257. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/runtime/factory.py +0 -0
  258. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/runtime/llm_runtime.py +0 -0
  259. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/runtime/manager.py +0 -0
  260. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/security/__init__.py +0 -0
  261. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/security/audit_report.py +0 -0
  262. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/security/content_scanner.py +0 -0
  263. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/security/file_scanner.py +0 -0
  264. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/security/gate.py +0 -0
  265. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/update_policy.py +0 -0
  266. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/utils/__init__.py +0 -0
  267. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/utils/atomic_io.py +0 -0
  268. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/utils/exclude.py +0 -0
  269. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/utils/github_host.py +0 -0
  270. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/utils/helpers.py +0 -0
  271. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/utils/path_security.py +0 -0
  272. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/utils/paths.py +0 -0
  273. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/utils/subprocess_env.py +0 -0
  274. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/utils/version_checker.py +0 -0
  275. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/utils/yaml_io.py +0 -0
  276. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/version.py +0 -0
  277. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/workflow/__init__.py +0 -0
  278. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/workflow/discovery.py +0 -0
  279. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/workflow/parser.py +0 -0
  280. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli/workflow/runner.py +0 -0
  281. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli.egg-info/dependency_links.txt +0 -0
  282. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli.egg-info/entry_points.txt +0 -0
  283. {apm_cli-0.12.0 → apm_cli-0.12.2}/src/apm_cli.egg-info/top_level.txt +0 -0
  284. {apm_cli-0.12.0 → apm_cli-0.12.2}/tests/test_apm_package_models.py +0 -0
  285. {apm_cli-0.12.0 → apm_cli-0.12.2}/tests/test_apm_resolver.py +0 -0
  286. {apm_cli-0.12.0 → apm_cli-0.12.2}/tests/test_codex_docker_args_fix.py +0 -0
  287. {apm_cli-0.12.0 → apm_cli-0.12.2}/tests/test_codex_empty_string_and_defaults.py +0 -0
  288. {apm_cli-0.12.0 → apm_cli-0.12.2}/tests/test_collision_integration.py +0 -0
  289. {apm_cli-0.12.0 → apm_cli-0.12.2}/tests/test_console.py +0 -0
  290. {apm_cli-0.12.0 → apm_cli-0.12.2}/tests/test_distributed_compilation.py +0 -0
  291. {apm_cli-0.12.0 → apm_cli-0.12.2}/tests/test_empty_string_and_defaults.py +0 -0
  292. {apm_cli-0.12.0 → apm_cli-0.12.2}/tests/test_enhanced_discovery.py +0 -0
  293. {apm_cli-0.12.0 → apm_cli-0.12.2}/tests/test_github_downloader.py +0 -0
  294. {apm_cli-0.12.0 → apm_cli-0.12.2}/tests/test_github_downloader_token_precedence.py +0 -0
  295. {apm_cli-0.12.0 → apm_cli-0.12.2}/tests/test_lockfile.py +0 -0
  296. {apm_cli-0.12.0 → apm_cli-0.12.2}/tests/test_runnable_prompts.py +0 -0
  297. {apm_cli-0.12.0 → apm_cli-0.12.2}/tests/test_runtime_manager_token_precedence.py +0 -0
  298. {apm_cli-0.12.0 → apm_cli-0.12.2}/tests/test_token_manager.py +0 -0
  299. {apm_cli-0.12.0 → apm_cli-0.12.2}/tests/test_virtual_package_multi_install.py +0 -0
@@ -1215,6 +1215,43 @@ _Copyright (c) 2014-2026 Anthon van der Neut, Ruamel bvba_
1215
1215
 
1216
1216
  ---
1217
1217
 
1218
+ ## Component. filelock
1219
+
1220
+ - Version requirement: `>=3.12`
1221
+ - Upstream: https://github.com/tox-dev/filelock
1222
+ - SPDX: `Unlicense`
1223
+ - Notes: Used for cross-process file-based locks in the persistent install cache.
1224
+
1225
+ ### Open Source License/Copyright Notice.
1226
+
1227
+ _Released into the public domain via The Unlicense (no copyright claimed by upstream)._
1228
+
1229
+ ```
1230
+ MIT License
1231
+
1232
+ Copyright (c) 2025 Bernát Gábor and contributors
1233
+
1234
+ Permission is hereby granted, free of charge, to any person obtaining a copy
1235
+ of this software and associated documentation files (the "Software"), to deal
1236
+ in the Software without restriction, including without limitation the rights
1237
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1238
+ copies of the Software, and to permit persons to whom the Software is
1239
+ furnished to do so, subject to the following conditions:
1240
+
1241
+ The above copyright notice and this permission notice shall be included in all
1242
+ copies or substantial portions of the Software.
1243
+
1244
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1245
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1246
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1247
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1248
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1249
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1250
+ SOFTWARE.
1251
+ ```
1252
+
1253
+ ---
1254
+
1218
1255
  Submitted on behalf of a third-party
1219
1256
 
1220
1257
  The contributions below are identified as submitted on behalf of a
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: apm-cli
3
- Version: 0.12.0
3
+ Version: 0.12.2
4
4
  Summary: MCP configuration tool
5
5
  Author-email: Daniel Meppiel <user@example.com>
6
6
  License: MIT License
@@ -51,6 +51,7 @@ Requires-Dist: rich-click>=1.7.0
51
51
  Requires-Dist: watchdog>=3.0.0
52
52
  Requires-Dist: GitPython>=3.1.0
53
53
  Requires-Dist: ruamel.yaml>=0.18.0
54
+ Requires-Dist: filelock>=3.12
54
55
  Provides-Extra: dev
55
56
  Requires-Dist: pytest>=7.0.0; extra == "dev"
56
57
  Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
@@ -145,6 +146,7 @@ Agent context is executable in effect — a prompt is a program for an LLM. APM
145
146
 
146
147
  - **[Content security](https://microsoft.github.io/apm/enterprise/security/)** — `apm install` blocks compromised packages before agents read them; `apm audit` runs the same checks on demand
147
148
  - **[Lockfile integrity](https://microsoft.github.io/apm/enterprise/governance/)** — `apm.lock` records resolved sources and content hashes for full provenance
149
+ - **[Drift detection](https://microsoft.github.io/apm/guides/drift-detection/)** — `apm audit` rebuilds your agent context in scratch and diffs it against your working tree to catch hand-edits before they ship
148
150
  - **[MCP trust boundaries](https://microsoft.github.io/apm/guides/mcp-servers/)** — transitive MCP servers require explicit consent
149
151
 
150
152
  ### 3. Governed by policy
@@ -81,6 +81,7 @@ Agent context is executable in effect — a prompt is a program for an LLM. APM
81
81
 
82
82
  - **[Content security](https://microsoft.github.io/apm/enterprise/security/)** — `apm install` blocks compromised packages before agents read them; `apm audit` runs the same checks on demand
83
83
  - **[Lockfile integrity](https://microsoft.github.io/apm/enterprise/governance/)** — `apm.lock` records resolved sources and content hashes for full provenance
84
+ - **[Drift detection](https://microsoft.github.io/apm/guides/drift-detection/)** — `apm audit` rebuilds your agent context in scratch and diffs it against your working tree to catch hand-edits before they ship
84
85
  - **[MCP trust boundaries](https://microsoft.github.io/apm/guides/mcp-servers/)** — transitive MCP servers require explicit consent
85
86
 
86
87
  ### 3. Governed by policy
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "apm-cli"
7
- version = "0.12.0"
7
+ version = "0.12.2"
8
8
  description = "MCP configuration tool"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -36,6 +36,7 @@ dependencies = [
36
36
  "watchdog>=3.0.0",
37
37
  "GitPython>=3.1.0",
38
38
  "ruamel.yaml>=0.18.0",
39
+ "filelock>=3.12",
39
40
  ]
40
41
 
41
42
  [project.optional-dependencies]
@@ -0,0 +1,16 @@
1
+ """Persistent content-addressable cache for APM install.
2
+
3
+ Public API
4
+ ----------
5
+ - :func:`get_cache_root` -- resolve the platform cache directory
6
+ - :class:`GitCache` -- content-addressable git repository + checkout cache
7
+ - :class:`HttpCache` -- HTTP response cache with conditional revalidation
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from .git_cache import GitCache
13
+ from .http_cache import HttpCache
14
+ from .paths import get_cache_root
15
+
16
+ __all__ = ["GitCache", "HttpCache", "get_cache_root"]
@@ -0,0 +1,577 @@
1
+ """Persistent content-addressable git cache.
2
+
3
+ Two-tier structure:
4
+ - ``git/db_v1/<shard>/`` -- bare git repositories (full clones)
5
+ - ``git/checkouts_v1/<shard>/<sha>/`` -- per-SHA working copies
6
+
7
+ Cache keys are derived from normalized repository URLs (see
8
+ :mod:`url_normalize`). Checkouts are keyed by resolved SHA, never
9
+ by mutable ref strings.
10
+
11
+ Resolution flow:
12
+ 1. If lockfile provides SHA for this dep -> use directly
13
+ 2. If ref looks like full SHA (40 hex chars) -> use as-is
14
+ 3. Else ``git ls-remote <url> <ref>`` to resolve ref -> SHA
15
+
16
+ On every cache HIT:
17
+ - Run integrity check (verify HEAD == expected SHA)
18
+ - Mismatch -> evict shard, fall through to fresh fetch, log warning
19
+
20
+ Concurrency:
21
+ - Per-shard file locks (via filelock) for atomic operations
22
+ - Atomic landing protocol for safe concurrent installs
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import contextlib
28
+ import logging
29
+ import os
30
+ import re
31
+ import subprocess
32
+ from pathlib import Path
33
+
34
+ from ..utils.path_security import ensure_path_within
35
+ from .integrity import verify_checkout_sha
36
+ from .locking import atomic_land, cleanup_incomplete, shard_lock, stage_path
37
+ from .paths import get_git_checkouts_path, get_git_db_path
38
+ from .url_normalize import cache_shard_key
39
+
40
+ _log = logging.getLogger(__name__)
41
+
42
+ # Full SHA pattern: 40 hex characters
43
+ _SHA_RE = re.compile(r"^[0-9a-f]{40}$", re.IGNORECASE)
44
+
45
+
46
+ class GitCache:
47
+ """Content-addressable git cache with integrity verification.
48
+
49
+ Args:
50
+ cache_root: Root cache directory (from :func:`get_cache_root`).
51
+ refresh: If True, force revalidation even on cache hit.
52
+ """
53
+
54
+ def __init__(self, cache_root: Path, *, refresh: bool = False) -> None:
55
+ self._cache_root = cache_root
56
+ self._refresh = refresh
57
+ self._db_root = get_git_db_path(cache_root)
58
+ self._checkouts_root = get_git_checkouts_path(cache_root)
59
+
60
+ # Ensure bucket directories exist
61
+ self._db_root.mkdir(parents=True, exist_ok=True)
62
+ self._checkouts_root.mkdir(parents=True, exist_ok=True)
63
+ os.chmod(str(self._db_root), 0o700)
64
+ os.chmod(str(self._checkouts_root), 0o700)
65
+
66
+ # Clean up any stale incomplete operations from previous crashes
67
+ cleanup_incomplete(self._db_root)
68
+ cleanup_incomplete(self._checkouts_root)
69
+
70
+ def get_checkout(
71
+ self,
72
+ url: str,
73
+ ref: str | None,
74
+ *,
75
+ locked_sha: str | None = None,
76
+ env: dict[str, str] | None = None,
77
+ ) -> Path:
78
+ """Return path to a cached checkout for the given repo+ref.
79
+
80
+ Args:
81
+ url: Repository URL (any supported form).
82
+ ref: Git ref (branch, tag, SHA) or None for default branch.
83
+ locked_sha: If provided (from lockfile), skip resolution and
84
+ use this SHA directly.
85
+ env: Environment dict for git subprocesses.
86
+
87
+ Returns:
88
+ Path to the checkout directory (guaranteed to contain valid
89
+ git working copy at the expected SHA).
90
+ """
91
+ shard_key = cache_shard_key(url)
92
+ sha = self._resolve_sha(url, ref, locked_sha=locked_sha, env=env)
93
+
94
+ checkout_dir = self._checkouts_root / shard_key / sha
95
+
96
+ # Cache hit path (skip if refresh requested)
97
+ if not self._refresh and checkout_dir.is_dir():
98
+ if verify_checkout_sha(checkout_dir, sha):
99
+ _log.debug("Cache HIT: %s @ %s", url, sha[:12])
100
+ return checkout_dir
101
+ else:
102
+ # Integrity failure -- evict
103
+ _log.warning(
104
+ "[!] Evicting corrupt cache entry: %s @ %s",
105
+ _sanitize_url(url),
106
+ sha[:12],
107
+ )
108
+ self._evict_checkout(checkout_dir)
109
+
110
+ # Cache miss: ensure we have the bare repo, then create checkout
111
+ self._ensure_bare_repo(url, shard_key, sha, env=env)
112
+ return self._create_checkout(url, shard_key, sha, env=env)
113
+
114
+ def _resolve_sha(
115
+ self,
116
+ url: str,
117
+ ref: str | None,
118
+ *,
119
+ locked_sha: str | None = None,
120
+ env: dict[str, str] | None = None,
121
+ ) -> str:
122
+ """Resolve a ref to a full SHA.
123
+
124
+ Priority:
125
+ 1. locked_sha from lockfile (trusted, no network)
126
+ 2. ref already looks like a full SHA
127
+ 3. git ls-remote to resolve ref -> SHA
128
+ """
129
+ if locked_sha and _SHA_RE.match(locked_sha):
130
+ return locked_sha.lower()
131
+
132
+ if ref and _SHA_RE.match(ref):
133
+ return ref.lower()
134
+
135
+ # Need to resolve via ls-remote
136
+ return self._ls_remote_resolve(url, ref, env=env)
137
+
138
+ def _ls_remote_resolve(
139
+ self,
140
+ url: str,
141
+ ref: str | None,
142
+ *,
143
+ env: dict[str, str] | None = None,
144
+ ) -> str:
145
+ """Resolve a ref to SHA via git ls-remote.
146
+
147
+ Args:
148
+ url: Repository URL.
149
+ ref: Ref to resolve (branch, tag, or None for HEAD).
150
+ env: Environment for subprocess.
151
+
152
+ Returns:
153
+ 40-char lowercase hex SHA.
154
+
155
+ Raises:
156
+ RuntimeError: If resolution fails.
157
+ """
158
+ from ..utils.git_env import get_git_executable, git_subprocess_env
159
+
160
+ git_exe = get_git_executable()
161
+ cmd = [git_exe, "ls-remote", url]
162
+ if ref:
163
+ cmd.append(ref)
164
+
165
+ subprocess_env = env if env is not None else git_subprocess_env()
166
+ try:
167
+ result = subprocess.run(
168
+ cmd,
169
+ capture_output=True,
170
+ text=True,
171
+ timeout=30,
172
+ env=subprocess_env,
173
+ )
174
+ except (subprocess.TimeoutExpired, OSError) as exc:
175
+ raise RuntimeError(
176
+ f"Failed to resolve ref '{ref}' for {_sanitize_url(url)}: {exc}"
177
+ ) from exc
178
+
179
+ if result.returncode != 0:
180
+ raise RuntimeError(
181
+ f"git ls-remote failed for {_sanitize_url(url)}: {result.stderr.strip()}"
182
+ )
183
+
184
+ # Parse ls-remote output: first column is SHA
185
+ for line in result.stdout.strip().splitlines():
186
+ parts = line.split("\t", 1)
187
+ if len(parts) >= 1 and _SHA_RE.match(parts[0]):
188
+ sha = parts[0].lower()
189
+ # If no ref specified, return HEAD (first line)
190
+ if not ref:
191
+ return sha
192
+ # Match exact ref or refs/heads/ref or refs/tags/ref
193
+ if len(parts) == 2:
194
+ remote_ref = parts[1]
195
+ if remote_ref in (
196
+ ref,
197
+ f"refs/heads/{ref}",
198
+ f"refs/tags/{ref}",
199
+ ):
200
+ return sha
201
+ # If we have any SHA from output, use the first one
202
+ for line in result.stdout.strip().splitlines():
203
+ parts = line.split("\t", 1)
204
+ if len(parts) >= 1 and _SHA_RE.match(parts[0]):
205
+ return parts[0].lower()
206
+
207
+ raise RuntimeError(f"Could not resolve ref '{ref}' for {_sanitize_url(url)}")
208
+
209
+ def _ensure_bare_repo(
210
+ self,
211
+ url: str,
212
+ shard_key: str,
213
+ sha: str,
214
+ *,
215
+ env: dict[str, str] | None = None,
216
+ ) -> Path:
217
+ """Ensure a bare repo clone exists for the given shard, fetching if needed.
218
+
219
+ Returns the path to the bare repo directory.
220
+ """
221
+ from ..utils.git_env import get_git_executable, git_subprocess_env
222
+
223
+ bare_dir = self._db_root / shard_key
224
+ # Containment guard: defends against pathological shard_key
225
+ # values bypassing the cache root.
226
+ ensure_path_within(bare_dir, self._db_root)
227
+ lock = shard_lock(bare_dir)
228
+
229
+ # Acquire the shard lock BEFORE the existence probe so that two
230
+ # concurrent processes hitting a cold shard cannot both perform
231
+ # a full network clone (one would lose the atomic_land race
232
+ # later, but only after wasting bandwidth + wall time).
233
+ with lock:
234
+ if bare_dir.is_dir():
235
+ # Repo exists -- check if we have the required SHA
236
+ if self._bare_has_sha(bare_dir, sha, env=env):
237
+ return bare_dir
238
+ # Need to fetch the SHA (lock already held; call the
239
+ # inner helper that does NOT re-acquire).
240
+ self._fetch_into_bare_locked(bare_dir, url, sha, env=env)
241
+ return bare_dir
242
+
243
+ # Cold miss: clone bare repo
244
+ git_exe = get_git_executable()
245
+ staged = stage_path(bare_dir)
246
+ ensure_path_within(staged, self._db_root)
247
+ staged.mkdir(parents=True, exist_ok=True)
248
+ os.chmod(str(staged), 0o700)
249
+
250
+ subprocess_env = env if env is not None else git_subprocess_env()
251
+ try:
252
+ # Full bare clone (no --filter): we extract file contents at
253
+ # checkout time, so all blobs must be present locally. A
254
+ # partial clone would leave the working tree empty after
255
+ # `git clone --local --shared` + `git checkout`, because the
256
+ # alternates pointer would resolve trees but not blobs.
257
+ subprocess.run(
258
+ [git_exe, "clone", "--bare", url, str(staged)],
259
+ capture_output=True,
260
+ text=True,
261
+ timeout=300,
262
+ env=subprocess_env,
263
+ check=True,
264
+ )
265
+ except (subprocess.CalledProcessError, subprocess.TimeoutExpired, OSError) as exc:
266
+ # Clean up staged on failure
267
+ from ..utils.file_ops import robust_rmtree
268
+
269
+ robust_rmtree(staged, ignore_errors=True)
270
+ raise RuntimeError(f"Failed to clone {_sanitize_url(url)}: {exc}") from exc
271
+
272
+ # Atomic land (lock is already held; pass it through so the
273
+ # rename completes under the same critical section).
274
+ if not atomic_land(staged, bare_dir, lock):
275
+ # Another process won between our staging and rename
276
+ # (possible only on lock-acquisition timeout fallthrough);
277
+ # verify it has our SHA.
278
+ if not self._bare_has_sha(bare_dir, sha, env=env):
279
+ self._fetch_into_bare_locked(bare_dir, url, sha, env=env)
280
+
281
+ return bare_dir
282
+
283
+ def _create_checkout(
284
+ self,
285
+ url: str,
286
+ shard_key: str,
287
+ sha: str,
288
+ *,
289
+ env: dict[str, str] | None = None,
290
+ ) -> Path:
291
+ """Create a checkout at the specified SHA from the bare repo.
292
+
293
+ Uses ``git clone --local --shared`` from the bare repo for
294
+ efficiency (no network, hardlinks objects).
295
+
296
+ Concurrency / write-deduplication
297
+ ---------------------------------
298
+ Acquires the shard lock BEFORE staging any work. On lock entry
299
+ we re-probe the final shard and short-circuit if another
300
+ process populated it while we were waiting on the lock. This
301
+ collapses N racing installs of the same SHA from N concurrent
302
+ ``git clone`` operations to ~1: only the lock winner pays the
303
+ clone cost; all losers see a populated shard the moment they
304
+ get the lock and return immediately. Critical for CI matrix
305
+ builds where multiple jobs hit the same uncached repo.
306
+ """
307
+ from ..utils.git_env import get_git_executable, git_subprocess_env
308
+
309
+ bare_dir = self._db_root / shard_key
310
+ checkout_parent = self._checkouts_root / shard_key
311
+ # Containment guards: the shard_key + sha components are
312
+ # derived from sha256 / hex but defend at the boundary anyway.
313
+ ensure_path_within(checkout_parent, self._checkouts_root)
314
+ checkout_parent.mkdir(parents=True, exist_ok=True)
315
+ os.chmod(str(checkout_parent), 0o700)
316
+
317
+ final_dir = checkout_parent / sha
318
+ ensure_path_within(final_dir, self._checkouts_root)
319
+ lock = shard_lock(final_dir)
320
+
321
+ # Acquire the lock BEFORE doing any work so that a concurrent
322
+ # install of the same shard does not duplicate the clone work.
323
+ # The lock winner clones; every other process re-probes after
324
+ # the lock and short-circuits.
325
+ with lock:
326
+ # Write-dedup re-probe: another process may have populated
327
+ # this shard while we were waiting. Verify integrity to
328
+ # rule out a poisoned half-write (atomic_land guards
329
+ # against that, but we re-check defensively).
330
+ if final_dir.is_dir() and verify_checkout_sha(final_dir, sha):
331
+ _log.debug("Write-dedup HIT under lock: %s @ %s", url, sha[:12])
332
+ return final_dir
333
+
334
+ staged = stage_path(final_dir)
335
+ ensure_path_within(staged, self._checkouts_root)
336
+ staged.mkdir(parents=True, exist_ok=True)
337
+ os.chmod(str(staged), 0o700)
338
+
339
+ git_exe = get_git_executable()
340
+ subprocess_env = env if env is not None else git_subprocess_env()
341
+
342
+ try:
343
+ # Clone from local bare repo (fast, no network)
344
+ subprocess.run(
345
+ [
346
+ git_exe,
347
+ "clone",
348
+ "--local",
349
+ "--shared",
350
+ "--no-checkout",
351
+ str(bare_dir),
352
+ str(staged),
353
+ ],
354
+ capture_output=True,
355
+ text=True,
356
+ timeout=60,
357
+ env=subprocess_env,
358
+ check=True,
359
+ )
360
+ # Checkout the specific SHA
361
+ subprocess.run(
362
+ [git_exe, "-C", str(staged), "checkout", sha],
363
+ capture_output=True,
364
+ text=True,
365
+ timeout=60,
366
+ env=subprocess_env,
367
+ check=True,
368
+ )
369
+ except (subprocess.CalledProcessError, subprocess.TimeoutExpired, OSError) as exc:
370
+ from ..utils.file_ops import robust_rmtree
371
+
372
+ robust_rmtree(staged, ignore_errors=True)
373
+ raise RuntimeError(
374
+ f"Failed to create checkout for {_sanitize_url(url)} @ {sha[:12]}: {exc}"
375
+ ) from exc
376
+
377
+ # We hold the shard lock, so atomic_land's re-acquire is a
378
+ # reentrant no-op (filelock supports same-process recursion).
379
+ if not atomic_land(staged, final_dir, lock):
380
+ # Another process landed first between our re-probe and
381
+ # the rename (only possible if our lock dropped, which
382
+ # it didn't); verify integrity defensively.
383
+ if not verify_checkout_sha(final_dir, sha):
384
+ self._evict_checkout(final_dir)
385
+ raise RuntimeError(
386
+ f"Race condition: concurrent checkout failed integrity "
387
+ f"for {_sanitize_url(url)} @ {sha[:12]}"
388
+ )
389
+ return final_dir
390
+
391
+ def _bare_has_sha(self, bare_dir: Path, sha: str, *, env: dict[str, str] | None = None) -> bool:
392
+ """Check if the bare repo contains the specified commit."""
393
+ from ..utils.git_env import get_git_executable, git_subprocess_env
394
+
395
+ git_exe = get_git_executable()
396
+ subprocess_env = env if env is not None else git_subprocess_env()
397
+ try:
398
+ result = subprocess.run(
399
+ [git_exe, "-C", str(bare_dir), "cat-file", "-t", sha],
400
+ capture_output=True,
401
+ text=True,
402
+ timeout=10,
403
+ env=subprocess_env,
404
+ )
405
+ return result.returncode == 0 and "commit" in result.stdout.strip()
406
+ except (subprocess.TimeoutExpired, OSError):
407
+ return False
408
+
409
+ def _fetch_into_bare(
410
+ self,
411
+ bare_dir: Path,
412
+ url: str,
413
+ sha: str,
414
+ *,
415
+ env: dict[str, str] | None = None,
416
+ ) -> None:
417
+ """Fetch a specific SHA into an existing bare repo (acquires lock)."""
418
+ lock = shard_lock(bare_dir)
419
+ with lock:
420
+ if self._bare_has_sha(bare_dir, sha, env=env):
421
+ return
422
+ self._fetch_into_bare_locked(bare_dir, url, sha, env=env)
423
+
424
+ def _fetch_into_bare_locked(
425
+ self,
426
+ bare_dir: Path,
427
+ url: str,
428
+ sha: str,
429
+ *,
430
+ env: dict[str, str] | None = None,
431
+ ) -> None:
432
+ """Fetch a specific SHA into a bare repo. Caller MUST hold the shard lock."""
433
+ from ..utils.git_env import get_git_executable, git_subprocess_env
434
+
435
+ git_exe = get_git_executable()
436
+ subprocess_env = env if env is not None else git_subprocess_env()
437
+ try:
438
+ subprocess.run(
439
+ [git_exe, "-C", str(bare_dir), "fetch", url, sha],
440
+ capture_output=True,
441
+ text=True,
442
+ timeout=120,
443
+ env=subprocess_env,
444
+ check=True,
445
+ )
446
+ except subprocess.CalledProcessError:
447
+ # Some servers don't allow fetching by SHA -- fetch all refs
448
+ subprocess.run(
449
+ [git_exe, "-C", str(bare_dir), "fetch", "--all"],
450
+ capture_output=True,
451
+ text=True,
452
+ timeout=120,
453
+ env=subprocess_env,
454
+ check=True,
455
+ )
456
+
457
+ def _evict_checkout(self, checkout_dir: Path) -> None:
458
+ """Safely remove a corrupt checkout shard."""
459
+ from ..utils.file_ops import robust_rmtree
460
+
461
+ try:
462
+ robust_rmtree(checkout_dir, ignore_errors=True)
463
+ except Exception as exc:
464
+ _log.debug("Failed to evict checkout %s: %s", checkout_dir, exc)
465
+
466
+ def get_cache_stats(self) -> dict[str, int]:
467
+ """Return cache statistics for ``apm cache info``.
468
+
469
+ Returns:
470
+ Dict with keys: db_count, checkout_count, total_size_bytes.
471
+ """
472
+ db_count = 0
473
+ checkout_count = 0
474
+ total_size = 0
475
+
476
+ if self._db_root.is_dir():
477
+ for entry in os.scandir(str(self._db_root)):
478
+ if entry.is_dir(follow_symlinks=False) and not entry.name.endswith(".lock"):
479
+ db_count += 1
480
+ total_size += _dir_size(Path(entry.path))
481
+
482
+ if self._checkouts_root.is_dir():
483
+ for shard_entry in os.scandir(str(self._checkouts_root)):
484
+ if shard_entry.is_dir(follow_symlinks=False):
485
+ for sha_entry in os.scandir(shard_entry.path):
486
+ if sha_entry.is_dir(follow_symlinks=False):
487
+ checkout_count += 1
488
+ total_size += _dir_size(Path(sha_entry.path))
489
+
490
+ return {
491
+ "db_count": db_count,
492
+ "checkout_count": checkout_count,
493
+ "total_size_bytes": total_size,
494
+ }
495
+
496
+ def clean_all(self) -> None:
497
+ """Remove ALL cache content (db + checkouts). Used by ``apm cache clean``."""
498
+ from ..utils.file_ops import robust_rmtree
499
+
500
+ for bucket in (self._db_root, self._checkouts_root):
501
+ if bucket.is_dir():
502
+ for entry in os.scandir(str(bucket)):
503
+ if entry.is_dir(follow_symlinks=False):
504
+ robust_rmtree(Path(entry.path), ignore_errors=True)
505
+ elif entry.is_file(follow_symlinks=False):
506
+ with contextlib.suppress(OSError):
507
+ os.unlink(entry.path)
508
+
509
+ def prune(self, *, max_age_days: int = 30) -> int:
510
+ """Remove checkout entries older than *max_age_days*.
511
+
512
+ Uses mtime of the checkout directory as the access indicator.
513
+
514
+ Returns:
515
+ Number of entries pruned.
516
+ """
517
+ import time
518
+
519
+ from ..utils.file_ops import robust_rmtree
520
+
521
+ cutoff = time.time() - (max_age_days * 86400)
522
+ pruned = 0
523
+
524
+ if not self._checkouts_root.is_dir():
525
+ return 0
526
+
527
+ for shard_entry in os.scandir(str(self._checkouts_root)):
528
+ if not shard_entry.is_dir(follow_symlinks=False):
529
+ continue
530
+ for sha_entry in os.scandir(shard_entry.path):
531
+ if not sha_entry.is_dir(follow_symlinks=False):
532
+ continue
533
+ try:
534
+ stat = sha_entry.stat(follow_symlinks=False)
535
+ if stat.st_mtime < cutoff:
536
+ robust_rmtree(Path(sha_entry.path), ignore_errors=True)
537
+ pruned += 1
538
+ except OSError:
539
+ continue
540
+
541
+ return pruned
542
+
543
+
544
+ def _dir_size(path: Path) -> int:
545
+ """Calculate total size of a directory (non-recursive symlink-safe)."""
546
+ total = 0
547
+ try:
548
+ for root, _dirs, files in os.walk(str(path)):
549
+ for f in files:
550
+ fp = os.path.join(root, f)
551
+ try:
552
+ st = os.lstat(fp)
553
+ total += st.st_size
554
+ except OSError:
555
+ pass
556
+ except OSError:
557
+ pass
558
+ return total
559
+
560
+
561
+ def _sanitize_url(url: str) -> str:
562
+ """Strip credentials from URL for safe logging."""
563
+ import urllib.parse
564
+
565
+ try:
566
+ parsed = urllib.parse.urlparse(url)
567
+ if parsed.password:
568
+ # Replace password with ***
569
+ netloc = parsed.hostname or ""
570
+ if parsed.username:
571
+ netloc = f"{parsed.username}:***@{netloc}"
572
+ if parsed.port:
573
+ netloc = f"{netloc}:{parsed.port}"
574
+ return urllib.parse.urlunparse(parsed._replace(netloc=netloc))
575
+ except Exception:
576
+ pass
577
+ return url