Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
42 changes: 42 additions & 0 deletions docs/src/content/docs/guides/dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ APM supports multiple dependency types:
| **Marketplace Plugin** | Has `plugin.json` (no `apm.yml`) | `github/awesome-copilot/plugins/context-engineering` |
| **Claude Skill** | Has `SKILL.md` (no `apm.yml`) | `ComposioHQ/awesome-claude-skills/brand-guidelines` || **Hook Package** | Has `hooks/*.json` (no `apm.yml` or `SKILL.md`) | `anthropics/claude-plugins-official/plugins/hookify` || **Virtual Subdirectory Package** | Folder path in monorepo | `ComposioHQ/awesome-claude-skills/mcp-builder` |
| **Virtual Subdirectory Package** | Folder path in repo | `github/awesome-copilot/skills/review-and-refactor` |
| **Local Path Package** | Path starts with `./`, `../`, or `/` | `./packages/my-shared-skills` |
| **ADO Package** | Azure DevOps repo | `dev.azure.com/org/project/_git/repo` |

**Virtual Subdirectory Packages** are skill folders from monorepos - they download an entire folder and may contain a SKILL.md plus resources.
Expand Down Expand Up @@ -92,6 +93,10 @@ dependencies:
# FQDN shorthand with virtual path (any host)
- gitlab.com/acme/repo/prompts/code-review.prompt.md

# Local path (for development / monorepo workflows)
- ./packages/my-shared-skills # relative to project root
- /home/user/repos/my-ai-package # absolute path

# Object format: git URL + sub-path / ref / alias
- git: https://gitlab.com/acme/coding-standards.git
path: instructions/security
Expand Down Expand Up @@ -119,6 +124,7 @@ APM accepts dependencies in two forms:
- GitLab nested groups: `gitlab.com/group/subgroup/repo`
- Virtual paths on simple repos: `gitlab.com/owner/repo/file.prompt.md`
- For nested groups + virtual paths, use the object format below
- **Local path** (`./path`, `../path`, `/absolute/path`) — local filesystem package

**Object format** (when you need `path`, `ref`, or `alias` on a git URL):

Expand Down Expand Up @@ -164,6 +170,8 @@ APM normalizes every dependency entry on write — no matter how you specify a p
| `gitlab.com/group/subgroup/repo` | `gitlab.com/group/subgroup/repo` |
| `git@gitlab.com:group/subgroup/repo.git` | `gitlab.com/group/subgroup/repo` |
| `git@bitbucket.org:team/standards.git` | `bitbucket.org/team/standards` |
| `./packages/my-skills` | `./packages/my-skills` |
| `/home/user/repos/my-pkg` | `/home/user/repos/my-pkg` |

Virtual paths, refs, and aliases are preserved:

Expand Down Expand Up @@ -218,6 +226,40 @@ apm compile
# See docs/wip/distributed-agents-compilation-strategy.md for detailed compilation logic
```

## Local Path Dependencies

Install packages from the local filesystem for fast iteration during development.

```bash
# Relative path
apm install ./packages/my-shared-skills

# Absolute path
apm install /home/user/repos/my-ai-package
```

Or declare them in `apm.yml`:

```yaml
dependencies:
apm:
- ./packages/my-shared-skills # relative to project root
- /home/user/repos/my-ai-package # absolute path
- microsoft/apm-sample-package # remote (can be mixed)
```

**How it works:**
- Files are **copied** (not symlinked) to `apm_modules/_local/<package-name>/`
- Local packages are validated the same as remote packages (must have `apm.yml` or `SKILL.md`)
- `apm compile` works identically regardless of dependency source
- Transitive dependencies are resolved recursively (local packages can depend on remote packages)

**Re-install behavior:** Local deps are always re-copied on `apm install` since there is no commit SHA to cache against. This ensures you always get the latest local changes.

**Lockfile representation:** Local dependencies are tracked with `source: local` and `local_path` fields. No `resolved_commit` is stored.

**Pack guard:** `apm pack` rejects packages with local path dependencies — replace them with remote references before distributing.

## MCP Dependency Formats

MCP dependencies support three forms: string references, overlay objects, and self-defined servers.
Expand Down
1 change: 1 addition & 0 deletions docs/src/content/docs/guides/pack-distribute.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ No APM binary, no Python runtime, no network calls. The action handles extractio

1. **`apm.lock.yaml`** — the resolved lockfile produced by `apm install`. Pack reads the `deployed_files` manifest from this file to know what to include.
2. **Installed files on disk** — the actual files referenced in `deployed_files` must exist at their expected paths. Pack verifies this and fails with a clear error if files are missing.
3. **No local path dependencies** — `apm pack` rejects packages that depend on local filesystem paths (`./path` or `/absolute/path`). Replace local dependencies with remote references before packing.

The typical sequence is:

Expand Down
6 changes: 5 additions & 1 deletion docs/src/content/docs/reference/cli-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ apm install [PACKAGES...] [OPTIONS]
```

