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