apm-cli 0.8.5__tar.gz → 0.8.6__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.5/src/apm_cli.egg-info → apm_cli-0.8.6}/PKG-INFO +1 -1
- {apm_cli-0.8.5 → apm_cli-0.8.6}/pyproject.toml +1 -1
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/commands/install.py +119 -56
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/compilation/claude_formatter.py +4 -1
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/compilation/context_optimizer.py +3 -2
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/compilation/distributed_compiler.py +4 -1
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/compilation/template_builder.py +3 -3
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/core/auth.py +23 -18
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/deps/github_downloader.py +29 -41
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/deps/lockfile.py +20 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/integration/skill_integrator.py +107 -140
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/integration/targets.py +40 -13
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/models/dependency/reference.py +314 -239
- apm_cli-0.8.6/src/apm_cli/utils/file_ops.py +294 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/utils/path_security.py +50 -6
- {apm_cli-0.8.5 → apm_cli-0.8.6/src/apm_cli.egg-info}/PKG-INFO +1 -1
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli.egg-info/SOURCES.txt +1 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/tests/test_apm_package_models.py +463 -348
- {apm_cli-0.8.5 → apm_cli-0.8.6}/tests/test_github_downloader.py +9 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/tests/test_lockfile.py +68 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/AUTHORS +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/LICENSE +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/README.md +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/setup.cfg +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/__init__.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/adapters/__init__.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/adapters/client/__init__.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/adapters/client/base.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/adapters/client/codex.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/adapters/client/copilot.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/adapters/client/cursor.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/adapters/client/opencode.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/adapters/client/vscode.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/adapters/package_manager/__init__.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/adapters/package_manager/base.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/adapters/package_manager/default_manager.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/bundle/__init__.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/bundle/lockfile_enrichment.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/bundle/packer.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/bundle/plugin_exporter.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/bundle/unpacker.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/cli.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/commands/__init__.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/commands/_helpers.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/commands/audit.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/commands/compile/__init__.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/commands/compile/cli.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/commands/compile/watcher.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/commands/config.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/commands/deps/__init__.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/commands/deps/_utils.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/commands/deps/cli.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/commands/init.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/commands/list_cmd.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/commands/mcp.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/commands/pack.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/commands/prune.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/commands/run.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/commands/runtime.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/commands/uninstall/__init__.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/commands/uninstall/cli.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/commands/uninstall/engine.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/commands/update.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/compilation/__init__.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/compilation/agents_compiler.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/compilation/constants.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/compilation/constitution.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/compilation/constitution_block.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/compilation/injector.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/compilation/link_resolver.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/config.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/constants.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/core/__init__.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/core/command_logger.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/core/conflict_detector.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/core/docker_args.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/core/operations.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/core/safe_installer.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/core/script_runner.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/core/target_detection.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/core/token_manager.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/deps/__init__.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/deps/aggregator.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/deps/apm_resolver.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/deps/collection_parser.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/deps/dependency_graph.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/deps/package_validator.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/deps/plugin_parser.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/deps/verifier.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/drift.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/factory.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/integration/__init__.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/integration/agent_integrator.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/integration/base_integrator.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/integration/command_integrator.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/integration/hook_integrator.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/integration/instruction_integrator.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/integration/mcp_integrator.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/integration/prompt_integrator.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/integration/skill_transformer.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/integration/utils.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/models/__init__.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/models/apm_package.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/models/dependency/__init__.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/models/dependency/mcp.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/models/dependency/types.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/models/plugin.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/models/results.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/models/validation.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/output/__init__.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/output/formatters.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/output/models.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/output/script_formatters.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/policy/__init__.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/policy/ci_checks.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/policy/discovery.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/policy/inheritance.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/policy/matcher.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/policy/models.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/policy/parser.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/policy/policy_checks.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/policy/schema.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/primitives/__init__.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/primitives/discovery.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/primitives/models.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/primitives/parser.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/registry/__init__.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/registry/client.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/registry/integration.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/registry/operations.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/runtime/__init__.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/runtime/base.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/runtime/codex_runtime.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/runtime/copilot_runtime.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/runtime/factory.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/runtime/llm_runtime.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/runtime/manager.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/security/__init__.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/security/audit_report.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/security/content_scanner.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/security/file_scanner.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/security/gate.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/utils/__init__.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/utils/console.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/utils/content_hash.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/utils/diagnostics.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/utils/github_host.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/utils/helpers.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/utils/paths.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/utils/version_checker.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/utils/yaml_io.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/version.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/workflow/__init__.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/workflow/discovery.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/workflow/parser.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli/workflow/runner.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli.egg-info/dependency_links.txt +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli.egg-info/entry_points.txt +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli.egg-info/requires.txt +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/src/apm_cli.egg-info/top_level.txt +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/tests/test_apm_resolver.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/tests/test_codex_docker_args_fix.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/tests/test_codex_empty_string_and_defaults.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/tests/test_collision_integration.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/tests/test_console.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/tests/test_distributed_compilation.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/tests/test_empty_string_and_defaults.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/tests/test_enhanced_discovery.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/tests/test_github_downloader_token_precedence.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/tests/test_runnable_prompts.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/tests/test_runtime_manager_token_precedence.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/tests/test_token_manager.py +0 -0
- {apm_cli-0.8.5 → apm_cli-0.8.6}/tests/test_virtual_package_multi_install.py +0 -0
|
@@ -35,6 +35,12 @@ set = builtins.set
|
|
|
35
35
|
list = builtins.list
|
|
36
36
|
dict = builtins.dict
|
|
37
37
|
|
|
38
|
+
# AuthResolver has no optional deps (stdlib + internal utils only), so it must
|
|
39
|
+
# be imported unconditionally here -- NOT inside the APM_DEPS_AVAILABLE guard.
|
|
40
|
+
# If it were gated, a missing optional dep (e.g. GitPython) would cause a
|
|
41
|
+
# NameError in install() before the graceful APM_DEPS_AVAILABLE check fires.
|
|
42
|
+
from ..core.auth import AuthResolver
|
|
43
|
+
|
|
38
44
|
# APM Dependencies (conditional import for graceful degradation)
|
|
39
45
|
APM_DEPS_AVAILABLE = False
|
|
40
46
|
_APM_IMPORT_ERROR = None
|
|
@@ -56,7 +62,7 @@ except ImportError as e:
|
|
|
56
62
|
# ---------------------------------------------------------------------------
|
|
57
63
|
|
|
58
64
|
|
|
59
|
-
def _validate_and_add_packages_to_apm_yml(packages, dry_run=False, dev=False, logger=None):
|
|
65
|
+
def _validate_and_add_packages_to_apm_yml(packages, dry_run=False, dev=False, logger=None, auth_resolver=None):
|
|
60
66
|
"""Validate packages exist and can be accessed, then add to apm.yml dependencies section.
|
|
61
67
|
|
|
62
68
|
Implements normalize-on-write: any input form (HTTPS URL, SSH URL, FQDN, shorthand)
|
|
@@ -68,6 +74,7 @@ def _validate_and_add_packages_to_apm_yml(packages, dry_run=False, dev=False, lo
|
|
|
68
74
|
dry_run: If True, only show what would be added.
|
|
69
75
|
dev: If True, write to devDependencies instead of dependencies.
|
|
70
76
|
logger: InstallLogger for structured output.
|
|
77
|
+
auth_resolver: Shared auth resolver for caching credentials.
|
|
71
78
|
|
|
72
79
|
Returns:
|
|
73
80
|
Tuple of (validated_packages list, _ValidationOutcome).
|
|
@@ -146,7 +153,7 @@ def _validate_and_add_packages_to_apm_yml(packages, dry_run=False, dev=False, lo
|
|
|
146
153
|
|
|
147
154
|
# Validate package exists and is accessible
|
|
148
155
|
verbose = bool(logger and logger.verbose)
|
|
149
|
-
if _validate_package_exists(package, verbose=verbose):
|
|
156
|
+
if _validate_package_exists(package, verbose=verbose, auth_resolver=auth_resolver):
|
|
150
157
|
valid_outcomes.append((canonical, already_in_deps))
|
|
151
158
|
if logger:
|
|
152
159
|
logger.validation_pass(canonical, already_present=already_in_deps)
|
|
@@ -258,7 +265,7 @@ def _local_path_no_markers_hint(local_dir, verbose_log=None):
|
|
|
258
265
|
_rich_echo(f" ... and {len(found) - 5} more", color="dim")
|
|
259
266
|
|
|
260
267
|
|
|
261
|
-
def _validate_package_exists(package, verbose=False):
|
|
268
|
+
def _validate_package_exists(package, verbose=False, auth_resolver=None):
|
|
262
269
|
"""Validate that a package exists and is accessible on GitHub, Azure DevOps, or locally."""
|
|
263
270
|
import os
|
|
264
271
|
import subprocess
|
|
@@ -266,7 +273,9 @@ def _validate_package_exists(package, verbose=False):
|
|
|
266
273
|
from apm_cli.core.auth import AuthResolver
|
|
267
274
|
|
|
268
275
|
verbose_log = (lambda msg: _rich_echo(f" {msg}", color="dim")) if verbose else None
|
|
269
|
-
|
|
276
|
+
# Use provided resolver or create new one if not in a CLI session context
|
|
277
|
+
if auth_resolver is None:
|
|
278
|
+
auth_resolver = AuthResolver()
|
|
270
279
|
|
|
271
280
|
try:
|
|
272
281
|
# Parse the package to check if it's a virtual package or ADO
|
|
@@ -300,8 +309,8 @@ def _validate_package_exists(package, verbose=False):
|
|
|
300
309
|
org = dep_ref.repo_url.split('/')[0] if dep_ref.repo_url and '/' in dep_ref.repo_url else None
|
|
301
310
|
if verbose_log:
|
|
302
311
|
verbose_log(f"Auth resolved: host={host}, org={org}, source={ctx.source}, type={ctx.token_type}")
|
|
303
|
-
|
|
304
|
-
result =
|
|
312
|
+
virtual_downloader = GitHubPackageDownloader(auth_resolver=auth_resolver)
|
|
313
|
+
result = virtual_downloader.validate_virtual_package_exists(dep_ref)
|
|
305
314
|
if not result and verbose_log:
|
|
306
315
|
try:
|
|
307
316
|
err_ctx = auth_resolver.build_error_context(host, f"accessing {package}", org=org)
|
|
@@ -316,26 +325,39 @@ def _validate_package_exists(package, verbose=False):
|
|
|
316
325
|
if dep_ref.is_azure_devops() or (dep_ref.host and dep_ref.host != "github.com"):
|
|
317
326
|
from apm_cli.utils.github_host import is_github_hostname, is_azure_devops_hostname
|
|
318
327
|
|
|
319
|
-
|
|
328
|
+
# Determine host type before building the URL so we know whether to
|
|
329
|
+
# embed a token. Generic (non-GitHub, non-ADO) hosts are excluded
|
|
330
|
+
# from APM-managed auth; they rely on git credential helpers via the
|
|
331
|
+
# relaxed validate_env below.
|
|
332
|
+
is_generic = not is_github_hostname(dep_ref.host) and not is_azure_devops_hostname(dep_ref.host)
|
|
333
|
+
|
|
334
|
+
# For GHES / ADO: resolve per-dependency auth up front so the URL
|
|
335
|
+
# carries an embedded token and avoids triggering OS credential
|
|
336
|
+
# helper popups during git ls-remote validation.
|
|
337
|
+
_url_token = None
|
|
338
|
+
if not is_generic:
|
|
339
|
+
_dep_ctx = auth_resolver.resolve_for_dep(dep_ref)
|
|
340
|
+
_url_token = _dep_ctx.token
|
|
341
|
+
|
|
342
|
+
ado_downloader = GitHubPackageDownloader(auth_resolver=auth_resolver)
|
|
320
343
|
# Set the host
|
|
321
344
|
if dep_ref.host:
|
|
322
|
-
|
|
345
|
+
ado_downloader.github_host = dep_ref.host
|
|
323
346
|
|
|
324
|
-
# Build authenticated URL using
|
|
325
|
-
package_url =
|
|
326
|
-
dep_ref.repo_url, use_ssh=False, dep_ref=dep_ref
|
|
347
|
+
# Build authenticated URL using the resolved per-dep token.
|
|
348
|
+
package_url = ado_downloader._build_repo_url(
|
|
349
|
+
dep_ref.repo_url, use_ssh=False, dep_ref=dep_ref, token=_url_token
|
|
327
350
|
)
|
|
328
351
|
|
|
329
352
|
# For generic hosts (not GitHub, not ADO), relax the env so native
|
|
330
353
|
# credential helpers (SSH keys, macOS Keychain, etc.) can work.
|
|
331
354
|
# This mirrors _clone_with_fallback() which does the same relaxation.
|
|
332
|
-
is_generic = not is_github_hostname(dep_ref.host) and not is_azure_devops_hostname(dep_ref.host)
|
|
333
355
|
if is_generic:
|
|
334
|
-
validate_env = {k: v for k, v in
|
|
356
|
+
validate_env = {k: v for k, v in ado_downloader.git_env.items()
|
|
335
357
|
if k not in ('GIT_ASKPASS', 'GIT_CONFIG_GLOBAL', 'GIT_CONFIG_NOSYSTEM')}
|
|
336
358
|
validate_env['GIT_TERMINAL_PROMPT'] = '0'
|
|
337
359
|
else:
|
|
338
|
-
validate_env = {**os.environ, **
|
|
360
|
+
validate_env = {**os.environ, **ado_downloader.git_env}
|
|
339
361
|
|
|
340
362
|
if verbose_log:
|
|
341
363
|
verbose_log(f"Trying git ls-remote for {dep_ref.host}")
|
|
@@ -513,8 +535,19 @@ def _validate_package_exists(package, verbose=False):
|
|
|
513
535
|
default=False,
|
|
514
536
|
help="Install as development dependency (devDependencies)",
|
|
515
537
|
)
|
|
538
|
+
@click.option(
|
|
539
|
+
"--target",
|
|
540
|
+
"-t",
|
|
541
|
+
"target",
|
|
542
|
+
type=click.Choice(
|
|
543
|
+
["copilot", "claude", "cursor", "opencode", "vscode", "agents", "all"],
|
|
544
|
+
case_sensitive=False,
|
|
545
|
+
),
|
|
546
|
+
default=None,
|
|
547
|
+
help="Force deployment to a specific target (overrides auto-detection)",
|
|
548
|
+
)
|
|
516
549
|
@click.pass_context
|
|
517
|
-
def install(ctx, packages, runtime, exclude, only, update, dry_run, force, verbose, trust_transitive_mcp, parallel_downloads, dev):
|
|
550
|
+
def install(ctx, packages, runtime, exclude, only, update, dry_run, force, verbose, trust_transitive_mcp, parallel_downloads, dev, target):
|
|
518
551
|
"""Install APM and MCP dependencies from apm.yml (like npm install).
|
|
519
552
|
|
|
520
553
|
This command automatically detects AI runtimes from your apm.yml scripts and installs
|
|
@@ -539,6 +572,10 @@ def install(ctx, packages, runtime, exclude, only, update, dry_run, force, verbo
|
|
|
539
572
|
is_partial = bool(packages)
|
|
540
573
|
logger = InstallLogger(verbose=verbose, dry_run=dry_run, partial=is_partial)
|
|
541
574
|
|
|
575
|
+
# Create shared auth resolver for all downloads in this CLI invocation
|
|
576
|
+
# to ensure credentials are cached and reused (prevents duplicate auth popups)
|
|
577
|
+
auth_resolver = AuthResolver()
|
|
578
|
+
|
|
542
579
|
# Check if apm.yml exists
|
|
543
580
|
apm_yml_exists = Path(APM_YML_FILENAME).exists()
|
|
544
581
|
|
|
@@ -560,7 +597,7 @@ def install(ctx, packages, runtime, exclude, only, update, dry_run, force, verbo
|
|
|
560
597
|
# If packages are specified, validate and add them to apm.yml first
|
|
561
598
|
if packages:
|
|
562
599
|
validated_packages, outcome = _validate_and_add_packages_to_apm_yml(
|
|
563
|
-
packages, dry_run, dev=dev, logger=logger,
|
|
600
|
+
packages, dry_run, dev=dev, logger=logger, auth_resolver=auth_resolver,
|
|
564
601
|
)
|
|
565
602
|
# Short-circuit: all packages failed validation — nothing to install
|
|
566
603
|
if outcome.all_failed:
|
|
@@ -661,6 +698,8 @@ def install(ctx, packages, runtime, exclude, only, update, dry_run, force, verbo
|
|
|
661
698
|
apm_package, update, verbose, only_pkgs, force=force,
|
|
662
699
|
parallel_downloads=parallel_downloads,
|
|
663
700
|
logger=logger,
|
|
701
|
+
auth_resolver=auth_resolver,
|
|
702
|
+
target=target,
|
|
664
703
|
)
|
|
665
704
|
apm_count = install_result.installed_count
|
|
666
705
|
prompt_count = install_result.prompts_integrated
|
|
@@ -874,12 +913,20 @@ def _integrate_package_primitives(
|
|
|
874
913
|
package_info, project_root,
|
|
875
914
|
diagnostics=diagnostics, managed_files=managed_files, force=force,
|
|
876
915
|
)
|
|
916
|
+
# Build human-readable list of target dirs from deployed paths
|
|
917
|
+
_skill_target_dirs: set[str] = set()
|
|
918
|
+
for tp in skill_result.target_paths:
|
|
919
|
+
rel = tp.relative_to(project_root)
|
|
920
|
+
if rel.parts:
|
|
921
|
+
_skill_target_dirs.add(rel.parts[0])
|
|
922
|
+
_skill_targets = sorted(_skill_target_dirs)
|
|
923
|
+
_skill_target_str = ", ".join(f"{d}/skills/" for d in _skill_targets) or "skills/"
|
|
877
924
|
if skill_result.skill_created:
|
|
878
925
|
result["skills"] += 1
|
|
879
|
-
_log_integration(f"
|
|
926
|
+
_log_integration(f" |-- Skill integrated -> {_skill_target_str}")
|
|
880
927
|
if skill_result.sub_skills_promoted > 0:
|
|
881
928
|
result["sub_skills"] += skill_result.sub_skills_promoted
|
|
882
|
-
_log_integration(f"
|
|
929
|
+
_log_integration(f" |-- {skill_result.sub_skills_promoted} skill(s) integrated -> {_skill_target_str}")
|
|
883
930
|
for tp in skill_result.target_paths:
|
|
884
931
|
deployed.append(tp.relative_to(project_root).as_posix())
|
|
885
932
|
|
|
@@ -951,19 +998,20 @@ def _integrate_package_primitives(
|
|
|
951
998
|
deployed.append(tp.relative_to(project_root).as_posix())
|
|
952
999
|
|
|
953
1000
|
# --- commands (.claude) ---
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1001
|
+
if integrate_claude:
|
|
1002
|
+
command_result = command_integrator.integrate_package_commands(
|
|
1003
|
+
package_info, project_root,
|
|
1004
|
+
force=force, managed_files=managed_files,
|
|
1005
|
+
diagnostics=diagnostics,
|
|
1006
|
+
)
|
|
1007
|
+
if command_result.files_integrated > 0:
|
|
1008
|
+
result["commands"] += command_result.files_integrated
|
|
1009
|
+
_log_integration(f" └─ {command_result.files_integrated} commands integrated -> .claude/commands/")
|
|
1010
|
+
if command_result.files_updated > 0:
|
|
1011
|
+
_log_integration(f" └─ {command_result.files_updated} commands updated")
|
|
1012
|
+
result["links_resolved"] += command_result.links_resolved
|
|
1013
|
+
for tp in command_result.target_paths:
|
|
1014
|
+
deployed.append(tp.relative_to(project_root).as_posix())
|
|
967
1015
|
|
|
968
1016
|
# --- OpenCode commands (.opencode) ---
|
|
969
1017
|
opencode_command_result = command_integrator.integrate_package_commands_opencode(
|
|
@@ -1071,6 +1119,8 @@ def _install_apm_dependencies(
|
|
|
1071
1119
|
force: bool = False,
|
|
1072
1120
|
parallel_downloads: int = 4,
|
|
1073
1121
|
logger: "InstallLogger" = None,
|
|
1122
|
+
auth_resolver: "AuthResolver" = None,
|
|
1123
|
+
target: str = None,
|
|
1074
1124
|
):
|
|
1075
1125
|
"""Install APM package dependencies.
|
|
1076
1126
|
|
|
@@ -1082,6 +1132,8 @@ def _install_apm_dependencies(
|
|
|
1082
1132
|
force: Whether to overwrite locally-authored files on collision
|
|
1083
1133
|
parallel_downloads: Max concurrent downloads (0 disables parallelism)
|
|
1084
1134
|
logger: InstallLogger for structured output
|
|
1135
|
+
auth_resolver: Shared auth resolver for caching credentials
|
|
1136
|
+
target: Explicit target override from --target CLI flag
|
|
1085
1137
|
"""
|
|
1086
1138
|
if not APM_DEPS_AVAILABLE:
|
|
1087
1139
|
raise RuntimeError("APM dependency system not available")
|
|
@@ -1114,8 +1166,12 @@ def _install_apm_dependencies(
|
|
|
1114
1166
|
apm_modules_dir = project_root / APM_MODULES_DIR
|
|
1115
1167
|
apm_modules_dir.mkdir(exist_ok=True)
|
|
1116
1168
|
|
|
1169
|
+
# Use provided resolver or create new one if not in a CLI session context
|
|
1170
|
+
if auth_resolver is None:
|
|
1171
|
+
auth_resolver = AuthResolver()
|
|
1172
|
+
|
|
1117
1173
|
# Create downloader early so it can be used for transitive dependency resolution
|
|
1118
|
-
downloader = GitHubPackageDownloader()
|
|
1174
|
+
downloader = GitHubPackageDownloader(auth_resolver=auth_resolver)
|
|
1119
1175
|
|
|
1120
1176
|
# Track direct dependency keys so the download callback can distinguish them from transitive
|
|
1121
1177
|
direct_dep_keys = builtins.set(dep.get_unique_key() for dep in apm_deps)
|
|
@@ -1302,23 +1358,27 @@ def _install_apm_dependencies(
|
|
|
1302
1358
|
# Get config target from apm.yml if available
|
|
1303
1359
|
config_target = apm_package.target
|
|
1304
1360
|
|
|
1305
|
-
#
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
#
|
|
1309
|
-
#
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
)
|
|
1361
|
+
# Resolve effective explicit target: CLI --target wins, then apm.yml
|
|
1362
|
+
_explicit = target or config_target or None
|
|
1363
|
+
|
|
1364
|
+
# Determine active targets. When --target or apm.yml target is set
|
|
1365
|
+
# the user's choice wins. Otherwise auto-detect from existing dirs,
|
|
1366
|
+
# falling back to copilot when nothing is found.
|
|
1367
|
+
from apm_cli.integration.targets import active_targets as _active_targets
|
|
1368
|
+
|
|
1369
|
+
_targets = _active_targets(project_root, explicit_target=_explicit)
|
|
1370
|
+
for _t in _targets:
|
|
1371
|
+
_target_dir = project_root / _t.root_dir
|
|
1372
|
+
if not _target_dir.exists():
|
|
1373
|
+
_target_dir.mkdir(parents=True, exist_ok=True)
|
|
1374
|
+
if logger:
|
|
1375
|
+
logger.verbose_detail(
|
|
1376
|
+
f"Created {_t.root_dir}/ ({_t.name} target)"
|
|
1377
|
+
)
|
|
1318
1378
|
|
|
1319
1379
|
detected_target, detection_reason = detect_target(
|
|
1320
1380
|
project_root=project_root,
|
|
1321
|
-
explicit_target=
|
|
1381
|
+
explicit_target=_explicit,
|
|
1322
1382
|
config_target=config_target,
|
|
1323
1383
|
)
|
|
1324
1384
|
|
|
@@ -1472,13 +1532,9 @@ def _install_apm_dependencies(
|
|
|
1472
1532
|
_pre_downloaded_keys = builtins.set(_pre_download_results.keys())
|
|
1473
1533
|
|
|
1474
1534
|
# Create progress display for sequential integration
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
from apm_cli.core.auth import AuthResolver
|
|
1479
|
-
_auth_resolver = AuthResolver()
|
|
1480
|
-
except Exception:
|
|
1481
|
-
pass
|
|
1535
|
+
# Reuse the shared auth_resolver (already created in this invocation) so
|
|
1536
|
+
# verbose auth logging does not trigger a duplicate credential-helper popup.
|
|
1537
|
+
_auth_resolver = auth_resolver
|
|
1482
1538
|
|
|
1483
1539
|
with Progress(
|
|
1484
1540
|
SpinnerColumn(),
|
|
@@ -2109,9 +2165,16 @@ def _install_apm_dependencies(
|
|
|
2109
2165
|
existing.add_dependency(dep)
|
|
2110
2166
|
lockfile = existing
|
|
2111
2167
|
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2168
|
+
# Only write when the semantic content has actually changed
|
|
2169
|
+
# (avoids generated_at churn in version control).
|
|
2170
|
+
existing_lockfile = LockFile.read(lockfile_path) if lockfile_path.exists() else None
|
|
2171
|
+
if existing_lockfile and lockfile.is_semantically_equivalent(existing_lockfile):
|
|
2172
|
+
if logger:
|
|
2173
|
+
logger.verbose_detail("apm.lock.yaml unchanged -- skipping write")
|
|
2174
|
+
else:
|
|
2175
|
+
lockfile.save(lockfile_path)
|
|
2176
|
+
if logger:
|
|
2177
|
+
logger.verbose_detail(f"Generated apm.lock.yaml with {len(lockfile.dependencies)} dependencies")
|
|
2115
2178
|
except Exception as e:
|
|
2116
2179
|
_lock_msg = f"Could not generate apm.lock.yaml: {e}"
|
|
2117
2180
|
diagnostics.error(_lock_msg)
|
|
@@ -291,7 +291,10 @@ class ClaudeFormatter:
|
|
|
291
291
|
sections.append(f"## Files matching `{pattern}`")
|
|
292
292
|
sections.append("")
|
|
293
293
|
|
|
294
|
-
for instruction in
|
|
294
|
+
for instruction in sorted(
|
|
295
|
+
pattern_instructions,
|
|
296
|
+
key=lambda i: portable_relpath(i.file_path, self.base_dir),
|
|
297
|
+
):
|
|
295
298
|
content = instruction.content.strip()
|
|
296
299
|
if content:
|
|
297
300
|
# Add source attribution comment
|
|
@@ -172,8 +172,9 @@ class ContextOptimizer:
|
|
|
172
172
|
self._file_list_cache = []
|
|
173
173
|
for root, dirs, files in os.walk(self.base_dir):
|
|
174
174
|
# Skip hidden and excluded directories for performance
|
|
175
|
-
|
|
176
|
-
for
|
|
175
|
+
# Sort to guarantee deterministic traversal order across filesystems
|
|
176
|
+
dirs[:] = sorted(d for d in dirs if not d.startswith('.') and d not in DEFAULT_EXCLUDED_DIRNAMES)
|
|
177
|
+
for file in sorted(files):
|
|
177
178
|
if not file.startswith('.'):
|
|
178
179
|
self._file_list_cache.append(Path(root) / file)
|
|
179
180
|
return self._file_list_cache
|
|
@@ -536,7 +536,10 @@ class DistributedAgentsCompiler:
|
|
|
536
536
|
sections.append(f"## Files matching `{pattern}`")
|
|
537
537
|
sections.append("")
|
|
538
538
|
|
|
539
|
-
for instruction in
|
|
539
|
+
for instruction in sorted(
|
|
540
|
+
pattern_instructions,
|
|
541
|
+
key=lambda i: portable_relpath(i.file_path, self.base_dir),
|
|
542
|
+
):
|
|
540
543
|
content = instruction.content.strip()
|
|
541
544
|
if content:
|
|
542
545
|
# Add source attribution for individual instructions
|
|
@@ -34,12 +34,12 @@ def build_conditional_sections(instructions: List[Instruction]) -> str:
|
|
|
34
34
|
|
|
35
35
|
sections = []
|
|
36
36
|
|
|
37
|
-
for pattern, pattern_instructions in pattern_groups.items():
|
|
37
|
+
for pattern, pattern_instructions in sorted(pattern_groups.items()):
|
|
38
38
|
sections.append(f"## Files matching `{pattern}`")
|
|
39
39
|
sections.append("")
|
|
40
|
-
|
|
40
|
+
|
|
41
41
|
# Combine content from all instructions for this pattern
|
|
42
|
-
for instruction in pattern_instructions:
|
|
42
|
+
for instruction in sorted(pattern_instructions, key=lambda i: portable_relpath(i.file_path, Path.cwd())):
|
|
43
43
|
content = instruction.content.strip()
|
|
44
44
|
if content:
|
|
45
45
|
# Add source file comment before the content
|
|
@@ -182,25 +182,30 @@ class AuthResolver:
|
|
|
182
182
|
"""Resolve auth for *(host, org)*. Cached & thread-safe."""
|
|
183
183
|
key = (host.lower() if host else host, org.lower() if org else org)
|
|
184
184
|
with self._lock:
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
git_env=
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
185
|
+
cached = self._cache.get(key)
|
|
186
|
+
if cached is not None:
|
|
187
|
+
return cached
|
|
188
|
+
|
|
189
|
+
# Hold lock during entire credential resolution to prevent duplicate
|
|
190
|
+
# credential-helper popups when parallel downloads resolve the same
|
|
191
|
+
# (host, org) concurrently. The first caller fills the cache; all
|
|
192
|
+
# subsequent callers for the same key become O(1) cache hits.
|
|
193
|
+
# Bounded by APM_GIT_CREDENTIAL_TIMEOUT (default 60s). No deadlock
|
|
194
|
+
# risk: single lock, never nested.
|
|
195
|
+
host_info = self.classify_host(host)
|
|
196
|
+
token, source = self._resolve_token(host_info, org)
|
|
197
|
+
token_type = self.detect_token_type(token) if token else "unknown"
|
|
198
|
+
git_env = self._build_git_env(token)
|
|
199
|
+
|
|
200
|
+
ctx = AuthContext(
|
|
201
|
+
token=token,
|
|
202
|
+
source=source,
|
|
203
|
+
token_type=token_type,
|
|
204
|
+
host_info=host_info,
|
|
205
|
+
git_env=git_env,
|
|
206
|
+
)
|
|
202
207
|
self._cache[key] = ctx
|
|
203
|
-
|
|
208
|
+
return ctx
|
|
204
209
|
|
|
205
210
|
def resolve_for_dep(self, dep_ref: "DependencyReference") -> AuthContext:
|
|
206
211
|
"""Resolve auth from a ``DependencyReference``."""
|
|
@@ -85,27 +85,11 @@ def _close_repo(repo) -> None:
|
|
|
85
85
|
def _rmtree(path) -> None:
|
|
86
86
|
"""Remove a directory tree, handling read-only files and brief Windows locks.
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
an onerror callback for read-only files and a single retry for lock races.
|
|
88
|
+
Delegates to :func:`robust_rmtree` which retries with exponential backoff
|
|
89
|
+
on transient lock errors (e.g. antivirus scanning on Windows).
|
|
91
90
|
"""
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
try:
|
|
95
|
-
os.chmod(fpath, stat.S_IWRITE)
|
|
96
|
-
func(fpath)
|
|
97
|
-
except OSError:
|
|
98
|
-
pass
|
|
99
|
-
|
|
100
|
-
try:
|
|
101
|
-
shutil.rmtree(path, onerror=_on_readonly)
|
|
102
|
-
except PermissionError:
|
|
103
|
-
if sys.platform == 'win32':
|
|
104
|
-
# Single retry after a brief wait for lingering git handles
|
|
105
|
-
time.sleep(0.5)
|
|
106
|
-
shutil.rmtree(path, ignore_errors=True)
|
|
107
|
-
# On all platforms: don't raise from cleanup — just leave the
|
|
108
|
-
# temp dir behind (the OS will clean it up eventually).
|
|
91
|
+
from ..utils.file_ops import robust_rmtree
|
|
92
|
+
robust_rmtree(path, ignore_errors=True)
|
|
109
93
|
|
|
110
94
|
|
|
111
95
|
class GitProgressReporter(RemoteProgress):
|
|
@@ -202,20 +186,18 @@ class GitHubPackageDownloader:
|
|
|
202
186
|
else:
|
|
203
187
|
env['GIT_CONFIG_GLOBAL'] = '/dev/null'
|
|
204
188
|
|
|
205
|
-
#
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
self.
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
)
|
|
189
|
+
# IMPORTANT: Do not resolve credentials via helpers at construction time.
|
|
190
|
+
# AuthResolver.resolve(...) can trigger OS credential helper UI. If we do
|
|
191
|
+
# this eagerly (host-only key) and later resolve per-dependency (host+org),
|
|
192
|
+
# users can see duplicate auth prompts. Keep constructor token state env-only
|
|
193
|
+
# and resolve lazily per dependency during clone/validate flows.
|
|
194
|
+
self.github_token = self.token_manager.get_token_for_purpose('modules', env)
|
|
195
|
+
self.has_github_token = self.github_token is not None
|
|
196
|
+
self._github_token_from_credential_fill = False
|
|
214
197
|
|
|
215
|
-
# Azure DevOps
|
|
216
|
-
|
|
217
|
-
self.
|
|
218
|
-
self.has_ado_token = ado_ctx.token is not None
|
|
198
|
+
# Azure DevOps (env-only at init; lazy auth resolution happens per dep)
|
|
199
|
+
self.ado_token = self.token_manager.get_token_for_purpose('ado_modules', env)
|
|
200
|
+
self.has_ado_token = self.ado_token is not None
|
|
219
201
|
|
|
220
202
|
# JFrog Artifactory (not host-based, uses dedicated env var)
|
|
221
203
|
self.artifactory_token = self.token_manager.get_token_for_purpose('artifactory_modules', env)
|
|
@@ -664,7 +646,9 @@ class GitHubPackageDownloader:
|
|
|
664
646
|
f"If this package lives on a different server (e.g., github.com), "
|
|
665
647
|
f"use the full hostname in apm.yml: {suggested}"
|
|
666
648
|
)
|
|
667
|
-
elif not
|
|
649
|
+
elif not has_token:
|
|
650
|
+
# No auth was resolved (neither env var nor credential helper).
|
|
651
|
+
# Guide the user through setting up authentication.
|
|
668
652
|
host = dep_host or default_host()
|
|
669
653
|
org = dep_ref.repo_url.split('/')[0] if dep_ref and dep_ref.repo_url else None
|
|
670
654
|
error_msg += self.auth_resolver.build_error_context(host, "clone", org=org)
|
|
@@ -1626,14 +1610,16 @@ author: {dep_ref.repo_url.split('/')[0]}
|
|
|
1626
1610
|
_rmtree(target_path)
|
|
1627
1611
|
target_path.mkdir(parents=True, exist_ok=True)
|
|
1628
1612
|
|
|
1629
|
-
# Copy subdirectory contents to target
|
|
1613
|
+
# Copy subdirectory contents to target (retry on transient
|
|
1614
|
+
# file-lock errors caused by antivirus scanning on Windows).
|
|
1615
|
+
from ..utils.file_ops import robust_copytree, robust_copy2
|
|
1630
1616
|
for item in source_subdir.iterdir():
|
|
1631
1617
|
src = source_subdir / item.name
|
|
1632
1618
|
dst = target_path / item.name
|
|
1633
1619
|
if src.is_dir():
|
|
1634
|
-
|
|
1620
|
+
robust_copytree(src, dst)
|
|
1635
1621
|
else:
|
|
1636
|
-
|
|
1622
|
+
robust_copy2(src, dst)
|
|
1637
1623
|
|
|
1638
1624
|
# Capture commit SHA; close the Repo object immediately so its file
|
|
1639
1625
|
# handles are released before _rmtree() runs in the finally block.
|
|
@@ -1723,16 +1709,17 @@ author: {dep_ref.repo_url.split('/')[0]}
|
|
|
1723
1709
|
f"Artifactory ({host}/{prefix}/{owner}/{repo}#{ref})"
|
|
1724
1710
|
)
|
|
1725
1711
|
target_path.mkdir(parents=True, exist_ok=True)
|
|
1712
|
+
from ..utils.file_ops import robust_rmtree, robust_copytree, robust_copy2
|
|
1726
1713
|
if target_path.exists() and any(target_path.iterdir()):
|
|
1727
|
-
|
|
1714
|
+
robust_rmtree(target_path)
|
|
1728
1715
|
target_path.mkdir(parents=True, exist_ok=True)
|
|
1729
1716
|
for item in source_subdir.iterdir():
|
|
1730
1717
|
src = source_subdir / item.name
|
|
1731
1718
|
dst = target_path / item.name
|
|
1732
1719
|
if src.is_dir():
|
|
1733
|
-
|
|
1720
|
+
robust_copytree(src, dst)
|
|
1734
1721
|
else:
|
|
1735
|
-
|
|
1722
|
+
robust_copy2(src, dst)
|
|
1736
1723
|
|
|
1737
1724
|
if progress_obj and progress_task_id is not None:
|
|
1738
1725
|
progress_obj.update(progress_task_id, completed=80, total=100)
|
|
@@ -1771,7 +1758,8 @@ author: {dep_ref.repo_url.split('/')[0]}
|
|
|
1771
1758
|
|
|
1772
1759
|
_debug(f"Downloading from Artifactory: {host}/{prefix}/{owner}/{repo}#{ref}")
|
|
1773
1760
|
if target_path.exists() and any(target_path.iterdir()):
|
|
1774
|
-
|
|
1761
|
+
from ..utils.file_ops import robust_rmtree
|
|
1762
|
+
robust_rmtree(target_path)
|
|
1775
1763
|
target_path.mkdir(parents=True, exist_ok=True)
|
|
1776
1764
|
if progress_obj and progress_task_id is not None:
|
|
1777
1765
|
progress_obj.update(progress_task_id, total=100, completed=10)
|
|
@@ -302,6 +302,26 @@ class LockFile:
|
|
|
302
302
|
"""Save lock file to disk (alias for write)."""
|
|
303
303
|
self.write(path)
|
|
304
304
|
|
|
305
|
+
def is_semantically_equivalent(self, other: "LockFile") -> bool:
|
|
306
|
+
"""Return True if *other* has the same deps, MCP servers, and configs.
|
|
307
|
+
|
|
308
|
+
Ignores ``generated_at`` and ``apm_version`` so that a no-change
|
|
309
|
+
install does not dirty the lockfile.
|
|
310
|
+
"""
|
|
311
|
+
if self.lockfile_version != other.lockfile_version:
|
|
312
|
+
return False
|
|
313
|
+
if set(self.dependencies.keys()) != set(other.dependencies.keys()):
|
|
314
|
+
return False
|
|
315
|
+
for key, dep in self.dependencies.items():
|
|
316
|
+
other_dep = other.dependencies[key]
|
|
317
|
+
if dep.to_dict() != other_dep.to_dict():
|
|
318
|
+
return False
|
|
319
|
+
if sorted(self.mcp_servers) != sorted(other.mcp_servers):
|
|
320
|
+
return False
|
|
321
|
+
if self.mcp_configs != other.mcp_configs:
|
|
322
|
+
return False
|
|
323
|
+
return True
|
|
324
|
+
|
|
305
325
|
@classmethod
|
|
306
326
|
def installed_paths_for_project(cls, project_root: Path) -> List[str]:
|
|
307
327
|
"""Load apm.lock.yaml from project_root and return installed paths.
|