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
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- `apm config set prefer-ssh true` / `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` (or export env vars in shell profiles) on every `apm install`. Resolution order: CLI flag > `APM_GIT_PROTOCOL` / `APM_ALLOW_PROTOCOL_FALLBACK` env var > `apm config` > built-in default. `apm config unset prefer-ssh` and `apm config unset allow-protocol-fallback` remove the persisted value. (#1243)
- `apm pack --marketplace=FORMATS` filters which marketplace formats are built in a single run; accepts comma-separated names and sentinels `all`/`none`. (#1317)
- `apm pack --marketplace-path FORMAT=PATH` overrides the output path for a specific marketplace format at invocation time. Env var overrides (`APM_MARKETPLACE_<FORMAT>_PATH`) are planned for v0.15. (#1317)
- `apm pack --json` emits a stable JSON contract to stdout (`{ok, dry_run, warnings, errors, marketplace: {outputs: [{format, path, ...}]}}`); all logs move to stderr so downstream tooling can `jq` the output safely. (#1317)
- `marketplace.outputs` in `apm.yml` now accepts a map form keyed by format name (`outputs: {claude: {}, codex: {path: ...}}`), replacing the deprecated list form; the list form still parses with a one-cycle deprecation warning. (#1317)
- `apm marketplace init` now scaffolds the explicit map-form `outputs: {claude: {}}` so the default state is observable in the manifest. (#1317)
## [0.14.1] - 2026-05-20

### Added
Expand Down Expand Up @@ -43,6 +51,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `install.ps1` accepts air-gapped / GHES env vars (`VERSION`, `GITHUB_URL`, `APM_REPO`, `APM_INSTALL_DIR`, `APM_SKIP_CHECKSUM`) for Windows pinned installs, matching `install.sh`. (#1246)
- Prefer APM-managed runtime binaries over system PATH; warn at setup time on Codex >= v0.116 + GitHub Models incompatibility instead of failing silently later. (#1356)

Comment on lines +10 to +53
### Fixed

- MCP server installation now respects the `targets:` whitelist exactly like `apm install`: drop a non-listed runtime even when its `.cursor/`, `.codex/`, or other on-disk signal exists. Previously the MCP install path called `active_targets()` reading the singular `target:` key only, so projects whitelisting `targets: [copilot]` could still receive `~/.codex/config.toml` and `.cursor/mcp.json` writes from foreign signals. The fix audits both paths: (a) the call site at `local_bundle_handler.py` now forwards the canonical plural list; (b) the gate now delegates to the same `resolve_targets` resolver that backs `apm install` skills, so a malformed `targets:` field (conflicting `target:` + `targets:`, `targets: []`, or unknown target name) fails closed with the same `[x]` red error voice + remediation block. The same delegation closes a related asymmetry: a greenfield project (no `targets:`, no `--target` flag, no detected signals) used to silently fall back to `[copilot]` for MCP-only invocations, while `apm install` raised `NoHarnessError` on the same input -- both surfaces now error consistently. Drop lines now use the `[i] Skipped MCP config for X (active targets: Y)` format mirroring the canonical `Targets: X (source: Y)` provenance line. The `-g`/`--global` carve-out is unchanged: `apm install -g --mcp NAME` writes to user-scope (`~/.config/...`, `~/.codex/`, etc.) bypassing the project-scope gate by design. (#1335)
- Gemini CLI: `apm install -g --mcp NAME` now correctly writes to `~/.gemini/settings.json` (user scope) and `apm install` from outside the target project writes to `<project_root>/.gemini/settings.json` instead of `cwd`. Previously `--global` had no effect on Gemini and project-scope writes silently landed in the wrong directory. The matching opt-in gate and cleanup paths in `MCPIntegrator` are aligned in the same change. (#1299)
- `apm install --target claude` now preserves self-defined stdio MCP `env` values from `apm.yml` and writes non-string values such as `PORT: 3000` and `DEBUG: false` as MCP-compatible strings. (#1222)
- Non-skill integrators (agent, instruction, prompt, command, hook script-copy) silently adopt byte-identical pre-existing files so a degraded `deployed_files=[]` lockfile no longer permanently blocks installs gated by `required-packages-deployed`. (#1313)
- `apm audit` drift check now returns skip-with-info (`passed=True`) when the install cache is cold, instead of failing the audit; bare `apm audit` surfaces the skip reason on stderr so CI pipelines that have not yet run `apm install` are not incorrectly red-marked. (#1289)

### Changed

- **`apm update` is roughly two orders of magnitude faster on multi-dep manifests**: a four-tier resolver (in-memory cache -> GitHub commits API -> bare `rev-parse` -> legacy clone) collapses redundant `git clone --depth=1` calls. Same wiring benefits install, outdated, and publish. (#1376, closes #1369)
Expand Down
10 changes: 10 additions & 0 deletions docs/src/content/docs/getting-started/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,16 @@ apm install --https
export APM_GIT_PROTOCOL=https
```

To persist the HTTPS preference across all future installs without re-exporting the variable:

```bash
apm config set prefer-ssh false # explicit: never prefer SSH for shorthand deps
# or, if you want APM to always try HTTPS for shorthand deps:
# export APM_GIT_PROTOCOL=https # process-scoped; add to shell profile for persistence
```

See [apm config](../../reference/cli/config/) for the full transport-preference config surface.

## Choosing transport (SSH vs HTTPS)

Authentication and transport are independent decisions:
Expand Down
32 changes: 29 additions & 3 deletions docs/src/content/docs/reference/cli/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,16 @@ Write `KEY` to `~/.apm/config.json`. Validates the value before writing:

### `apm config unset KEY`

Remove `KEY` from `~/.apm/config.json`. No-op if the key is not set. Only `temp-dir` and `copilot-cowork-skills-dir` are unsettable; 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. All settable keys are unsettable: `temp-dir`, `copilot-cowork-skills-dir`, `prefer-ssh`, and `allow-protocol-fallback`. After unsetting a key the effective value falls back to the environment variable, then the built-in 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. |
| `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`. |
| `copilot-cowork-skills-dir` | absolute path | auto-detected | Override the resolved Cowork OneDrive skills directory. Requires the `copilot-cowork` experimental flag for `set`. |

### Resolution order
Expand All @@ -63,6 +65,13 @@ Remove `KEY` from `~/.apm/config.json`. No-op if the key is not set. Only `temp-
2. Value in `~/.apm/config.json`
3. Built-in default (system temp / platform auto-detection)

`allow-protocol-fallback` and `prefer-ssh` follow the layered transport precedence:

1. CLI flag (`--allow-protocol-fallback`, `--ssh`) — highest priority
2. Environment variable (`APM_ALLOW_PROTOCOL_FALLBACK=1`, `APM_GIT_PROTOCOL=ssh`)
3. Value in `~/.apm/config.json` (`apm config set …`)
4. Built-in default (`false` / no preference)

## Examples

Show everything:
Expand All @@ -78,6 +87,22 @@ apm config get auto-integrate
apm config set auto-integrate false
```

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

```bash
apm config set prefer-ssh true
apm config get prefer-ssh
# Remove the persisted preference:
apm config unset prefer-ssh
```

Persist cross-protocol fallback (useful when migrating from SSH to HTTPS or vice versa):

```bash
apm config set allow-protocol-fallback true
apm config get allow-protocol-fallback
```

Pin a writable temp directory on Windows:

```bash
Expand Down Expand Up @@ -106,10 +131,11 @@ apm config unset copilot-cowork-skills-dir
- **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`, `copilot_cowork_skills_dir`); CLI keys use kebab-case. The CLI translates between the two.
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.

## Related

- [`apm install`](../install/) -- consumes `temp-dir` for clone/download work.
- [`apm install`](../install/) -- consumes `temp-dir` for clone/download work and `allow-protocol-fallback` / `prefer-ssh` for transport selection.
- [`apm compile`](../compile/) -- affected by `auto-integrate`.
- [`apm experimental`](../experimental/) -- gates `copilot-cowork-skills-dir`.
- [Environment variables](../environment-variables/) -- `APM_ALLOW_PROTOCOL_FALLBACK`, `APM_GIT_PROTOCOL` are the env-var equivalents of the transport keys.
9 changes: 9 additions & 0 deletions docs/src/content/docs/reference/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ PAT / bearer credentials APM reads when cloning packages, calling host APIs, or
| `GIT_SSH_COMMAND` | Standard git SSH command override. APM reads it before composing its own SSH env. | unset | If you set it, APM preserves your value. |
| `APM_GIT_CREDENTIAL_TIMEOUT` | Seconds to wait for a `git credential fill` response. | implementation default | Integer-like string; invalid values are ignored. |

## Transport and protocol

Controls how APM clones packages from Git hosts. These settings can also be persisted via [`apm config set`](./cli/config/) to avoid repeating flags or environment-variable exports.

| Variable | Purpose | Default | Notes |
|---|---|---|---|
| `APM_GIT_PROTOCOL` | Preferred clone protocol for shorthand (`owner/repo`) dependencies. Accepted values: `ssh`, `https`. | unset | Equivalent to `--ssh` / `--https` flag. Resolution: CLI flag → env var → `prefer-ssh` key in `~/.apm/config.json` → git `insteadOf` rules → HTTPS. |
| `APM_ALLOW_PROTOCOL_FALLBACK` | Set to `1` (or `true`/`yes`/`on`) to enable the legacy cross-protocol fallback chain. When enabled, a failed clone is retried with the opposite protocol. | unset | Equivalent to `--allow-protocol-fallback`. Resolution: CLI flag → env var → `allow-protocol-fallback` key in `~/.apm/config.json` → `false`. |
Comment thread
Aaryan-Dadu marked this conversation as resolved.
Comment on lines +36 to +41

## Registry (MCP and proxy)

| Variable | Purpose | Default | Notes |
Expand Down
79 changes: 70 additions & 9 deletions src/apm_cli/commands/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
"auto_integrate": "auto-integrate",
"temp_dir": "temp-dir",
"copilot_cowork_skills_dir": "copilot-cowork-skills-dir",
"allow_protocol_fallback": "allow-protocol-fallback",
"prefer_ssh": "prefer-ssh",
}


Expand All @@ -35,27 +37,31 @@ def _parse_bool_value(value: str) -> bool:

def _get_config_setters():
"""Return config setters keyed by CLI option name."""
from ..config import set_auto_integrate
from ..config import set_allow_protocol_fallback, set_auto_integrate, set_prefer_ssh

return {
"auto-integrate": (set_auto_integrate, "Auto-integration"),
"allow-protocol-fallback": (set_allow_protocol_fallback, "Protocol fallback"),
"prefer-ssh": (set_prefer_ssh, "SSH transport preference"),
}


def _get_config_getters():
"""Return config getters keyed by CLI option name."""
from ..config import get_auto_integrate
from ..config import get_allow_protocol_fallback, get_auto_integrate, get_prefer_ssh

return {
"auto-integrate": get_auto_integrate,
"allow-protocol-fallback": get_allow_protocol_fallback,
"prefer-ssh": get_prefer_ssh,
}


def _valid_config_keys() -> str:
"""Return valid config keys for messages."""
from ..core.experimental import is_enabled

keys = ["auto-integrate", "temp-dir"]
keys = ["auto-integrate", "temp-dir", "allow-protocol-fallback", "prefer-ssh"]
if is_enabled("copilot_cowork"):
keys.append("copilot-cowork-skills-dir")
return ", ".join(keys)
Comment thread
Aaryan-Dadu marked this conversation as resolved.
Comment on lines 60 to 67
Expand Down Expand Up @@ -120,12 +126,23 @@ def config(ctx):

config_table.add_row("Global", "APM CLI Version", get_version())

from ..config import get_allow_protocol_fallback as _get_apf
from ..config import get_prefer_ssh as _get_prefer_ssh_cfg
from ..config import get_temp_dir as _get_temp_dir

_temp_dir_val = _get_temp_dir()
if _temp_dir_val:
config_table.add_row("", "Temp Directory", _temp_dir_val)

# Only surface transport keys when they have been enabled -- the
# false-default rows add noise for users who never configured them.
_apf = _get_apf()
_prefer_ssh = _get_prefer_ssh_cfg()
if _apf:
config_table.add_row("", "Allow Protocol Fallback", "true")
if _prefer_ssh:
config_table.add_row("", "Prefer SSH Transport", "true")

from ..core.experimental import is_enabled as _is_enabled

if _is_enabled("copilot_cowork"):
Expand Down Expand Up @@ -159,12 +176,17 @@ def config(ctx):
click.echo(f"\n{HIGHLIGHT}Global:{RESET}")
click.echo(f" APM CLI Version: {get_version()}")

from ..config import get_allow_protocol_fallback as _get_apf_fb
from ..config import get_prefer_ssh as _get_prefer_ssh_fb
from ..config import get_temp_dir as _get_temp_dir_fb

_temp_dir_fb = _get_temp_dir_fb()
if _temp_dir_fb:
click.echo(f" Temp Directory: {_temp_dir_fb}")

click.echo(f" allow-protocol-fallback: {str(_get_apf_fb()).lower()}")
click.echo(f" prefer-ssh: {str(_get_prefer_ssh_fb()).lower()}")

from ..core.experimental import is_enabled as _is_enabled_fb

if _is_enabled_fb("copilot_cowork"):
Expand Down Expand Up @@ -236,10 +258,20 @@ def set(key, value): # noqa: F811

setter, label = config_entry
setter(enabled)
if enabled:
logger.success(f"{label} enabled")
else:
logger.success(f"{label} disabled")
logger.success(f"{label} set to {'true' if enabled else 'false'}")

# Warn when persisting allow-protocol-fallback=true in a CI environment where
# $HOME is often shared across jobs -- the persisted value will affect all
# subsequent apm install runs on that host. The env var is safer for CI.
import os as _os

if key == "allow-protocol-fallback" and enabled and _os.environ.get("CI"):
logger.warning(
"allow-protocol-fallback is now persisted to ~/.apm/config.json. "
"In CI environments with a shared $HOME this will affect all subsequent "
"apm install runs on this host. "
"Prefer APM_ALLOW_PROTOCOL_FALLBACK=1 as an invocation-scoped alternative."
)


@config.command(help="Get a configuration value")
Expand Down Expand Up @@ -283,17 +315,30 @@ def get(key):
)
sys.exit(1)
value = getter()
click.echo(f"{key}: {value}")
# Render booleans as lowercase true/false (npm convention).
if isinstance(value, bool):
click.echo(f"{key}: {str(value).lower()}")
else:
click.echo(f"{key}: {value}")
else:
# Show all user-settable keys with their effective values (including
# defaults). Iterating raw config keys would hide settings that
# have not been written yet (e.g. auto_integrate on a fresh install).
from ..config import get_allow_protocol_fallback, get_prefer_ssh

logger.progress("APM Configuration:")
click.echo(f" auto-integrate: {get_auto_integrate()}")
click.echo(f" auto-integrate: {str(get_auto_integrate()).lower()}")
temp_dir = get_temp_dir()
click.echo(
f" temp-dir: {temp_dir if temp_dir is not None else 'Not set (using system default)'}"
)
# Only show transport keys when non-default to reduce noise.
_apf_val = get_allow_protocol_fallback()
_ssh_val = get_prefer_ssh()
if _apf_val:
click.echo(" allow-protocol-fallback: true")
if _ssh_val:
click.echo(" prefer-ssh: true")

from ..core.experimental import is_enabled as _is_enabled_get

Expand All @@ -314,6 +359,8 @@ def unset(key):

Examples:
apm config unset temp-dir
apm config unset allow-protocol-fallback
apm config unset prefer-ssh
apm config unset copilot-cowork-skills-dir
"""
logger = CommandLogger("config unset")
Expand All @@ -325,6 +372,20 @@ def unset(key):
logger.success("Temporary directory configuration removed")
return

if key == "allow-protocol-fallback":
from ..config import unset_allow_protocol_fallback

unset_allow_protocol_fallback()
logger.success("Protocol fallback preference removed (will use env var or default)")
return

if key == "prefer-ssh":
from ..config import unset_prefer_ssh

unset_prefer_ssh()
logger.success("SSH transport preference removed (will use env var or default)")
return

if key == "copilot-cowork-skills-dir":
from ..config import unset_copilot_cowork_skills_dir

Expand Down
15 changes: 10 additions & 5 deletions src/apm_cli/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -1340,8 +1340,6 @@ def install( # noqa: PLR0913
# Resolve transport selection inputs.
from ..deps.transport_selection import (
ProtocolPreference,
is_fallback_allowed,
protocol_pref_from_env,
)

if use_ssh and use_https:
Expand All @@ -1352,9 +1350,16 @@ def install( # noqa: PLR0913
elif use_https:
protocol_pref = ProtocolPreference.HTTPS
else:
protocol_pref = protocol_pref_from_env()
# CLI flag OR env var enables fallback.
allow_protocol_fallback = allow_protocol_fallback or is_fallback_allowed()
# Precedence: APM_GIT_PROTOCOL env var > apm config ssh > git insteadOf
from ..config import get_apm_protocol_pref as _get_apm_protocol_pref

_pref_str = _get_apm_protocol_pref()
protocol_pref = ProtocolPreference.from_str(_pref_str)
# CLI flag > env var (APM_ALLOW_PROTOCOL_FALLBACK) > apm config > default.
# get_apm_allow_protocol_fallback() already encodes env > config > False.
from ..config import get_apm_allow_protocol_fallback as _get_apm_apf

allow_protocol_fallback = allow_protocol_fallback or _get_apm_apf()

# Resolve scope
from ..core.scope import (
Expand Down
Loading