feat: add --global / -g flag for scoped package installation#452
feat: add --global / -g flag for scoped package installation#452
Conversation
Co-authored-by: sergio-sisternes-epam <207026618+sergio-sisternes-epam@users.noreply.github.com> Agent-Logs-Url: https://github.com/microsoft/apm/sessions/b5b08e0d-ae11-469c-b041-1397894f0e20
Add InstallScope enum and scope resolution module (core/scope.py). Wire --global flag into install and uninstall commands. User-scope packages deploy to ~/.github/, ~/.claude/, etc. Metadata (manifest, lockfile, modules) lives under ~/.apm/. Co-authored-by: sergio-sisternes-epam <207026618+sergio-sisternes-epam@users.noreply.github.com> Agent-Logs-Url: https://github.com/microsoft/apm/sessions/b5b08e0d-ae11-469c-b041-1397894f0e20
…GELOG Co-authored-by: sergio-sisternes-epam <207026618+sergio-sisternes-epam@users.noreply.github.com> Agent-Logs-Url: https://github.com/microsoft/apm/sessions/b5b08e0d-ae11-469c-b041-1397894f0e20
Co-authored-by: sergio-sisternes-epam <207026618+sergio-sisternes-epam@users.noreply.github.com> Agent-Logs-Url: https://github.com/microsoft/apm/sessions/b5b08e0d-ae11-469c-b041-1397894f0e20
| - Lockfile: `~/.apm/apm.lock.yaml` | ||
| - Deployed primitives: `~/.github/`, `~/.claude/`, `~/.cursor/`, `~/.opencode/` | ||
|
|
||
| ### Where user-scope primitives land |
There was a problem hiding this comment.
We should add also the scope of global primitives.
We need to confirm all targets are supported. If not supported, a gap should be logged on the table and a warning should be traced during installation
There was a problem hiding this comment.
Updated in 144c9fa. The per-target support table now shows status (Supported / Not supported / Unverified), which primitives work at user scope per target, and reference links to each tool's documentation. Only Claude Code is verified as supported. APM now also shows a [!] warning during apm install --global listing the targets that lack native user-level support.
|
@copilot Review my comments. If I posted a question just answer it. If I want a code change I'll ask |
…tall warning - Update USER_SCOPE_TARGETS: only Claude Code is verified as supported; Copilot, Cursor, and OpenCode are marked unsupported with reference URLs - Add warn_unsupported_user_scope() and get_unsupported_targets() helpers - Show warning during `apm install --global` listing unsupported targets - Rewrite scoped-installation.md with per-target support table, status column, primitives breakdown, and reference links - Add 8 new tests for target support flags and warning messages Co-authored-by: sergio-sisternes-epam <207026618+sergio-sisternes-epam@users.noreply.github.com> Agent-Logs-Url: https://github.com/microsoft/apm/sessions/82d9226e-bb1a-4c76-92bd-d3105dfc3408
Understood. I'll only answer questions going forward and wait for explicit requests before making code changes. |
|
@copilot we need to consider including some integration tests to cover the new global feature. This is especially important, given we are supporting multiple operating systems |
There was a problem hiding this comment.
Pull request overview
Adds a user-level installation scope to APM so packages can be installed/uninstalled globally (metadata under ~/.apm/) while keeping existing project-scope behavior unchanged.
Changes:
- Introduces
InstallScope+ scope-aware path helpers and user-scope target support/warning helpers. - Wires
-g/--globalthroughapm installandapm uninstall, including scoped manifest/lockfile/modules handling. - Adds unit tests and documentation for scoped installation behavior, plus changelog entries.
Reviewed changes
Copilot reviewed 9 out of 10 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
src/apm_cli/core/scope.py |
New scope abstraction (project vs user), path resolution helpers, and unsupported-target warning helpers. |
src/apm_cli/commands/install.py |
Adds -g/--global, uses scope-aware manifest/modules/lockfile paths, and passes scope into the install engine. |
src/apm_cli/commands/uninstall/cli.py |
Adds -g/--global and uses scope-aware manifest/modules/lockfile paths and deploy root for cleanup. |
src/apm_cli/commands/_helpers.py |
Allows writing a minimal manifest to an explicit path (needed for ~/.apm/apm.yml). |
tests/unit/core/test_scope.py |
New unit tests for InstallScope, path helpers, dir creation, and warnings. |
tests/unit/test_install_command.py |
New tests for apm install -g/--global behavior and manifest creation. |
docs/src/content/docs/guides/scoped-installation.md |
New guide explaining project vs user scope and per-target support. |
docs/src/content/docs/reference/cli-commands.md |
Documents -g/--global for install/uninstall and adds examples. |
CHANGELOG.md |
Adds Unreleased entries describing the new global scope feature. |
Comments suppressed due to low confidence (1)
src/apm_cli/commands/install.py:615
- The verbose log line still says
Parsed apm.ymleven when running with--globaland parsing~/.apm/apm.yml. This makes--verboseoutput misleading in user scope. Consider usingmanifest_display(or the resolvedmanifest_path) in this message so the output reflects the actual file being parsed.
logger.verbose_detail(
f"Parsed {APM_YML_FILENAME}: {len(apm_package.get_apm_dependencies())} APM deps, "
f"{len(apm_package.get_mcp_dependencies())} MCP deps"
+ (f", {len(apm_package.get_dev_apm_dependencies())} dev deps"
if apm_package.get_dev_apm_dependencies() else "")
)
| result = self.runner.invoke( | ||
| cli, ["install", "--global", "test/pkg"] | ||
| ) | ||
|
|
There was a problem hiding this comment.
This test verifies that ~/.apm/apm.yml is created, but it never asserts result.exit_code == 0 (or inspects result.exception). Adding an exit-code assertion would ensure the command completed successfully and prevent false positives where the manifest is created but the command later fails.
| assert result.exit_code == 0 |
| - `apm install --global` / `-g` flag for user-scope package installation; deploys primitives to `~/.github/`, `~/.claude/`, etc. and tracks metadata under `~/.apm/` | ||
| - `apm uninstall --global` / `-g` flag for removing user-scope packages | ||
| - `InstallScope` enum and scope resolution module (`core/scope.py`) defining project vs user deployment paths | ||
|
|
There was a problem hiding this comment.
The new Unreleased changelog entries don't follow the existing file's convention of ending each bullet with a PR reference like (#NNN) (see other entries below). Also, these three bullets read like one logical change and could be consolidated into a single concise entry with the PR number appended for consistency.
| - `apm install --global` / `-g` flag for user-scope package installation; deploys primitives to `~/.github/`, `~/.claude/`, etc. and tracks metadata under `~/.apm/` | |
| - `apm uninstall --global` / `-g` flag for removing user-scope packages | |
| - `InstallScope` enum and scope resolution module (`core/scope.py`) defining project vs user deployment paths | |
| - Global `apm install --global` / `-g` and `apm uninstall --global` flags for user-scope package installation, backed by `InstallScope`-based scope resolution in `core/scope.py`; deploys primitives to `~/.github/`, `~/.claude/`, etc. and tracks metadata under `~/.apm/` (#440) |
| try: | ||
| # Resolve scope | ||
| from ..core.scope import InstallScope, get_deploy_root, get_apm_dir, get_manifest_path, get_modules_dir, ensure_user_dirs, warn_unsupported_user_scope | ||
| scope = InstallScope.USER if global_ else InstallScope.PROJECT | ||
|
|
||
| if scope is InstallScope.USER: | ||
| ensure_user_dirs() | ||
| _rich_info("[i] Installing to user scope (~/.apm/)") | ||
| _scope_warn = warn_unsupported_user_scope() | ||
| if _scope_warn: | ||
| from ..utils.console import _rich_warning | ||
| _rich_warning(_scope_warn) | ||
|
|
There was a problem hiding this comment.
In the new user-scope flow, several operations (importing scope helpers, ensure_user_dirs(), warn_unsupported_user_scope()) run before logger is instantiated, but the surrounding except Exception block later calls logger.error(...). If an exception is raised before logger is assigned, this will trigger an UnboundLocalError and mask the original failure. Create the InstallLogger before any scope initialization (or guard the exception handler to fall back to _rich_error when logger is not available).
| if not Path(APM_YML_FILENAME).exists(): | ||
| logger.error(f"No {APM_YML_FILENAME} found. Run 'apm init' first.") | ||
| if not manifest_path.exists(): | ||
| logger.error(f"No {manifest_path} found. Run 'apm init' first.") |
There was a problem hiding this comment.
For apm uninstall -g, the missing-manifest message suggests running apm init first. apm init creates a project-scope ./apm.yml, so this guidance is incorrect for user scope. Consider branching the message by scope (e.g., instruct to create ~/.apm/apm.yml or run an apm install -g <pkg> bootstrap) while keeping the current apm init suggestion for project scope.
| logger.error(f"No {manifest_path} found. Run 'apm init' first.") | |
| if scope is InstallScope.USER: | |
| logger.error( | |
| "No user manifest found at ~/.apm/apm.yml. Install a package globally " | |
| "first with 'apm install -g <package>' or create the file manually." | |
| ) | |
| else: | |
| logger.error(f"No {manifest_path} found. Run 'apm init' in this project first.") |
| result = self.runner.invoke(cli, ["install", "--global"]) | ||
| assert "user scope" in result.output.lower() or "~/.apm/" in result.output | ||
| # Should warn about unsupported targets | ||
| assert "copilot" in result.output.lower() |
There was a problem hiding this comment.
The new --global install test asserts on output content but does not assert the expected exit code. Adding an assert result.exit_code == 1 here (since no manifest/packages are provided) would make the test stricter and avoid passing if the command unexpectedly succeeds/fails in a different way.
Tests cover cross-platform path resolution, directory creation, CLI output/warnings, manifest placement, error handling, and uninstall lifecycle using isolated HOME directories via subprocess. Co-authored-by: sergio-sisternes-epam <207026618+sergio-sisternes-epam@users.noreply.github.com> Agent-Logs-Url: https://github.com/microsoft/apm/sessions/30612a84-96a3-4e75-9dba-0f49927a1494
Co-authored-by: sergio-sisternes-epam <207026618+sergio-sisternes-epam@users.noreply.github.com> Agent-Logs-Url: https://github.com/microsoft/apm/sessions/30612a84-96a3-4e75-9dba-0f49927a1494
Added 17 integration tests in
All tests pass on Linux. The HOME/USERPROFILE override pattern matches existing integration tests ( |
Description
APM currently installs all packages at project scope with no way to install to user-level directories. This adds
--global/-gtoapm installandapm uninstall, mirroring npm's-gsemantics.User-scope stores metadata under
~/.apm/(manifest, lockfile, modules) and deploys primitives to user-level directories where supported. Currently, only Claude Code natively reads from~/.claude/at user level. Copilot, Cursor, and OpenCode do not support user-level primitives via the filesystem — APM warns about this during--globalinstalls.Core changes:
src/apm_cli/core/scope.py—InstallScopeenum, path resolution helpers (get_deploy_root,get_apm_dir,get_modules_dir,get_manifest_path),USER_SCOPE_TARGETSregistry with per-target support status, primitives list, and reference URLs to official documentationapm install—--globalflag wired through to_install_apm_dependencies(), scope-aware manifest/lockfile/modules paths, warning emitted for targets without native user-scope supportapm uninstall—--globalflag with scope-aware paths for all cleanup steps_create_minimal_apm_yml/_validate_and_add_packages_to_apm_yml— accept optional target/manifest path for non-CWD manifests (needed because global scope writes to~/.apm/apm.yml)warn_unsupported_user_scope()/get_unsupported_targets()— helpers that report which targets lack native user-level config supportdocs/src/content/docs/guides/scoped-installation.md— per-target support table with status, primitives at user scope, and reference links to each tool's documentationType of change
Testing
26 tests in
tests/unit/core/test_scope.py(enum, path resolution,ensure_user_dirs, target registry support flags, unsupported target warnings). 4 tests intests/unit/test_install_command.py(--globalflag behavior with warning output,-gshort flag, user manifest creation, error handling). 17 integration tests intests/integration/test_global_scope_e2e.pycovering cross-platform scenarios: directory creation under~/.apm/, CLI output/warnings, error handling, manifest placement (auto-bootstrap, no CWD pollution, lockfile location), cross-platform path resolution (absolute paths, POSIX format, tilde prefixes), and uninstall lifecycle (package removal from manifest, not-found warnings). Tests use subprocess-based CLI invocation with isolated HOME/USERPROFILE directories. All 3059 tests pass. CodeQL: 0 alerts.Original prompt
🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.