**Arguments:**
- `PACKAGES` - Optional APM packages to add and install. Accepts shorthand (`owner/repo`), HTTPS URLs, SSH URLs, or FQDN shorthand (`host/owner/repo`). All forms are normalized to canonical format in `apm.yml`.
- `PACKAGES` - Optional APM packages to add and install. Accepts shorthand (`owner/repo`), HTTPS URLs, SSH URLs, FQDN shorthand (`host/owner/repo`), or local filesystem paths (`./path`, `../path`, `/absolute/path`, `~/path`). All forms are normalized to canonical format in `apm.yml`.

**Options:**
- `--runtime TEXT` - Target specific runtime only (copilot, codex, vscode)
Expand Down Expand Up @@ -136,6 +136,10 @@ apm install --exclude codex

# Trust self-defined MCP servers from transitive packages
apm install --trust-transitive-mcp

# Install from a local path (copies to apm_modules/_local/)
apm install ./packages/my-shared-skills
apm install /home/user/repos/my-ai-package
```

**Auto-Bootstrap Behavior:**
Expand Down
14 changes: 9 additions & 5 deletions docs/src/content/docs/reference/lockfile-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,26 +111,30 @@ fields:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `repo_url` | string | MUST | Source repository URL. |
| `repo_url` | string | MUST | Source repository URL, or `_local/<name>` for local path dependencies. |
| `host` | string | MAY | Git host identifier (e.g., `github.com`). Omitted when inferrable from `repo_url`. |
| `resolved_commit` | string | MUST | Full 40-character commit SHA that was checked out. |
| `resolved_ref` | string | MUST | Git ref (tag, branch, SHA) that resolved to `resolved_commit`. |
| `resolved_commit` | string | MUST (remote) | Full 40-character commit SHA that was checked out. Required for remote (git) dependencies; MUST be omitted for local (`source: "local"`) dependencies. |
| `resolved_ref` | string | MUST (remote) | Git ref (tag, branch, SHA) that resolved to `resolved_commit`. Required for remote (git) dependencies; MUST be omitted for local (`source: "local"`) dependencies. |
| `version` | string | MAY | Semantic version of the package, if declared in its manifest. |
| `virtual_path` | string | MAY | Sub-path within the repository for virtual (monorepo) packages. |
| `is_virtual` | boolean | MAY | `true` if the package is a virtual sub-package. Omitted when `false`. |
| `depth` | integer | MUST | Dependency depth. `1` = direct dependency, `2`+ = transitive. |
| `resolved_by` | string | MAY | `repo_url` of the parent that introduced this transitive dependency. Present only when `depth >= 2`. |
| `package_type` | string | MUST | Package type: `apm_package`, `plugin`, `virtual`, or other registered types. |
| `deployed_files` | array of strings | MUST | Every file path APM deployed for this dependency, relative to project root. |
| `source` | string | MAY | Dependency source. `"local"` for local path dependencies. Omitted for remote (git) dependencies. |
| `local_path` | string | MAY | Filesystem path (relative or absolute) to the local package. Present only when `source` is `"local"`. |

Fields with empty or default values (empty strings, `false` booleans, empty
lists) SHOULD be omitted from the serialized output to keep the file concise.

### 4.3 Unique Key

Each dependency is uniquely identified by its `repo_url`, or by the
combination of `repo_url` and `virtual_path` for virtual packages. A
conforming lock file MUST NOT contain duplicate entries for the same key.
combination of `repo_url` and `virtual_path` for virtual packages.
For local path dependencies (`source: "local"`), the unique key is the
`local_path` value. A conforming lock file MUST NOT contain duplicate
entries for the same key.

## 5. Path Conventions

Expand Down
19 changes: 16 additions & 3 deletions docs/src/content/docs/reference/manifest-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,10 @@ Each element MUST be one of two forms: **string** or **object**.
Grammar (ABNF-style):

```
dependency = url_form / shorthand_form
dependency = url_form / shorthand_form / local_path_form
url_form = ("https://" / "http://" / "ssh://git@" / "git@") clone-url
shorthand_form = [host "/"] owner "/" repo ["/" virtual_path] ["#" ref] ["@" alias]
local_path_form = ("./" / "../" / "/" / "~/" / ".\\" / "..\\" / "~\\") path
```

| Segment | Required | Pattern | Description |
Expand Down Expand Up @@ -208,6 +209,10 @@ dependencies:

# Azure DevOps
- dev.azure.com/org/project/_git/repo

# Local path (development only)
- ./packages/my-shared-skills # relative to project root
- ../sibling-repo/my-package # parent directory
```

