apm-cli 0.7.8__tar.gz → 0.8.0__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 (142) hide show
  1. {apm_cli-0.7.8/src/apm_cli.egg-info → apm_cli-0.8.0}/PKG-INFO +27 -4
  2. {apm_cli-0.7.8 → apm_cli-0.8.0}/README.md +25 -2
  3. {apm_cli-0.7.8 → apm_cli-0.8.0}/pyproject.toml +2 -2
  4. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/adapters/client/codex.py +3 -3
  5. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/adapters/client/copilot.py +2 -2
  6. apm_cli-0.8.0/src/apm_cli/adapters/client/cursor.py +138 -0
  7. apm_cli-0.8.0/src/apm_cli/adapters/client/opencode.py +157 -0
  8. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/adapters/client/vscode.py +1 -1
  9. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/bundle/lockfile_enrichment.py +1 -1
  10. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/bundle/packer.py +58 -14
  11. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/bundle/unpacker.py +15 -10
  12. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/cli.py +48 -1
  13. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/commands/_helpers.py +4 -4
  14. apm_cli-0.8.0/src/apm_cli/commands/audit.py +504 -0
  15. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/commands/compile.py +57 -44
  16. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/commands/deps.py +38 -38
  17. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/commands/init.py +5 -5
  18. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/commands/install.py +710 -21
  19. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/commands/list_cmd.py +2 -2
  20. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/commands/mcp.py +19 -19
  21. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/commands/pack.py +5 -5
  22. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/commands/prune.py +7 -8
  23. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/commands/run.py +6 -6
  24. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/commands/runtime.py +5 -5
  25. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/commands/uninstall.py +47 -19
  26. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/commands/update.py +50 -19
  27. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/compilation/agents_compiler.py +19 -7
  28. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/compilation/claude_formatter.py +11 -0
  29. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/compilation/context_optimizer.py +14 -8
  30. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/compilation/distributed_compiler.py +7 -7
  31. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/compilation/link_resolver.py +5 -4
  32. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/core/safe_installer.py +6 -6
  33. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/core/script_runner.py +29 -15
  34. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/core/target_detection.py +45 -17
  35. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/core/token_manager.py +1 -1
  36. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/deps/apm_resolver.py +1 -1
  37. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/deps/github_downloader.py +20 -11
  38. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/deps/lockfile.py +59 -5
  39. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/deps/plugin_parser.py +22 -22
  40. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/factory.py +4 -0
  41. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/integration/__init__.py +12 -0
  42. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/integration/agent_integrator.py +198 -4
  43. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/integration/base_integrator.py +35 -13
  44. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/integration/command_integrator.py +73 -0
  45. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/integration/hook_integrator.py +182 -16
  46. apm_cli-0.8.0/src/apm_cli/integration/instruction_integrator.py +261 -0
  47. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/integration/mcp_integrator.py +107 -39
  48. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/integration/prompt_integrator.py +1 -1
  49. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/integration/skill_integrator.py +178 -64
  50. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/integration/skill_transformer.py +2 -2
  51. apm_cli-0.8.0/src/apm_cli/integration/targets.py +180 -0
  52. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/models/dependency.py +105 -23
  53. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/models/validation.py +5 -5
  54. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/output/formatters.py +85 -85
  55. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/output/script_formatters.py +21 -21
  56. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/primitives/models.py +2 -2
  57. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/registry/operations.py +2 -2
  58. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/runtime/manager.py +78 -44
  59. apm_cli-0.8.0/src/apm_cli/security/__init__.py +5 -0
  60. apm_cli-0.8.0/src/apm_cli/security/content_scanner.py +303 -0
  61. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/utils/console.py +26 -26
  62. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/utils/diagnostics.py +89 -6
  63. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/utils/github_host.py +4 -4
  64. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/utils/helpers.py +3 -1
  65. {apm_cli-0.7.8 → apm_cli-0.8.0/src/apm_cli.egg-info}/PKG-INFO +27 -4
  66. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli.egg-info/SOURCES.txt +6 -0
  67. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli.egg-info/requires.txt +3 -1
  68. {apm_cli-0.7.8 → apm_cli-0.8.0}/tests/test_apm_package_models.py +9 -4
  69. {apm_cli-0.7.8 → apm_cli-0.8.0}/tests/test_apm_resolver.py +1 -1
  70. {apm_cli-0.7.8 → apm_cli-0.8.0}/tests/test_enhanced_discovery.py +3 -3
  71. {apm_cli-0.7.8 → apm_cli-0.8.0}/tests/test_github_downloader.py +23 -2
  72. {apm_cli-0.7.8 → apm_cli-0.8.0}/tests/test_lockfile.py +42 -3
  73. {apm_cli-0.7.8 → apm_cli-0.8.0}/tests/test_runnable_prompts.py +2 -2
  74. apm_cli-0.7.8/src/apm_cli/integration/instruction_integrator.py +0 -114
  75. {apm_cli-0.7.8 → apm_cli-0.8.0}/AUTHORS +0 -0
  76. {apm_cli-0.7.8 → apm_cli-0.8.0}/LICENSE +0 -0
  77. {apm_cli-0.7.8 → apm_cli-0.8.0}/setup.cfg +0 -0
  78. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/__init__.py +0 -0
  79. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/adapters/__init__.py +0 -0
  80. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/adapters/client/__init__.py +0 -0
  81. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/adapters/client/base.py +0 -0
  82. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/adapters/package_manager/__init__.py +0 -0
  83. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/adapters/package_manager/base.py +0 -0
  84. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/adapters/package_manager/default_manager.py +0 -0
  85. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/bundle/__init__.py +0 -0
  86. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/commands/__init__.py +0 -0
  87. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/commands/config.py +0 -0
  88. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/compilation/__init__.py +0 -0
  89. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/compilation/constants.py +0 -0
  90. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/compilation/constitution.py +0 -0
  91. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/compilation/constitution_block.py +0 -0
  92. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/compilation/injector.py +0 -0
  93. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/compilation/template_builder.py +0 -0
  94. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/config.py +0 -0
  95. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/core/__init__.py +0 -0
  96. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/core/conflict_detector.py +0 -0
  97. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/core/docker_args.py +0 -0
  98. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/core/operations.py +0 -0
  99. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/deps/__init__.py +0 -0
  100. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/deps/aggregator.py +0 -0
  101. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/deps/collection_parser.py +0 -0
  102. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/deps/dependency_graph.py +0 -0
  103. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/deps/package_validator.py +0 -0
  104. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/deps/verifier.py +0 -0
  105. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/drift.py +0 -0
  106. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/integration/utils.py +0 -0
  107. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/models/__init__.py +0 -0
  108. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/models/apm_package.py +0 -0
  109. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/models/plugin.py +0 -0
  110. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/output/__init__.py +0 -0
  111. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/output/models.py +0 -0
  112. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/primitives/__init__.py +0 -0
  113. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/primitives/discovery.py +0 -0
  114. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/primitives/parser.py +0 -0
  115. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/registry/__init__.py +0 -0
  116. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/registry/client.py +0 -0
  117. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/registry/integration.py +0 -0
  118. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/runtime/__init__.py +0 -0
  119. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/runtime/base.py +0 -0
  120. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/runtime/codex_runtime.py +0 -0
  121. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/runtime/copilot_runtime.py +0 -0
  122. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/runtime/factory.py +0 -0
  123. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/runtime/llm_runtime.py +0 -0
  124. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/utils/__init__.py +0 -0
  125. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/utils/version_checker.py +0 -0
  126. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/version.py +0 -0
  127. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/workflow/__init__.py +0 -0
  128. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/workflow/discovery.py +0 -0
  129. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/workflow/parser.py +0 -0
  130. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/workflow/runner.py +0 -0
  131. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli.egg-info/dependency_links.txt +0 -0
  132. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli.egg-info/entry_points.txt +0 -0
  133. {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli.egg-info/top_level.txt +0 -0
  134. {apm_cli-0.7.8 → apm_cli-0.8.0}/tests/test_codex_docker_args_fix.py +0 -0
  135. {apm_cli-0.7.8 → apm_cli-0.8.0}/tests/test_codex_empty_string_and_defaults.py +0 -0
  136. {apm_cli-0.7.8 → apm_cli-0.8.0}/tests/test_collision_integration.py +0 -0
  137. {apm_cli-0.7.8 → apm_cli-0.8.0}/tests/test_console.py +0 -0
  138. {apm_cli-0.7.8 → apm_cli-0.8.0}/tests/test_distributed_compilation.py +0 -0
  139. {apm_cli-0.7.8 → apm_cli-0.8.0}/tests/test_empty_string_and_defaults.py +0 -0
  140. {apm_cli-0.7.8 → apm_cli-0.8.0}/tests/test_github_downloader_token_precedence.py +0 -0
  141. {apm_cli-0.7.8 → apm_cli-0.8.0}/tests/test_runtime_manager_token_precedence.py +0 -0
  142. {apm_cli-0.7.8 → apm_cli-0.8.0}/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.7.8
3
+ Version: 0.8.0
4
4
  Summary: MCP configuration tool
5
5
  Author-email: Daniel Meppiel <user@example.com>
6
6
  License: MIT License
@@ -52,7 +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: black>=23.0.0; extra == "dev"
55
+ Requires-Dist: black>=26.3.1; python_version >= "3.10" and extra == "dev"
56
56
  Requires-Dist: isort>=5.0.0; extra == "dev"
57
57
  Requires-Dist: mypy>=1.0.0; extra == "dev"
58
58
  Provides-Extra: build
@@ -65,7 +65,7 @@ Dynamic: license-file
65
65
 
66
66
  Think `package.json`, `requirements.txt`, or `Cargo.toml` — but for AI agent configuration.
67
67
 
68
- GitHub Copilot · Claude Code
68
+ GitHub Copilot · Claude Code · Cursor · OpenCode
69
69
 
70
70
  **[Documentation](https://microsoft.github.io/apm/)** · **[Quick Start](https://microsoft.github.io/apm/getting-started/quick-start/)** · **[CLI Reference](https://microsoft.github.io/apm/reference/cli-commands/)**
71
71
 
@@ -101,19 +101,32 @@ apm install # every agent is configured
101
101
  - **One manifest for everything** — instructions, skills, prompts, agents, hooks, plugins, MCP servers
102
102
  - **Install from anywhere** — GitHub, GitLab, Bitbucket, Azure DevOps, GitHub Enterprise, any git host
103
103
  - **Transitive dependencies** — packages can depend on packages; APM resolves the full tree
104
- - **Compile to standards** — `apm compile` produces `AGENTS.md` (GitHub Copilot) and `CLAUDE.md` (Claude Code)
104
+ - **Compile to standards** — `apm compile` produces `AGENTS.md` (GitHub Copilot, OpenCode), `CLAUDE.md` (Claude Code), and `.cursor/rules/` (Cursor)
105
+ - **Content security** — `apm audit` scans for hidden Unicode characters; `apm install` blocks compromised packages before agents can read them
105
106
  - **Create & share** — `apm pack` bundles your current configuration as a zipped package
106
107
  - **CI/CD ready** — [GitHub Action](https://github.com/microsoft/apm-action) for automated workflows
107
108
 
108
109
  ## Get Started
109
110
 
111
+ #### Linux / macOS
112
+
110
113
  ```bash
111
114
  curl -sSL https://raw.githubusercontent.com/microsoft/apm/main/install.sh | sh
112
115
  ```
113
116
 
117
+ #### Windows
118
+
119
+ ```powershell
120
+ irm https://raw.githubusercontent.com/microsoft/apm/main/install.ps1 | iex
121
+ ```
122
+
123
+ Native release binaries are published for macOS, Linux, and Windows x86_64. `apm update` reuses the matching platform installer.
124
+
114
125
  <details>
115
126
  <summary>Other install methods</summary>
116
127
 
128
+ #### Linux / macOS
129
+
117
130
  ```bash
118
131
  # Homebrew
119
132
  brew install microsoft/apm/apm
@@ -121,6 +134,16 @@ brew install microsoft/apm/apm
121
134
  pip install apm-cli
122
135
  ```
123
136
 
137
+ #### Windows
138
+
139
+ ```powershell
140
+ # Scoop
141
+ scoop bucket add apm https://github.com/microsoft/scoop-apm
142
+ scoop install apm
143
+ # pip
144
+ pip install apm-cli
145
+ ```
146
+
124
147
  </details>
125
148
 
126
149
  Then start adding packages:
@@ -4,7 +4,7 @@
4
4
 
5
5
  Think `package.json`, `requirements.txt`, or `Cargo.toml` — but for AI agent configuration.
6
6
 
7
- GitHub Copilot · Claude Code
7
+ GitHub Copilot · Claude Code · Cursor · OpenCode
8
8
 
9
9
  **[Documentation](https://microsoft.github.io/apm/)** · **[Quick Start](https://microsoft.github.io/apm/getting-started/quick-start/)** · **[CLI Reference](https://microsoft.github.io/apm/reference/cli-commands/)**
10
10
 
@@ -40,19 +40,32 @@ apm install # every agent is configured
40
40
  - **One manifest for everything** — instructions, skills, prompts, agents, hooks, plugins, MCP servers
41
41
  - **Install from anywhere** — GitHub, GitLab, Bitbucket, Azure DevOps, GitHub Enterprise, any git host
42
42
  - **Transitive dependencies** — packages can depend on packages; APM resolves the full tree
43
- - **Compile to standards** — `apm compile` produces `AGENTS.md` (GitHub Copilot) and `CLAUDE.md` (Claude Code)
43
+ - **Compile to standards** — `apm compile` produces `AGENTS.md` (GitHub Copilot, OpenCode), `CLAUDE.md` (Claude Code), and `.cursor/rules/` (Cursor)
44
+ - **Content security** — `apm audit` scans for hidden Unicode characters; `apm install` blocks compromised packages before agents can read them
44
45
  - **Create & share** — `apm pack` bundles your current configuration as a zipped package
45
46
  - **CI/CD ready** — [GitHub Action](https://github.com/microsoft/apm-action) for automated workflows
46
47
 
47
48
  ## Get Started
48
49
 
50
+ #### Linux / macOS
51
+
49
52
  ```bash
50
53
  curl -sSL https://raw.githubusercontent.com/microsoft/apm/main/install.sh | sh
51
54
  ```
52
55
 
56
+ #### Windows
57
+
58
+ ```powershell
59
+ irm https://raw.githubusercontent.com/microsoft/apm/main/install.ps1 | iex
60
+ ```
61
+
62
+ Native release binaries are published for macOS, Linux, and Windows x86_64. `apm update` reuses the matching platform installer.
63
+
53
64
  <details>
54
65
  <summary>Other install methods</summary>
55
66
 
67
+ #### Linux / macOS
68
+
56
69
  ```bash
57
70
  # Homebrew
58
71
  brew install microsoft/apm/apm
@@ -60,6 +73,16 @@ brew install microsoft/apm/apm
60
73
  pip install apm-cli
61
74
  ```
62
75
 
76
+ #### Windows
77
+
78
+ ```powershell
79
+ # Scoop
80
+ scoop bucket add apm https://github.com/microsoft/scoop-apm
81
+ scoop install apm
82
+ # pip
83
+ pip install apm-cli
84
+ ```
85
+
63
86
  </details>
64
87
 
65
88
  Then start adding packages:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "apm-cli"
7
- version = "0.7.8"
7
+ version = "0.8.0"
8
8
  description = "MCP configuration tool"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -41,7 +41,7 @@ dependencies = [
41
41
  dev = [
42
42
  "pytest>=7.0.0",
43
43
  "pytest-cov>=4.0.0",
44
- "black>=23.0.0",
44
+ "black>=26.3.1; python_version>='3.10'",
45
45
  "isort>=5.0.0",
46
46
  "mypy>=1.0.0",
47
47
  ]
@@ -122,7 +122,7 @@ class CodexClientAdapter(MCPClientAdapter):
122
122
 
123
123
  # If server has only remote endpoints and no packages, it's a remote-only server
124
124
  if remotes and not packages:
125
- print(f"⚠️ Warning: MCP server '{server_url}' is a remote server (SSE type)")
125
+ print(f"[!] Warning: MCP server '{server_url}' is a remote server (SSE type)")
126
126
  print(" Codex CLI only supports local servers with command/args configuration")
127
127
  print(" Remote servers are not supported by Codex CLI")
128
128
  print(" Skipping installation for Codex CLI")
@@ -174,7 +174,7 @@ class CodexClientAdapter(MCPClientAdapter):
174
174
  "id": server_info.get("id", "") # Add registry UUID for conflict detection
175
175
  }
176
176
 
177
- # Self-defined stdio deps carry raw command/args use directly
177
+ # Self-defined stdio deps carry raw command/args -- use directly
178
178
  raw = server_info.get("_raw_stdio")
179
179
  if raw:
180
180
  config["command"] = raw["command"]
@@ -328,7 +328,7 @@ class CodexClientAdapter(MCPClientAdapter):
328
328
  # Check for CI/automated environment via APM_E2E_TESTS flag (more reliable than TTY detection)
329
329
  if os.getenv('APM_E2E_TESTS') == '1':
330
330
  skip_prompting = True
331
- print(f"💡 APM_E2E_TESTS detected, will skip environment variable prompts")
331
+ print(f" APM_E2E_TESTS detected, will skip environment variable prompts")
332
332
 
333
333
  # Also skip prompting if we're in a non-interactive environment (fallback)
334
334
  is_interactive = sys.stdin.isatty() and sys.stdout.isatty()
@@ -166,7 +166,7 @@ class CopilotClientAdapter(MCPClientAdapter):
166
166
  "id": server_info.get("id", "") # Add registry UUID for conflict detection
167
167
  }
168
168
 
169
- # Self-defined stdio deps carry raw command/args use directly
169
+ # Self-defined stdio deps carry raw command/args -- use directly
170
170
  raw = server_info.get("_raw_stdio")
171
171
  if raw:
172
172
  config["command"] = raw["command"]
@@ -331,7 +331,7 @@ class CopilotClientAdapter(MCPClientAdapter):
331
331
  # Check for CI/automated environment via APM_E2E_TESTS flag (more reliable than TTY detection)
332
332
  if os.getenv('APM_E2E_TESTS') == '1':
333
333
  skip_prompting = True
334
- print(f"💡 APM_E2E_TESTS detected, will skip environment variable prompts")
334
+ print(f" APM_E2E_TESTS detected, will skip environment variable prompts")
335
335
 
336
336
  # Also skip prompting if we're in a non-interactive environment (fallback)
337
337
  is_interactive = sys.stdin.isatty() and sys.stdout.isatty()
@@ -0,0 +1,138 @@
1
+ """Cursor IDE implementation of MCP client adapter.
2
+
3
+ Cursor uses the standard ``mcpServers`` JSON format at ``.cursor/mcp.json``
4
+ (repo-local). The config schema is identical to GitHub Copilot CLI, so this
5
+ adapter subclasses :class:`CopilotClientAdapter` and only overrides the
6
+ config-path logic and the user-facing labels.
7
+
8
+ APM only writes to ``.cursor/mcp.json`` when the ``.cursor/`` directory
9
+ already exists — Cursor support is opt-in.
10
+ """
11
+
12
+ import json
13
+ import os
14
+ from pathlib import Path
15
+
16
+ from .copilot import CopilotClientAdapter
17
+
18
+
19
+ class CursorClientAdapter(CopilotClientAdapter):
20
+ """Cursor IDE MCP client adapter.
21
+
22
+ Inherits all config formatting from :class:`CopilotClientAdapter`
23
+ (``mcpServers`` JSON with ``command``/``args``/``env``). Only the
24
+ config-file location differs: repo-local ``.cursor/mcp.json`` instead
25
+ of global ``~/.copilot/mcp-config.json``.
26
+ """
27
+
28
+ # ------------------------------------------------------------------ #
29
+ # Config path
30
+ # ------------------------------------------------------------------ #
31
+
32
+ def get_config_path(self):
33
+ """Return the path to ``.cursor/mcp.json`` in the repository root.
34
+
35
+ Unlike the Copilot adapter this is a **repo-local** path. The
36
+ ``.cursor/`` directory is *not* created automatically — APM only
37
+ writes here when the directory already exists.
38
+ """
39
+ cursor_dir = Path(os.getcwd()) / ".cursor"
40
+ return str(cursor_dir / "mcp.json")
41
+
42
+ # ------------------------------------------------------------------ #
43
+ # Config read / write — override to avoid auto-creating the directory
44
+ # ------------------------------------------------------------------ #
45
+
46
+ def update_config(self, config_updates):
47
+ """Merge *config_updates* into the ``mcpServers`` section.
48
+
49
+ The ``.cursor/`` directory must already exist; if it does not, this
50
+ method returns silently (opt-in behaviour).
51
+ """
52
+ config_path = Path(self.get_config_path())
53
+
54
+ # Opt-in: only write when .cursor/ already exists
55
+ if not config_path.parent.exists():
56
+ return
57
+
58
+ current_config = self.get_current_config()
59
+ if "mcpServers" not in current_config:
60
+ current_config["mcpServers"] = {}
61
+
62
+ current_config["mcpServers"].update(config_updates)
63
+
64
+ with open(config_path, "w", encoding="utf-8") as f:
65
+ json.dump(current_config, f, indent=2)
66
+
67
+ def get_current_config(self):
68
+ """Read the current ``.cursor/mcp.json`` contents."""
69
+ config_path = self.get_config_path()
70
+
71
+ if not os.path.exists(config_path):
72
+ return {}
73
+
74
+ try:
75
+ with open(config_path, "r", encoding="utf-8") as f:
76
+ return json.load(f)
77
+ except (json.JSONDecodeError, IOError):
78
+ return {}
79
+
80
+ # ------------------------------------------------------------------ #
81
+ # configure_mcp_server — thin override for the print label
82
+ # ------------------------------------------------------------------ #
83
+
84
+ def configure_mcp_server(
85
+ self,
86
+ server_url,
87
+ server_name=None,
88
+ enabled=True,
89
+ env_overrides=None,
90
+ server_info_cache=None,
91
+ runtime_vars=None,
92
+ ):
93
+ """Configure an MCP server in Cursor's ``.cursor/mcp.json``.
94
+
95
+ Delegates entirely to the parent implementation but prints a
96
+ Cursor-specific success message.
97
+ """
98
+ if not server_url:
99
+ print("Error: server_url cannot be empty")
100
+ return False
101
+
102
+ # Opt-in: skip silently when .cursor/ does not exist
103
+ cursor_dir = Path(os.getcwd()) / ".cursor"
104
+ if not cursor_dir.exists():
105
+ return True # nothing to do, not an error
106
+
107
+ try:
108
+ # Use cached server info if available, otherwise fetch from registry
109
+ if server_info_cache and server_url in server_info_cache:
110
+ server_info = server_info_cache[server_url]
111
+ else:
112
+ server_info = self.registry_client.find_server_by_reference(server_url)
113
+
114
+ if not server_info:
115
+ print(f"Error: MCP server '{server_url}' not found in registry")
116
+ return False
117
+
118
+ # Determine config key
119
+ if server_name:
120
+ config_key = server_name
121
+ elif "/" in server_url:
122
+ config_key = server_url.split("/")[-1]
123
+ else:
124
+ config_key = server_url
125
+
126
+ server_config = self._format_server_config(
127
+ server_info, env_overrides, runtime_vars
128
+ )
129
+ self.update_config({config_key: server_config})
130
+
131
+ print(
132
+ f"Successfully configured MCP server '{config_key}' for Cursor"
133
+ )
134
+ return True
135
+
136
+ except Exception as e:
137
+ print(f"Error configuring MCP server: {e}")
138
+ return False
@@ -0,0 +1,157 @@
1
+ """OpenCode implementation of MCP client adapter.
2
+
3
+ OpenCode uses ``opencode.json`` at the project root with an ``mcp`` key.
4
+ The schema differs from VSCode/Cursor:
5
+
6
+ .. code-block:: json
7
+
8
+ {
9
+ "mcp": {
10
+ "server-name": {
11
+ "type": "local",
12
+ "command": ["npx", "-y", "@modelcontextprotocol/server-foo"],
13
+ "environment": { "KEY": "value" },
14
+ "enabled": true
15
+ }
16
+ }
17
+ }
18
+
19
+ Key differences from Copilot/Cursor:
20
+ - Config file: ``opencode.json`` (not ``mcp.json``)
21
+ - Wrapper key: ``mcp`` (not ``mcpServers``)
22
+ - Command format: single array ``command`` (not ``command`` + ``args``)
23
+ - Env key: ``environment`` (not ``env``)
24
+
25
+ APM only writes to ``opencode.json`` when the ``.opencode/`` directory
26
+ already exists — OpenCode support is opt-in.
27
+ """
28
+
29
+ import json
30
+ import os
31
+ from pathlib import Path
32
+
33
+ from .copilot import CopilotClientAdapter
34
+
35
+
36
+ class OpenCodeClientAdapter(CopilotClientAdapter):
37
+ """OpenCode MCP client adapter.
38
+
39
+ Converts the standard Copilot config format into OpenCode's schema
40
+ and writes to ``opencode.json`` in the project root.
41
+ """
42
+
43
+ def get_config_path(self):
44
+ """Return the path to ``opencode.json`` in the repository root."""
45
+ return str(Path(os.getcwd()) / "opencode.json")
46
+
47
+ def update_config(self, config_updates, enabled=True):
48
+ """Merge *config_updates* into the ``mcp`` section of ``opencode.json``.
49
+
50
+ The ``.opencode/`` directory must already exist; if it does not, this
51
+ method returns silently (opt-in behaviour).
52
+
53
+ Translates Copilot-format entries (``command``/``args``/``env``) into
54
+ OpenCode format (``command`` array / ``environment``).
55
+ """
56
+ opencode_dir = Path(os.getcwd()) / ".opencode"
57
+ if not opencode_dir.is_dir():
58
+ return
59
+
60
+ config_path = Path(self.get_config_path())
61
+ current_config = self.get_current_config()
62
+ if "mcp" not in current_config:
63
+ current_config["mcp"] = {}
64
+
65
+ for name, copilot_entry in config_updates.items():
66
+ current_config["mcp"][name] = self._to_opencode_format(copilot_entry, enabled=enabled)
67
+
68
+ with open(config_path, "w", encoding="utf-8") as f:
69
+ json.dump(current_config, f, indent=2)
70
+
71
+ def get_current_config(self):
72
+ """Read the current ``opencode.json`` contents."""
73
+ config_path = self.get_config_path()
74
+ if not os.path.exists(config_path):
75
+ return {}
76
+ try:
77
+ with open(config_path, "r", encoding="utf-8") as f:
78
+ return json.load(f)
79
+ except (json.JSONDecodeError, IOError):
80
+ return {}
81
+
82
+ def configure_mcp_server(
83
+ self,
84
+ server_url,
85
+ server_name=None,
86
+ enabled=True,
87
+ env_overrides=None,
88
+ server_info_cache=None,
89
+ runtime_vars=None,
90
+ ):
91
+ """Configure an MCP server in ``opencode.json``.
92
+
93
+ Delegates to the parent for config formatting, then converts to
94
+ OpenCode schema before writing.
95
+ """
96
+ if not server_url:
97
+ print("Error: server_url cannot be empty")
98
+ return False
99
+
100
+ opencode_dir = Path(os.getcwd()) / ".opencode"
101
+ if not opencode_dir.is_dir():
102
+ return False
103
+
104
+ try:
105
+ if server_info_cache and server_url in server_info_cache:
106
+ server_info = server_info_cache[server_url]
107
+ else:
108
+ server_info = self.registry_client.find_server_by_reference(server_url)
109
+
110
+ if not server_info:
111
+ print(f"Error: MCP server '{server_url}' not found in registry")
112
+ return False
113
+
114
+ if server_name:
115
+ config_key = server_name
116
+ elif "/" in server_url:
117
+ config_key = server_url.split("/")[-1]
118
+ else:
119
+ config_key = server_url
120
+
121
+ server_config = self._format_server_config(
122
+ server_info, env_overrides, runtime_vars
123
+ )
124
+ self.update_config({config_key: server_config}, enabled=enabled)
125
+
126
+ print(
127
+ f"Successfully configured MCP server '{config_key}' for OpenCode"
128
+ )
129
+ return True
130
+
131
+ except Exception as e:
132
+ print(f"Error configuring MCP server: {e}")
133
+ return False
134
+
135
+ @staticmethod
136
+ def _to_opencode_format(copilot_entry: dict, enabled: bool = True) -> dict:
137
+ """Convert a Copilot-format server config to OpenCode format.
138
+
139
+ Copilot: ``{"command": "npx", "args": ["-y", "pkg"], "env": {...}}``
140
+ OpenCode: ``{"type": "local", "command": ["npx", "-y", "pkg"],
141
+ "environment": {...}, "enabled": true}``
142
+ """
143
+ entry: dict = {"type": "local", "enabled": enabled}
144
+
145
+ cmd = copilot_entry.get("command", "")
146
+ args = copilot_entry.get("args", [])
147
+ if cmd:
148
+ entry["command"] = [cmd] + list(args)
149
+ elif "url" in copilot_entry:
150
+ entry["type"] = "remote"
151
+ entry["url"] = copilot_entry["url"]
152
+
153
+ env = copilot_entry.get("env") or {}
154
+ if env:
155
+ entry["environment"] = env
156
+
157
+ return entry
@@ -187,7 +187,7 @@ class VSCodeClientAdapter(MCPClientAdapter):
187
187
  server_config = {}
188
188
  input_vars = []
189
189
 
190
- # Self-defined stdio deps carry raw command/args use directly
190
+ # Self-defined stdio deps carry raw command/args -- use directly
191
191
  raw = server_info.get("_raw_stdio")
192
192
  if raw:
193
193
  server_config = {
@@ -12,7 +12,7 @@ def enrich_lockfile_for_pack(
12
12
  ) -> str:
13
13
  """Create an enriched copy of the lockfile YAML with a ``pack:`` section.
14
14
 
15
- Does NOT mutate the original *lockfile* object serialises a copy and
15
+ Does NOT mutate the original *lockfile* object -- serialises a copy and
16
16
  prepends the pack metadata.
17
17
 
18
18
  Args:
@@ -1,12 +1,13 @@
1
- """Bundle packer creates self-contained APM bundles from the resolved dependency tree."""
1
+ """Bundle packer -- creates self-contained APM bundles from the resolved dependency tree."""
2
2
 
3
+ import os
3
4
  import shutil
4
5
  import tarfile
5
6
  from dataclasses import dataclass, field
6
7
  from pathlib import Path
7
8
  from typing import List, Optional
8
9
 
9
- from ..deps.lockfile import LockFile
10
+ from ..deps.lockfile import LockFile, get_lockfile_path, migrate_lockfile_if_needed
10
11
  from ..models.apm_package import APMPackage
11
12
  from ..core.target_detection import detect_target
12
13
  from .lockfile_enrichment import enrich_lockfile_for_pack
@@ -17,7 +18,9 @@ _TARGET_PREFIXES = {
17
18
  "copilot": [".github/"],
18
19
  "vscode": [".github/"],
19
20
  "claude": [".claude/"],
20
- "all": [".github/", ".claude/"],
21
+ "cursor": [".cursor/"],
22
+ "opencode": [".opencode/"],
23
+ "all": [".github/", ".claude/", ".cursor/", ".opencode/"],
21
24
  }
22
25
 
23
26
 
@@ -47,10 +50,10 @@ def pack_bundle(
47
50
  """Create a self-contained bundle from installed APM dependencies.
48
51
 
49
52
  Args:
50
- project_root: Root of the project containing ``apm.lock`` and ``apm.yml``.
53
+ project_root: Root of the project containing ``apm.lock.yaml`` and ``apm.yml``.
51
54
  output_dir: Directory where the bundle will be created.
52
- fmt: Bundle format ``"apm"`` (default) or ``"plugin"``.
53
- target: Target filter ``"vscode"``, ``"claude"``, ``"all"``, or *None*
55
+ fmt: Bundle format -- ``"apm"`` (default) or ``"plugin"``.
56
+ target: Target filter -- ``"vscode"``, ``"claude"``, ``"all"``, or *None*
54
57
  (auto-detect from apm.yml / project structure).
55
58
  archive: If *True*, produce a ``.tar.gz`` and remove the directory.
56
59
  dry_run: If *True*, resolve the file list but write nothing to disk.
@@ -59,15 +62,16 @@ def pack_bundle(
59
62
  :class:`PackResult` describing what was (or would be) produced.
60
63
 
61
64
  Raises:
62
- FileNotFoundError: If ``apm.lock`` is missing.
65
+ FileNotFoundError: If ``apm.lock.yaml`` is missing.
63
66
  ValueError: If deployed files referenced in the lockfile are missing on disk.
64
67
  """
65
- # 1. Read lockfile
66
- lockfile_path = project_root / "apm.lock"
68
+ # 1. Read lockfile (migrate legacy apm.lock → apm.lock.yaml if needed)
69
+ migrate_lockfile_if_needed(project_root)
70
+ lockfile_path = get_lockfile_path(project_root)
67
71
  lockfile = LockFile.read(lockfile_path)
68
72
  if lockfile is None:
69
73
  raise FileNotFoundError(
70
- "apm.lock not found run 'apm install' first to resolve dependencies."
74
+ "apm.lock.yaml not found -- run 'apm install' first to resolve dependencies."
71
75
  )
72
76
 
73
77
  # 2. Read apm.yml for name / version / config target
@@ -77,7 +81,19 @@ def pack_bundle(
77
81
  pkg_name = package.name
78
82
  pkg_version = package.version or "0.0.0"
79
83
  config_target = package.target
80
- except (FileNotFoundError, ValueError):
84
+
85
+ # Guard: reject local-path dependencies (non-portable)
86
+ for dep_ref in package.get_apm_dependencies():
87
+ if dep_ref.is_local:
88
+ raise ValueError(
89
+ f"Cannot pack — apm.yml contains local path dependency: "
90
+ f"{dep_ref.local_path}\n"
91
+ f"Local dependencies are for development only. Replace them with "
92
+ f"remote references (e.g., 'owner/repo') before packing."
93
+ )
94
+ except ValueError:
95
+ raise
96
+ except FileNotFoundError:
81
97
  pkg_name = project_root.resolve().name
82
98
  pkg_version = "0.0.0"
83
99
  config_target = None
@@ -88,7 +104,7 @@ def pack_bundle(
88
104
  explicit_target=target,
89
105
  config_target=config_target,
90
106
  )
91
- # For packing purposes, "minimal" means nothing to pack treat as "all"
107
+ # For packing purposes, "minimal" means nothing to pack -- treat as "all"
92
108
  if effective_target == "minimal":
93
109
  effective_target = "all"
94
110
 
@@ -126,7 +142,7 @@ def pack_bundle(
126
142
  missing.append(rel_path)
127
143
  if missing:
128
144
  raise ValueError(
129
- f"The following deployed files are missing on disk "
145
+ f"The following deployed files are missing on disk -- "
130
146
  f"run 'apm install' to restore them:\n"
131
147
  + "\n".join(f" - {m}" for m in missing)
132
148
  )
@@ -140,6 +156,34 @@ def pack_bundle(
140
156
  lockfile_enriched=True,
141
157
  )
142
158
 
159
+ # 5b. Scan files for hidden characters before bundling
160
+ from ..security.content_scanner import ContentScanner
161
+ from ..utils.console import _rich_warning
162
+
163
+ _scan_findings_total = 0
164
+ for rel_path in unique_files:
165
+ src = project_root / rel_path
166
+ if src.is_symlink():
167
+ continue
168
+ if src.is_file():
169
+ findings = ContentScanner.scan_file(src)
170
+ if findings:
171
+ _scan_findings_total += len(findings)
172
+ elif src.is_dir():
173
+ for dirpath, _dirnames, filenames in os.walk(src, followlinks=False):
174
+ for fname in filenames:
175
+ fpath = Path(dirpath) / fname
176
+ if fpath.is_symlink():
177
+ continue
178
+ findings = ContentScanner.scan_file(fpath)
179
+ if findings:
180
+ _scan_findings_total += len(findings)
181
+ if _scan_findings_total:
182
+ _rich_warning(
183
+ f"Bundle contains {_scan_findings_total} hidden character(s) across source files "
184
+ f"— run 'apm audit' to inspect before publishing"
185
+ )
186
+
143
187
  # 6. Build output directory
144
188
  bundle_dir = output_dir / f"{pkg_name}-{pkg_version}"
145
189
  bundle_dir.mkdir(parents=True, exist_ok=True)
@@ -156,7 +200,7 @@ def pack_bundle(
156
200
 
157
201
  # 8. Enrich lockfile copy and write to bundle
158
202
  enriched_yaml = enrich_lockfile_for_pack(lockfile, fmt, effective_target)
159
- (bundle_dir / "apm.lock").write_text(enriched_yaml, encoding="utf-8")
203
+ (bundle_dir / "apm.lock.yaml").write_text(enriched_yaml, encoding="utf-8")
160
204
 
161
205
  result = PackResult(
162
206
  bundle_path=bundle_dir,