apm-cli 0.7.9__tar.gz → 0.8.1__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.9/src/apm_cli.egg-info → apm_cli-0.8.1}/PKG-INFO +6 -5
- {apm_cli-0.7.9 → apm_cli-0.8.1}/README.md +5 -4
- {apm_cli-0.7.9 → apm_cli-0.8.1}/pyproject.toml +1 -1
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/adapters/client/base.py +33 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/adapters/client/codex.py +18 -3
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/adapters/client/copilot.py +5 -0
- apm_cli-0.8.1/src/apm_cli/adapters/client/cursor.py +138 -0
- apm_cli-0.8.1/src/apm_cli/adapters/client/opencode.py +157 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/adapters/client/vscode.py +37 -1
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/bundle/packer.py +38 -3
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/bundle/unpacker.py +50 -3
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/cli.py +2 -0
- apm_cli-0.8.1/src/apm_cli/commands/audit.py +570 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/commands/compile.py +28 -2
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/commands/install.py +379 -31
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/commands/pack.py +14 -2
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/commands/uninstall.py +29 -1
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/compilation/agents_compiler.py +20 -2
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/compilation/claude_formatter.py +17 -1
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/core/target_detection.py +45 -17
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/core/token_manager.py +64 -2
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/deps/github_downloader.py +125 -15
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/drift.py +7 -1
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/factory.py +4 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/integration/__init__.py +12 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/integration/agent_integrator.py +197 -3
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/integration/base_integrator.py +33 -8
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/integration/command_integrator.py +73 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/integration/hook_integrator.py +168 -4
- apm_cli-0.8.1/src/apm_cli/integration/instruction_integrator.py +261 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/integration/mcp_integrator.py +72 -4
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/integration/skill_integrator.py +83 -35
- apm_cli-0.8.1/src/apm_cli/integration/targets.py +180 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/models/dependency.py +7 -28
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/registry/client.py +28 -27
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/registry/operations.py +16 -2
- apm_cli-0.8.1/src/apm_cli/security/__init__.py +24 -0
- apm_cli-0.8.1/src/apm_cli/security/audit_report.py +251 -0
- apm_cli-0.8.1/src/apm_cli/security/content_scanner.py +303 -0
- apm_cli-0.8.1/src/apm_cli/security/gate.py +222 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/utils/console.py +1 -1
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/utils/diagnostics.py +106 -2
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/utils/github_host.py +25 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/utils/helpers.py +3 -1
- {apm_cli-0.7.9 → apm_cli-0.8.1/src/apm_cli.egg-info}/PKG-INFO +6 -5
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli.egg-info/SOURCES.txt +9 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/tests/test_apm_package_models.py +15 -35
- {apm_cli-0.7.9 → apm_cli-0.8.1}/tests/test_enhanced_discovery.py +2 -2
- {apm_cli-0.7.9 → apm_cli-0.8.1}/tests/test_github_downloader.py +444 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/tests/test_github_downloader_token_precedence.py +5 -2
- apm_cli-0.8.1/tests/test_token_manager.py +196 -0
- apm_cli-0.7.9/src/apm_cli/integration/instruction_integrator.py +0 -114
- {apm_cli-0.7.9 → apm_cli-0.8.1}/AUTHORS +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/LICENSE +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/setup.cfg +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/__init__.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/adapters/__init__.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/adapters/client/__init__.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/adapters/package_manager/__init__.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/adapters/package_manager/base.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/adapters/package_manager/default_manager.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/bundle/__init__.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/bundle/lockfile_enrichment.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/commands/__init__.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/commands/_helpers.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/commands/config.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/commands/deps.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/commands/init.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/commands/list_cmd.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/commands/mcp.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/commands/prune.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/commands/run.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/commands/runtime.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/commands/update.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/compilation/__init__.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/compilation/constants.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/compilation/constitution.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/compilation/constitution_block.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/compilation/context_optimizer.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/compilation/distributed_compiler.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/compilation/injector.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/compilation/link_resolver.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/compilation/template_builder.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/config.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/core/__init__.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/core/conflict_detector.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/core/docker_args.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/core/operations.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/core/safe_installer.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/core/script_runner.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/deps/__init__.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/deps/aggregator.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/deps/apm_resolver.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/deps/collection_parser.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/deps/dependency_graph.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/deps/lockfile.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/deps/package_validator.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/deps/plugin_parser.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/deps/verifier.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/integration/prompt_integrator.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/integration/skill_transformer.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/integration/utils.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/models/__init__.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/models/apm_package.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/models/plugin.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/models/validation.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/output/__init__.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/output/formatters.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/output/models.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/output/script_formatters.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/primitives/__init__.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/primitives/discovery.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/primitives/models.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/primitives/parser.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/registry/__init__.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/registry/integration.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/runtime/__init__.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/runtime/base.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/runtime/codex_runtime.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/runtime/copilot_runtime.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/runtime/factory.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/runtime/llm_runtime.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/runtime/manager.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/utils/__init__.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/utils/version_checker.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/version.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/workflow/__init__.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/workflow/discovery.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/workflow/parser.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli/workflow/runner.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli.egg-info/dependency_links.txt +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli.egg-info/entry_points.txt +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli.egg-info/requires.txt +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/src/apm_cli.egg-info/top_level.txt +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/tests/test_apm_resolver.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/tests/test_codex_docker_args_fix.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/tests/test_codex_empty_string_and_defaults.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/tests/test_collision_integration.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/tests/test_console.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/tests/test_distributed_compilation.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/tests/test_empty_string_and_defaults.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/tests/test_lockfile.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/tests/test_runnable_prompts.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/tests/test_runtime_manager_token_precedence.py +0 -0
- {apm_cli-0.7.9 → apm_cli-0.8.1}/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.1
|
|
4
4
|
Summary: MCP configuration tool
|
|
5
5
|
Author-email: Daniel Meppiel <user@example.com>
|
|
6
6
|
License: MIT License
|
|
@@ -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
|
|
|
@@ -88,7 +88,7 @@ dependencies:
|
|
|
88
88
|
# Specific agent primitives from any repository
|
|
89
89
|
- github/awesome-copilot/agents/api-architect.agent.md
|
|
90
90
|
# A full APM package with instructions, skills, prompts, hooks...
|
|
91
|
-
- microsoft/apm-sample-package
|
|
91
|
+
- microsoft/apm-sample-package#v1.0.0
|
|
92
92
|
```
|
|
93
93
|
|
|
94
94
|
```bash
|
|
@@ -101,7 +101,8 @@ 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
|
|
|
@@ -148,7 +149,7 @@ pip install apm-cli
|
|
|
148
149
|
Then start adding packages:
|
|
149
150
|
|
|
150
151
|
```bash
|
|
151
|
-
apm install microsoft/apm-sample-package
|
|
152
|
+
apm install microsoft/apm-sample-package#v1.0.0
|
|
152
153
|
```
|
|
153
154
|
|
|
154
155
|
See the **[Getting Started guide](https://microsoft.github.io/apm/getting-started/quick-start/)** for the full walkthrough.
|
|
@@ -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
|
|
|
@@ -27,7 +27,7 @@ dependencies:
|
|
|
27
27
|
# Specific agent primitives from any repository
|
|
28
28
|
- github/awesome-copilot/agents/api-architect.agent.md
|
|
29
29
|
# A full APM package with instructions, skills, prompts, hooks...
|
|
30
|
-
- microsoft/apm-sample-package
|
|
30
|
+
- microsoft/apm-sample-package#v1.0.0
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
```bash
|
|
@@ -40,7 +40,8 @@ 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
|
|
|
@@ -87,7 +88,7 @@ pip install apm-cli
|
|
|
87
88
|
Then start adding packages:
|
|
88
89
|
|
|
89
90
|
```bash
|
|
90
|
-
apm install microsoft/apm-sample-package
|
|
91
|
+
apm install microsoft/apm-sample-package#v1.0.0
|
|
91
92
|
```
|
|
92
93
|
|
|
93
94
|
See the **[Getting Started guide](https://microsoft.github.io/apm/getting-started/quick-start/)** for the full walkthrough.
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
"""Base adapter interface for MCP clients."""
|
|
2
2
|
|
|
3
|
+
import re
|
|
3
4
|
from abc import ABC, abstractmethod
|
|
4
5
|
|
|
6
|
+
_INPUT_VAR_RE = re.compile(r"\$\{input:([^}]+)\}")
|
|
7
|
+
|
|
5
8
|
|
|
6
9
|
class MCPClientAdapter(ABC):
|
|
7
10
|
"""Base adapter for MCP clients."""
|
|
@@ -84,3 +87,33 @@ class MCPClientAdapter(ABC):
|
|
|
84
87
|
return "nuget"
|
|
85
88
|
|
|
86
89
|
return ""
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def _warn_input_variables(mapping, server_name, runtime_label):
|
|
93
|
+
"""Emit a warning for each ``${input:...}`` reference found in *mapping*.
|
|
94
|
+
|
|
95
|
+
Runtimes that do not support VS Code-style input prompts (Copilot CLI,
|
|
96
|
+
Codex CLI, etc.) should call this so users know their placeholders
|
|
97
|
+
will not be resolved at runtime.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
mapping (dict): Header or env dict to scan.
|
|
101
|
+
server_name (str): Server name for the warning message.
|
|
102
|
+
runtime_label (str): Human-readable runtime name (e.g. "Copilot CLI").
|
|
103
|
+
"""
|
|
104
|
+
if not mapping:
|
|
105
|
+
return
|
|
106
|
+
seen: set = set()
|
|
107
|
+
for value in mapping.values():
|
|
108
|
+
if not isinstance(value, str):
|
|
109
|
+
continue
|
|
110
|
+
for match in _INPUT_VAR_RE.finditer(value):
|
|
111
|
+
var_id = match.group(1)
|
|
112
|
+
if var_id in seen:
|
|
113
|
+
continue
|
|
114
|
+
seen.add(var_id)
|
|
115
|
+
print(
|
|
116
|
+
f"[!] Warning: ${{input:{var_id}}} in server "
|
|
117
|
+
f"'{server_name}' will not be resolved \u2014 "
|
|
118
|
+
f"{runtime_label} does not support input variable prompts"
|
|
119
|
+
)
|
|
@@ -181,6 +181,7 @@ class CodexClientAdapter(MCPClientAdapter):
|
|
|
181
181
|
config["args"] = raw["args"]
|
|
182
182
|
if raw.get("env"):
|
|
183
183
|
config["env"] = raw["env"]
|
|
184
|
+
self._warn_input_variables(raw["env"], server_info.get("name", ""), "Codex CLI")
|
|
184
185
|
return config
|
|
185
186
|
|
|
186
187
|
# Note: Remote servers (SSE type) are handled in configure_mcp_server and rejected early
|
|
@@ -218,10 +219,24 @@ class CodexClientAdapter(MCPClientAdapter):
|
|
|
218
219
|
# Generate command and args based on package type
|
|
219
220
|
if registry_name == "npm":
|
|
220
221
|
config["command"] = runtime_hint or "npx"
|
|
221
|
-
# Always include package name; filter duplicates from legacy runtime_arguments
|
|
222
222
|
all_args = processed_runtime_args + processed_package_args
|
|
223
|
-
|
|
224
|
-
|
|
223
|
+
if all_args:
|
|
224
|
+
# If runtime_arguments already include the package (bare or
|
|
225
|
+
# versioned), use them as-is — they are authoritative from
|
|
226
|
+
# the registry and may carry a version pin.
|
|
227
|
+
has_pkg = any(
|
|
228
|
+
a == package_name or a.startswith(f"{package_name}@")
|
|
229
|
+
for a in all_args
|
|
230
|
+
)
|
|
231
|
+
if has_pkg:
|
|
232
|
+
config["args"] = all_args
|
|
233
|
+
else:
|
|
234
|
+
# Legacy: runtime_arguments don't mention the package,
|
|
235
|
+
# prepend -y + bare name ourselves.
|
|
236
|
+
extra_args = [a for a in all_args if a != "-y"]
|
|
237
|
+
config["args"] = ["-y", package_name] + extra_args
|
|
238
|
+
else:
|
|
239
|
+
config["args"] = ["-y", package_name]
|
|
225
240
|
# For NPM packages, also use env block for environment variables
|
|
226
241
|
if resolved_env:
|
|
227
242
|
config["env"] = resolved_env
|
|
@@ -173,6 +173,7 @@ class CopilotClientAdapter(MCPClientAdapter):
|
|
|
173
173
|
config["args"] = raw["args"]
|
|
174
174
|
if raw.get("env"):
|
|
175
175
|
config["env"] = raw["env"]
|
|
176
|
+
self._warn_input_variables(raw["env"], server_info.get("name", ""), "Copilot CLI")
|
|
176
177
|
# Apply tools override if present
|
|
177
178
|
tools_override = server_info.get("_apm_tools_override")
|
|
178
179
|
if tools_override:
|
|
@@ -218,6 +219,10 @@ class CopilotClientAdapter(MCPClientAdapter):
|
|
|
218
219
|
resolved_value = self._resolve_env_variable(header_name, header_value, env_overrides)
|
|
219
220
|
config["headers"][header_name] = resolved_value
|
|
220
221
|
|
|
222
|
+
# Warn about unresolvable ${input:...} references in headers
|
|
223
|
+
if config.get("headers"):
|
|
224
|
+
self._warn_input_variables(config["headers"], server_info.get("name", ""), "Copilot CLI")
|
|
225
|
+
|
|
221
226
|
# Apply tools override from MCP dependency overlay if present
|
|
222
227
|
tools_override = server_info.get("_apm_tools_override")
|
|
223
228
|
if tools_override:
|
|
@@ -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
|
|
@@ -8,7 +8,7 @@ https://code.visualstudio.com/docs/copilot/chat/mcp-servers
|
|
|
8
8
|
import json
|
|
9
9
|
import os
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from .base import MCPClientAdapter
|
|
11
|
+
from .base import MCPClientAdapter, _INPUT_VAR_RE
|
|
12
12
|
from ...registry.client import SimpleRegistryClient
|
|
13
13
|
from ...registry.integration import RegistryIntegration
|
|
14
14
|
|
|
@@ -197,6 +197,9 @@ class VSCodeClientAdapter(MCPClientAdapter):
|
|
|
197
197
|
}
|
|
198
198
|
if raw.get("env"):
|
|
199
199
|
server_config["env"] = raw["env"]
|
|
200
|
+
input_vars.extend(
|
|
201
|
+
self._extract_input_variables(raw["env"], server_info.get("name", ""))
|
|
202
|
+
)
|
|
200
203
|
return server_config, input_vars
|
|
201
204
|
|
|
202
205
|
# Check for packages information
|
|
@@ -308,6 +311,9 @@ class VSCodeClientAdapter(MCPClientAdapter):
|
|
|
308
311
|
"url": remote.get("url", ""),
|
|
309
312
|
"headers": headers,
|
|
310
313
|
}
|
|
314
|
+
input_vars.extend(
|
|
315
|
+
self._extract_input_variables(headers, server_info.get("name", ""))
|
|
316
|
+
)
|
|
311
317
|
# If no packages AND no endpoints/remotes, fail with clear error
|
|
312
318
|
else:
|
|
313
319
|
packages = server_info.get("packages", [])
|
|
@@ -323,6 +329,36 @@ class VSCodeClientAdapter(MCPClientAdapter):
|
|
|
323
329
|
|
|
324
330
|
return server_config, input_vars
|
|
325
331
|
|
|
332
|
+
def _extract_input_variables(self, mapping, server_name):
|
|
333
|
+
"""Scan dict values for ${input:...} references and return input variable definitions.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
mapping (dict): Header or env dict whose values may contain
|
|
337
|
+
``${input:<id>}`` placeholders.
|
|
338
|
+
server_name (str): Server name used in the description field.
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
list[dict]: Input variable definitions (``promptString``, ``password: true``).
|
|
342
|
+
Duplicates within *mapping* are already deduplicated.
|
|
343
|
+
"""
|
|
344
|
+
seen: set = set()
|
|
345
|
+
result: list = []
|
|
346
|
+
for value in (mapping or {}).values():
|
|
347
|
+
if not isinstance(value, str):
|
|
348
|
+
continue
|
|
349
|
+
for match in _INPUT_VAR_RE.finditer(value):
|
|
350
|
+
var_id = match.group(1)
|
|
351
|
+
if var_id in seen:
|
|
352
|
+
continue
|
|
353
|
+
seen.add(var_id)
|
|
354
|
+
result.append({
|
|
355
|
+
"type": "promptString",
|
|
356
|
+
"id": var_id,
|
|
357
|
+
"description": f"{var_id} for MCP server {server_name}",
|
|
358
|
+
"password": True,
|
|
359
|
+
})
|
|
360
|
+
return result
|
|
361
|
+
|
|
326
362
|
@staticmethod
|
|
327
363
|
def _extract_package_args(package):
|
|
328
364
|
"""Extract positional arguments from a package entry.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
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
|
|
@@ -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
|
|
|
@@ -153,6 +156,35 @@ def pack_bundle(
|
|
|
153
156
|
lockfile_enriched=True,
|
|
154
157
|
)
|
|
155
158
|
|
|
159
|
+
# 5b. Scan files for hidden characters before bundling.
|
|
160
|
+
# Intentionally non-blocking (warn only) — pack is an authoring tool.
|
|
161
|
+
# Critical findings here mean the author's own source files contain
|
|
162
|
+
# hidden characters. We surface them so the author can fix before
|
|
163
|
+
# publishing, but don't block the bundle. Consumers are protected by
|
|
164
|
+
# install/unpack which block on critical.
|
|
165
|
+
from ..security.gate import WARN_POLICY, SecurityGate
|
|
166
|
+
from ..utils.console import _rich_warning
|
|
167
|
+
|
|
168
|
+
_scan_findings_total = 0
|
|
169
|
+
for rel_path in unique_files:
|
|
170
|
+
src = project_root / rel_path
|
|
171
|
+
if src.is_symlink():
|
|
172
|
+
continue
|
|
173
|
+
if src.is_dir():
|
|
174
|
+
verdict = SecurityGate.scan_files(src, policy=WARN_POLICY)
|
|
175
|
+
_scan_findings_total += len(verdict.all_findings)
|
|
176
|
+
elif src.is_file():
|
|
177
|
+
verdict = SecurityGate.scan_text(
|
|
178
|
+
src.read_text(encoding="utf-8", errors="replace"),
|
|
179
|
+
str(src), policy=WARN_POLICY,
|
|
180
|
+
)
|
|
181
|
+
_scan_findings_total += len(verdict.all_findings)
|
|
182
|
+
if _scan_findings_total:
|
|
183
|
+
_rich_warning(
|
|
184
|
+
f"Bundle contains {_scan_findings_total} hidden character(s) across source files "
|
|
185
|
+
f"— run 'apm audit' to inspect before publishing"
|
|
186
|
+
)
|
|
187
|
+
|
|
156
188
|
# 6. Build output directory
|
|
157
189
|
bundle_dir = output_dir / f"{pkg_name}-{pkg_version}"
|
|
158
190
|
bundle_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -160,12 +192,15 @@ def pack_bundle(
|
|
|
160
192
|
# 7. Copy files preserving directory structure
|
|
161
193
|
for rel_path in unique_files:
|
|
162
194
|
src = project_root / rel_path
|
|
195
|
+
if src.is_symlink():
|
|
196
|
+
continue # Never bundle symlinks
|
|
163
197
|
dest = bundle_dir / rel_path
|
|
164
198
|
if src.is_dir():
|
|
165
|
-
|
|
199
|
+
from ..security.gate import ignore_symlinks
|
|
200
|
+
shutil.copytree(src, dest, dirs_exist_ok=True, ignore=ignore_symlinks)
|
|
166
201
|
else:
|
|
167
202
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
168
|
-
shutil.copy2(src, dest)
|
|
203
|
+
shutil.copy2(src, dest, follow_symlinks=False)
|
|
169
204
|
|
|
170
205
|
# 8. Enrich lockfile copy and write to bundle
|
|
171
206
|
enriched_yaml = enrich_lockfile_for_pack(lockfile, fmt, effective_target)
|