#### 4.1.2. Object Form
Expand All @@ -216,18 +221,26 @@ REQUIRED when the shorthand is ambiguous (e.g. nested-group repos with virtual p

| Field | Type | Required | Pattern / Constraint | Description |
|---|---|---|---|---|
| `git` | `string` | REQUIRED | HTTPS URL, SSH URL, or FQDN shorthand | Clone URL of the repository. |
| `path` | `string` | OPTIONAL | Relative path within the repo | Subdirectory, file, or collection (virtual package). |
| `git` | `string` | REQUIRED (remote) | HTTPS URL, SSH URL, or FQDN shorthand | Clone URL of the repository. Required for remote dependencies. |
| `path` | `string` | OPTIONAL / REQUIRED (local) | Relative path within the repo, or local filesystem path | When `git` is present: subdirectory, file, or collection (virtual package). When `git` is absent: local filesystem path (must start with `./`, `../`, `/`, or `~/`). |
| `ref` | `string` | OPTIONAL | Branch, tag, or commit SHA | Git reference to checkout. |
| `alias` | `string` | OPTIONAL | `^[a-zA-Z0-9._-]+$` | Local alias. |

Remote dependency (git URL + sub-path):

```yaml
- git: https://gitlab.com/acme/repo.git
path: instructions/security
ref: v2.0
alias: acme-sec
```

Local path dependency (development only):

```yaml
- path: ./packages/my-shared-skills
```

#### 4.1.3. Virtual Packages

A dependency MAY target a subdirectory, file, or collection within a repository rather than the whole repo. Conforming resolvers MUST classify virtual packages using the following rules, evaluated in order:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ dependencies = [
dev = [
"pytest>=7.0.0",
"pytest-cov>=4.0.0",
"black>=23.0.0",
"black>=26.3.1; python_version>='3.10'",
"isort>=5.0.0",
"mypy>=1.0.0",
]
Expand Down
14 changes: 13 additions & 1 deletion src/apm_cli/bundle/packer.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,19 @@ def pack_bundle(
pkg_name = package.name
pkg_version = package.version or "0.0.0"
config_target = package.target
except (FileNotFoundError, ValueError):

# Guard: reject local-path dependencies (non-portable)
for dep_ref in package.get_apm_dependencies():
if dep_ref.is_local:
raise ValueError(
f"Cannot pack — apm.yml contains local path dependency: "
f"{dep_ref.local_path}\n"
f"Local dependencies are for development only. Replace them with "
f"remote references (e.g., 'owner/repo') before packing."
)
except ValueError:
raise
except FileNotFoundError:
pkg_name = project_root.resolve().name
pkg_version = "0.0.0"
config_target = None
Expand Down
Loading
Loading