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.
- {apm_cli-0.7.8/src/apm_cli.egg-info → apm_cli-0.8.0}/PKG-INFO +27 -4
- {apm_cli-0.7.8 → apm_cli-0.8.0}/README.md +25 -2
- {apm_cli-0.7.8 → apm_cli-0.8.0}/pyproject.toml +2 -2
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/adapters/client/codex.py +3 -3
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/adapters/client/copilot.py +2 -2
- apm_cli-0.8.0/src/apm_cli/adapters/client/cursor.py +138 -0
- apm_cli-0.8.0/src/apm_cli/adapters/client/opencode.py +157 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/adapters/client/vscode.py +1 -1
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/bundle/lockfile_enrichment.py +1 -1
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/bundle/packer.py +58 -14
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/bundle/unpacker.py +15 -10
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/cli.py +48 -1
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/commands/_helpers.py +4 -4
- apm_cli-0.8.0/src/apm_cli/commands/audit.py +504 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/commands/compile.py +57 -44
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/commands/deps.py +38 -38
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/commands/init.py +5 -5
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/commands/install.py +710 -21
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/commands/list_cmd.py +2 -2
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/commands/mcp.py +19 -19
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/commands/pack.py +5 -5
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/commands/prune.py +7 -8
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/commands/run.py +6 -6
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/commands/runtime.py +5 -5
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/commands/uninstall.py +47 -19
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/commands/update.py +50 -19
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/compilation/agents_compiler.py +19 -7
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/compilation/claude_formatter.py +11 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/compilation/context_optimizer.py +14 -8
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/compilation/distributed_compiler.py +7 -7
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/compilation/link_resolver.py +5 -4
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/core/safe_installer.py +6 -6
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/core/script_runner.py +29 -15
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/core/target_detection.py +45 -17
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/core/token_manager.py +1 -1
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/deps/apm_resolver.py +1 -1
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/deps/github_downloader.py +20 -11
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/deps/lockfile.py +59 -5
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/deps/plugin_parser.py +22 -22
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/factory.py +4 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/integration/__init__.py +12 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/integration/agent_integrator.py +198 -4
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/integration/base_integrator.py +35 -13
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/integration/command_integrator.py +73 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/integration/hook_integrator.py +182 -16
- apm_cli-0.8.0/src/apm_cli/integration/instruction_integrator.py +261 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/integration/mcp_integrator.py +107 -39
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/integration/prompt_integrator.py +1 -1
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/integration/skill_integrator.py +178 -64
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/integration/skill_transformer.py +2 -2
- apm_cli-0.8.0/src/apm_cli/integration/targets.py +180 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/models/dependency.py +105 -23
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/models/validation.py +5 -5
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/output/formatters.py +85 -85
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/output/script_formatters.py +21 -21
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/primitives/models.py +2 -2
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/registry/operations.py +2 -2
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/runtime/manager.py +78 -44
- apm_cli-0.8.0/src/apm_cli/security/__init__.py +5 -0
- apm_cli-0.8.0/src/apm_cli/security/content_scanner.py +303 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/utils/console.py +26 -26
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/utils/diagnostics.py +89 -6
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/utils/github_host.py +4 -4
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/utils/helpers.py +3 -1
- {apm_cli-0.7.8 → apm_cli-0.8.0/src/apm_cli.egg-info}/PKG-INFO +27 -4
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli.egg-info/SOURCES.txt +6 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli.egg-info/requires.txt +3 -1
- {apm_cli-0.7.8 → apm_cli-0.8.0}/tests/test_apm_package_models.py +9 -4
- {apm_cli-0.7.8 → apm_cli-0.8.0}/tests/test_apm_resolver.py +1 -1
- {apm_cli-0.7.8 → apm_cli-0.8.0}/tests/test_enhanced_discovery.py +3 -3
- {apm_cli-0.7.8 → apm_cli-0.8.0}/tests/test_github_downloader.py +23 -2
- {apm_cli-0.7.8 → apm_cli-0.8.0}/tests/test_lockfile.py +42 -3
- {apm_cli-0.7.8 → apm_cli-0.8.0}/tests/test_runnable_prompts.py +2 -2
- apm_cli-0.7.8/src/apm_cli/integration/instruction_integrator.py +0 -114
- {apm_cli-0.7.8 → apm_cli-0.8.0}/AUTHORS +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/LICENSE +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/setup.cfg +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/__init__.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/adapters/__init__.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/adapters/client/__init__.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/adapters/client/base.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/adapters/package_manager/__init__.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/adapters/package_manager/base.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/adapters/package_manager/default_manager.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/bundle/__init__.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/commands/__init__.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/commands/config.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/compilation/__init__.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/compilation/constants.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/compilation/constitution.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/compilation/constitution_block.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/compilation/injector.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/compilation/template_builder.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/config.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/core/__init__.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/core/conflict_detector.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/core/docker_args.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/core/operations.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/deps/__init__.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/deps/aggregator.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/deps/collection_parser.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/deps/dependency_graph.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/deps/package_validator.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/deps/verifier.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/drift.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/integration/utils.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/models/__init__.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/models/apm_package.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/models/plugin.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/output/__init__.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/output/models.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/primitives/__init__.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/primitives/discovery.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/primitives/parser.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/registry/__init__.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/registry/client.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/registry/integration.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/runtime/__init__.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/runtime/base.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/runtime/codex_runtime.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/runtime/copilot_runtime.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/runtime/factory.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/runtime/llm_runtime.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/utils/__init__.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/utils/version_checker.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/version.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/workflow/__init__.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/workflow/discovery.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/workflow/parser.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli/workflow/runner.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli.egg-info/dependency_links.txt +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli.egg-info/entry_points.txt +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/src/apm_cli.egg-info/top_level.txt +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/tests/test_codex_docker_args_fix.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/tests/test_codex_empty_string_and_defaults.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/tests/test_collision_integration.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/tests/test_console.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/tests/test_distributed_compilation.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/tests/test_empty_string_and_defaults.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/tests/test_github_downloader_token_precedence.py +0 -0
- {apm_cli-0.7.8 → apm_cli-0.8.0}/tests/test_runtime_manager_token_precedence.py +0 -0
- {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.
|
|
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>=
|
|
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)
|
|
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)
|
|
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
|
+
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>=
|
|
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"
|
|
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
|
|
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"
|
|
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
|
|
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"
|
|
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
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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
|
|
53
|
-
target: Target filter
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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,
|