Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- The shared gh-aw workflow `.github/workflows/shared/apm.md` exposes an optional `apm-version` import input that pins the apm CLI version for both the pack and restore `microsoft/apm-action` steps (so the two cannot skew), surviving `gh aw update` without hand-editing the vendored file. Omitting it falls through to the action's pinned default via a gh-aw schema default, so non-opting consumers stay reproducible instead of floating to `latest`. (#1842)
- `apm config set target <env>` configures a default install target so a bare
`apm install` deploys to it -- set the target once, then install everywhere
without repeating `--target`. Precedence is `--target` > `apm.yml` `target:` >
config default > auto-detect, and `apm config get/unset target` inspect and
clear it. (#1881)
- Org-wide policy discovery now cascades through candidate repo names
(`.github`, then `.apm`, then `_apm`) and speaks the Azure DevOps Items
API, so Azure DevOps organizations -- which forbid repo names that begin
Expand Down
17 changes: 14 additions & 3 deletions docs/src/content/docs/reference/cli/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,21 @@ Print the value of `KEY`. With no argument, prints all user-settable keys with t
Write `KEY` to `~/.apm/config.json`. Validates the value before writing:

- `temp-dir` must be an existing, writable directory. The path is expanded (`~`) and stored absolute.
- `target` must be a valid install target token (or comma-separated list), using the same validator as `apm install --target`.
- `copilot-cowork-skills-dir` must be absolute after expansion; the directory itself does not need to exist.
- `mcp-registry-url` must be an `http://` or `https://` URL with a valid host. All other schemes are rejected.
- Boolean keys reject anything outside the accepted truthy/falsy strings.

### `apm config unset KEY`

Remove `KEY` from `~/.apm/config.json`. No-op if the key is not set. Supported unset keys: `temp-dir`, `copilot-cowork-skills-dir`, `prefer-ssh`, `allow-protocol-fallback`, `audit-on-install`, `external.<name>.{llm,args}`, `mcp-registry-url`, and `registry.<name>.{url,token,default}`. After unsetting a key the effective value falls back to the environment variable, then the built-in default. Other boolean keys are reset by `set`-ing them to their default.
Remove `KEY` from `~/.apm/config.json`. No-op if the key is not set. Supported unset keys: `target`, `temp-dir`, `copilot-cowork-skills-dir`, `prefer-ssh`, `allow-protocol-fallback`, `audit-on-install`, `external.<name>.{llm,args}`, `mcp-registry-url`, and `registry.<name>.{url,token,default}`. After unsetting a key the effective value falls back to the environment variable, then the built-in default. Other boolean keys are reset by `set`-ing them to their default.

## Configuration keys

| Key | Type | Default | Description |
| --- | --- | --- | --- |
| `auto-integrate` | boolean | `true` | Auto-discover `.prompt.md` files under `.github/prompts/` and `.apm/prompts/` and merge them into compiled `AGENTS.md` output. |
| `target` | target token | unset | Default target for installs when `--target` and `apm.yml target(s)` are absent. Uses the same parser as `apm install --target` (single or comma-separated). |
| `temp-dir` | path | system temp | Directory used for clone and download operations. Useful when the OS temp directory is locked down (for example, corporate Windows endpoints rejecting `%TEMP%` with `[WinError 5]`). |
| `allow-protocol-fallback` | boolean | `false` | Enable the legacy cross-protocol fallback chain. When true, APM retries a failed clone with the opposite protocol (SSH→HTTPS or HTTPS→SSH). Equivalent to `--allow-protocol-fallback` or `APM_ALLOW_PROTOCOL_FALLBACK=1`. |
| `prefer-ssh` | boolean | `false` | Prefer SSH transport for shorthand (`owner/repo`) dependencies. Equivalent to `--ssh` or `APM_GIT_PROTOCOL=ssh`. |
Expand Down Expand Up @@ -120,6 +122,15 @@ apm config get auto-integrate
apm config set auto-integrate false
```

Persist a default install target:

```bash
apm config set target claude # set the default once
apm config get target # claude
apm install # no --target needed: deploys to claude
apm config unset target # clear it (back to auto-detection)
```

Persist SSH transport preference (no more `--ssh` on every install):

```bash
Expand Down Expand Up @@ -199,11 +210,11 @@ See [External scanners](../../../integrations/external-scanners/).
- **Format:** JSON object, one entry per stored key.
- **Created on first read** with `{"default_client": "vscode"}`. Hand-editing is supported but `apm config set` is preferred -- it validates input and normalizes paths.

Internal JSON keys use snake_case (`auto_integrate`, `temp_dir`, `allow_protocol_fallback`, `prefer_ssh`, `copilot_cowork_skills_dir`); CLI keys use kebab-case. The CLI translates between the two.
Internal JSON keys use snake_case (`auto_integrate`, `install_target`, `temp_dir`, `allow_protocol_fallback`, `prefer_ssh`, `copilot_cowork_skills_dir`); CLI keys use kebab-case (the CLI `target` key is stored as `install_target`). The CLI translates between the two.

## Related

- [`apm install`](../install/) -- consumes `temp-dir` for clone/download work and `allow-protocol-fallback` / `prefer-ssh` for transport selection.
- [`apm install`](../install/) -- consumes `target`, `temp-dir`, and `allow-protocol-fallback` / `prefer-ssh`.
- [`apm compile`](../compile/) -- affected by `auto-integrate`.
- [`apm experimental`](../experimental/) -- gates `copilot-cowork-skills-dir` and `registry.*` keys.
- [Environment variables](../../environment-variables/) -- `APM_ALLOW_PROTOCOL_FALLBACK`, `APM_GIT_PROTOCOL` are the env-var equivalents of the transport keys.
Expand Down
4 changes: 2 additions & 2 deletions docs/src/content/docs/reference/cli/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ With no arguments it installs everything from `apm.yml`. With one or more `PACKA

| Flag | Default | Description |
|---|---|---|
| `--target`, `-t VALUE` | auto-detect | Force deployment targets. Comma-separated for multiple (`-t claude,cursor`). Values: `copilot`, `claude`, `cursor`, `opencode`, `codex`, `gemini`, `windsurf`, `kiro`, `intellij`, `agent-skills`, `all`; experimental `copilot-cowork` and `copilot-app` are also accepted when enabled. `all` expands to every harness above except `agent-skills`; combine `all,agent-skills` for both. Highest precedence in the chain `--target` > `apm.yml targets:` > auto-detect. With nothing to detect, install exits `2` with a teaching message. |
| `--target`, `-t VALUE` | auto-detect | Force deployment targets. Comma-separated for multiple (`-t claude,cursor`). Values: `copilot`, `claude`, `cursor`, `opencode`, `codex`, `gemini`, `windsurf`, `kiro`, `intellij`, `agent-skills`, `all`; experimental `copilot-cowork` and `copilot-app` are also accepted when enabled. `all` expands to every harness above except `agent-skills`; combine `all,agent-skills` for both. Highest precedence in the chain `--target` > `apm.yml targets:` > `apm config set target ...` > auto-detect. With nothing to detect, install exits `2` with a teaching message. |
| `--runtime VALUE` | unset | Legacy alias for `--target` (single value only). Still accepted; prefer `--target`. |
| `--exclude VALUE` | unset | Skip a single runtime that auto-detect or `targets:` would otherwise enable. |
| `--only apm\|mcp` | both | Install only APM packages or only MCP servers. |
Expand Down Expand Up @@ -195,7 +195,7 @@ apm install owner/skill-bundle --skill '*' # reset to all skills
|---|---|
| `0` | Success. All requested dependencies and local content deployed. |
| `1` | Install failure: security scan blocked a critical finding, auth error, manifest write error, dependency resolution error, `--frozen` with a missing lockfile or a direct dependency absent from `apm.lock.yaml`, any reported install error (the diagnostic summary closes with `Installation failed with N error(s)`), or unhandled exception. `--force` does **not** suppress general install errors. The diagnostic summary names the cause. |
| `2` | Usage error: no deployment target detectable (no `--target`, no `targets:` in `apm.yml`, no harness signal in the project), `--ssh` and `--https` both passed, `--frozen` and `--update` both passed, `--root` combined with `--global`, or a Click flag conflict. |
| `2` | Usage error: no deployment target detectable (no `--target`, no `target(s):` in `apm.yml`, no default target configured via `apm config set target <value>`, and no harness signal in the project), `--ssh` and `--https` both passed, `--frozen` and `--update` both passed, `--root` combined with `--global`, or a Click flag conflict. |

## Notes

Expand Down
11 changes: 7 additions & 4 deletions packages/apm-guide/.apm/skills/apm-usage/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ When a default registry is configured, plain shorthand deps (`owner/repo#<ref>`)

1. `--target` flag (highest; CSV form: `--target claude,cursor`).
2. `apm.yml` `targets:` list (or singular `target:` sugar).
3. Auto-detect from filesystem signals (`.claude/` or `CLAUDE.md` -> claude, `.cursor/` -> cursor, `.github/copilot-instructions.md` or any of `.github/instructions/`, `.github/agents/`, `.github/prompts/`, `.github/hooks/` -> copilot, `.codex/` -> codex, `.gemini/` or `GEMINI.md` -> gemini, `.opencode/` -> opencode, `.windsurf/` -> windsurf, `.kiro/` -> kiro, the user-scope JetBrains Copilot MCP config directory `github-copilot/intellij/` -- `%LOCALAPPDATA%\github-copilot\intellij\` on Windows, `~/Library/Application Support/github-copilot/intellij/` on macOS, `~/.local/share/github-copilot/intellij/` on Linux -> intellij). All signals except JetBrains are project-scoped repo markers; the JetBrains signal is a machine-global user-scope directory, so once the Copilot plugin is installed it is detected for every project on that machine.
3. `apm config set target <value>` default.
4. Auto-detect from filesystem signals (`.claude/` or `CLAUDE.md` -> claude, `.cursor/` -> cursor, `.github/copilot-instructions.md` or any of `.github/instructions/`, `.github/agents/`, `.github/prompts/`, `.github/hooks/` -> copilot, `.codex/` -> codex, `.gemini/` or `GEMINI.md` -> gemini, `.opencode/` -> opencode, `.windsurf/` -> windsurf, `.kiro/` -> kiro, the user-scope JetBrains Copilot MCP config directory `github-copilot/intellij/` -- `%LOCALAPPDATA%\github-copilot\intellij\` on Windows, `~/Library/Application Support/github-copilot/intellij/` on macOS, `~/.local/share/github-copilot/intellij/` on Linux -> intellij). All signals except JetBrains are project-scoped repo markers; the JetBrains signal is a machine-global user-scope directory, so once the Copilot plugin is installed it is detected for every project on that machine.

`apm install` prints a one-line provenance summary before any mutation:

Expand Down Expand Up @@ -211,16 +212,18 @@ Experimental flags MUST NOT gate security-critical behaviour (content scanning,
| Command | Purpose | Key flags |
|---------|---------|-----------|
| `apm config` | Show current configuration | -- |
| `apm config get [KEY]` | Get a config value (`auto-integrate`, `temp-dir`, `allow-protocol-fallback`, `prefer-ssh`, `copilot-cowork-skills-dir`, `mcp-registry-url`) | -- |
| `apm config set KEY VALUE` | Set a config value (`auto-integrate`, `temp-dir`, `allow-protocol-fallback`, `prefer-ssh`, `mcp-registry-url`; `copilot-cowork-skills-dir` requires `apm experimental enable copilot-cowork`) | -- |
| `apm config unset KEY` | Remove a stored config value (`temp-dir`, `allow-protocol-fallback`, `prefer-ssh`, `copilot-cowork-skills-dir`, `mcp-registry-url`) | -- |
| `apm config get [KEY]` | Get a config value (`auto-integrate`, `target`, `temp-dir`, `allow-protocol-fallback`, `prefer-ssh`, `copilot-cowork-skills-dir`, `mcp-registry-url`) | -- |
| `apm config set KEY VALUE` | Set a config value (`auto-integrate`, `target`, `temp-dir`, `allow-protocol-fallback`, `prefer-ssh`, `mcp-registry-url`; `copilot-cowork-skills-dir` requires `apm experimental enable copilot-cowork`) | -- |
| `apm config unset KEY` | Remove a stored config value (`target`, `temp-dir`, `allow-protocol-fallback`, `prefer-ssh`, `copilot-cowork-skills-dir`, `mcp-registry-url`) | -- |
| `apm lock` | Resolve all dependencies in `apm.yml` and write `apm.lock.yaml` **without** deploying any files to agent targets. Mirrors `cargo generate-lockfile` / `pnpm lock`. Use to bootstrap or refresh the lockfile before reviewing and applying changes. | `--update` re-resolve to latest SHAs, `--verbose`, `-g/--global`, `--no-policy`, `--target` (comma-separated), `--parallel-downloads N` |
| `apm lock export` | Export an SBOM/inventory from the **existing** `apm.lock.yaml` -- reads the lockfile only (no re-resolve, no re-hash, no network). Emits component identity (purl), recorded hashes, and the declared license. Output is deterministic (components sorted by purl, pinned timestamp) for byte-identical reproducibility. This is an inventory export, not a security attestation. | `-f/--format [cyclonedx\|spdx]` (default `cyclonedx`), `-o/--output FILE` (default stdout), `-g/--global` read user-scope lockfile, `--timestamp ISO8601` pin the document timestamp (falls back to `SOURCE_DATE_EPOCH`, then the lockfile's `generated_at`) |
| `apm update [PKGS...]` | Refresh APM dependencies: resolves `apm.yml` against the latest refs, prints a structured plan (added/updated/removed/unchanged), and prompts before mutating anything (default `[y/N]`). Full-SHA pins are resolved against the latest annotated semver tag, rewritten to that tag's SHA, and annotated as `# <tag>` in `apm.yml`. Pass `[PKGS...]` to refresh only those deps, or `-g` for user scope (`~/.apm/`). Strict superset of the deprecated `apm deps update`. Skips the prompt with `--yes`; previews with `--dry-run`. | `--yes`, `--dry-run`, `--verbose`, `-g/--global`, `--force`, `--parallel-downloads N`, `--target` (comma-separated) |
| `apm self-update` | Update the APM CLI itself (or show distributor guidance when self-update is disabled at build time). | `--check` only check |

`apm config set prefer-ssh true` and `apm config set allow-protocol-fallback true` persist transport preferences to `~/.apm/config.json` so SSH-only and corporate GHES users no longer need to re-pass `--ssh` / `--allow-protocol-fallback` on every `apm install`. Resolution order: CLI flag > `APM_GIT_PROTOCOL` / `APM_ALLOW_PROTOCOL_FALLBACK` env var > `apm config` value > built-in default (`false`). `apm config unset prefer-ssh` and `apm config unset allow-protocol-fallback` remove the persisted value. In `apm config` / `apm config get` (no key), the two transport rows surface only when they have been enabled (the `false`-default rows are suppressed to keep the output noise-free); `apm config get <key>` always returns the effective value. Setting `allow-protocol-fallback=true` while `CI=1` emits a warning because the persisted value affects every subsequent `apm install` on a shared `$HOME`; prefer the env var in CI.

`apm config set target <value>` persists a default install target (single token or comma-separated list) for `apm install` when both `--target` and `apm.yml target(s)` are absent. `apm config unset target` removes this fallback.

`apm self-update` shares the Windows installer codepath used by `install.ps1`: it stages the new release under `%LOCALAPPDATA%\Programs\apm\releases\<tag>` before running `apm.exe --version`, so an AppLocker / WDAC allow-list rule for `%LOCALAPPDATA%\Programs\apm\*` suffices. When the smoke test fails with HRESULT `0x80070005` (`Access is denied`), the installer emits a specific AppLocker/WDAC diagnostic with three remediations (allow-list rule, set `APM_TEMP_DIR` to an allow-listed path, or fall back to `pip install --user apm-cli`) instead of silently retrying via pip.

`apm self-update` (and the startup version-checker) honours the same env vars as `install.sh` for air-gapped and GitHub Enterprise Server (GHE) environments: `GITHUB_URL` overrides the GitHub base URL and API host (`{GITHUB_URL}/api/v3` for GHE), `APM_REPO` overrides the repository (default `microsoft/apm`), and `VERSION` pins a release and skips the GitHub API call entirely. Example: `GITHUB_URL=https://gh.corp.com APM_REPO=corp/apm VERSION=v1.2.3 apm self-update`.
Expand Down
42 changes: 42 additions & 0 deletions src/apm_cli/commands/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@
}


def _render_target_value(value: str | list[str]) -> str:
"""Render a target value for CLI output."""
if isinstance(value, list):
return ",".join(value)
return value


def _parse_bool_value(value: str) -> bool:
"""Parse a CLI boolean value."""
normalized = value.strip().lower()
Expand Down Expand Up @@ -67,6 +74,7 @@ def _valid_config_keys() -> str:

keys = [
"auto-integrate",
"target",
"mcp-registry-url",
"temp-dir",
"allow-protocol-fallback",
Expand Down Expand Up @@ -344,6 +352,18 @@ def set(key, value): # noqa: F811
sys.exit(1)
return

if key == "target":
from ..config import set_install_target

try:
parsed = set_install_target(value)
rendered = _render_target_value(parsed)
logger.success(f"Default install target set to: {rendered}")
except ValueError as exc:
logger.error(str(exc))
sys.exit(1)
return

if key == "mcp-registry-url":
from ..config import get_mcp_registry_url, set_mcp_registry_url

Expand Down Expand Up @@ -465,6 +485,16 @@ def get(key):
click.echo(f"temp-dir: {value}")
return

if key == "target":
from ..config import get_install_target

value = get_install_target()
if value is None:
click.echo("target: Not set (using auto-detection)")
else:
click.echo(f"target: {_render_target_value(value)}")
return

if key == "mcp-registry-url":
from ..config import get_mcp_registry_url

Expand Down Expand Up @@ -526,6 +556,11 @@ def get(key):
click.echo(
f" temp-dir: {temp_dir if temp_dir is not None else 'Not set (using system default)'}"
)
from ..config import get_install_target as _get_install_target

_install_target = _get_install_target()
if _install_target is not None:
click.echo(f" target: {_render_target_value(_install_target)}")
# Only show transport keys when non-default to reduce noise.
_apf_val = get_allow_protocol_fallback()
_ssh_val = get_prefer_ssh()
Expand Down Expand Up @@ -609,6 +644,13 @@ def unset(key):
logger.success("Temporary directory configuration removed")
return

if key == "target":
from ..config import unset_install_target

unset_install_target()
logger.success("Default install target removed (will fall back to auto-detection)")
return

if key == "mcp-registry-url":
from ..config import unset_mcp_registry_url

Expand Down
Loading
Loading