Skip to content
Merged
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
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ dependencies:
# Specific agent primitives from any repository
- github/awesome-copilot/skills/review-and-refactor
- github/awesome-copilot/agents/api-architect.agent.md
# Plugins (auto-detected from plugin.json)
- github/awesome-copilot/plugins/context-engineering
```

New developer joins the team:
Expand All @@ -58,6 +60,7 @@ Skill registries install skills. APM manages **every primitive** your AI agents
| **Prompts** | Reusable slash commands | `/security-audit`, `/design-review` |
| **Agents** | Specialized personas | Accessibility auditor, API designer |
| **Hooks** | Lifecycle event handlers | Pre-tool validation, post-tool linting |
| **Plugins** | Pre-packaged agent bundles | Context engineering, commit helpers |
| **MCP Servers** | Tool integrations | Database access, API connectors |

All declared in one manifest. All installed with one command — including transitive dependencies:
Expand Down Expand Up @@ -89,6 +92,7 @@ pip install apm-cli
apm install microsoft/apm-sample-package
apm install anthropics/skills/skills/frontend-design
apm install github/awesome-copilot/agents/api-architect.agent.md
apm install github/awesome-copilot/plugins/context-engineering
```

**Done.** Open your project in VS Code or Claude and your AI tools are ready.
Expand Down Expand Up @@ -170,7 +174,7 @@ For private repos or Azure DevOps, set a token. For other hosts (GitLab, Bitbuck

## APM Packages

An APM package is anything you can point `apm install` at: a full package with an `apm.yml` manifest and `.apm/` folder, a single primitive file (`.instructions.md`, `.prompt.md`, `.agent.md`), a skill folder, or any subtree inside a repository. Hooks are auto-discovered when a package contains them. See [Primitives](docs/primitives.md) for details on each type.
An APM package is anything you can point `apm install` at: a full package with an `apm.yml` manifest and `.apm/` folder, a plugin with `plugin.json`, a single primitive file (`.instructions.md`, `.prompt.md`, `.agent.md`), a skill folder, or any subtree inside a repository. Hooks are auto-discovered when a package contains them. See [Primitives](docs/primitives.md) for details on each type.

APM installs from **any git host** — GitHub, GitLab, Bitbucket, self-hosted servers, GitHub Enterprise, and Azure DevOps. Use HTTPS or SSH git URLs, or the `owner/repo` shorthand for GitHub. See [Package Sources](docs/getting-started.md#package-sources) for host configuration.

Expand All @@ -193,7 +197,7 @@ See the [APM Roadmap](https://github.com/microsoft/apm/discussions/116) for what
| | |
|---|---|
| **Get Started** | [Quick Start](docs/getting-started.md) · [Core Concepts](docs/concepts.md) · [Examples](docs/examples.md) |
| **Reference** | [CLI Reference](docs/cli-reference.md) · [Compilation Engine](docs/compilation.md) · [Skills](docs/skills.md) · [Integrations](docs/integrations.md) |
| **Reference** | [CLI Reference](docs/cli-reference.md) · [Compilation Engine](docs/compilation.md) · [Skills](docs/skills.md) · [Plugins](docs/plugins.md) · [Integrations](docs/integrations.md) |
| **Advanced** | [Dependencies](docs/dependencies.md) · [Primitives](docs/primitives.md) · [Contributing](CONTRIBUTING.md) |

---
Expand Down
1 change: 1 addition & 0 deletions docs/dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ APM supports multiple dependency types:
| Type | Detection | Example |
|------|-----------|---------|
| **APM Package** | Has `apm.yml` | `microsoft/apm-sample-package` |
| **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` |
| **ADO Package** | Azure DevOps repo | `dev.azure.com/org/project/_git/repo` |
Expand Down
5 changes: 3 additions & 2 deletions docs/manifest-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,10 @@ Controls which output targets are generated during compilation. When unset, a co
|---|---|
| **Type** | `enum<string>` |
| **Required** | OPTIONAL |
| **Default** | None (unset — behaviour depends on package content) |
| **Default** | None (behaviour driven by package content; synthesized plugin manifests use `hybrid`) |
| **Allowed values** | `instructions` · `skill` · `hybrid` · `prompts` |

Declares how the package's content is processed during install and compile:
Declares how the package's content is processed during install and compile. Currently behaviour is driven by package content (presence of `SKILL.md`, component directories, etc.); this field is reserved for future explicit overrides.

| Value | Behaviour |
|---|---|
Expand Down Expand Up @@ -359,6 +359,7 @@ dependencies: # YAML list (not a map)
is_virtual: <bool> # True for virtual (file/subdirectory) packages
depth: <int> # 1 = direct, 2+ = transitive
resolved_by: <string> # Parent dependency (transitive only)
package_type: <string> # Package type (e.g. "apm_package", "marketplace_plugin")
deployed_files: <list<string>> # Workspace-relative paths of installed files
mcp_servers: <list<string>> # MCP dependency references managed by APM (OPTIONAL, e.g. "io.github.github/github-mcp-server")
```
Expand Down
54 changes: 29 additions & 25 deletions docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ When you run `apm install owner/repo/plugin-name`:

1. **Clone** - APM clones the repository to `apm_modules/`
2. **Detect** - It searches for `plugin.json` in priority order:
- `.github/plugin/plugin.json` (GitHub Copilot format)
- `.claude-plugin/plugin.json` (Claude format)
- `plugin.json` (root)
1. `plugin.json` (root)
2. `.github/plugin/plugin.json` (GitHub Copilot format)
3. `.claude-plugin/plugin.json` (Claude format)
3. **Map Artifacts** - Plugin primitives from the repository root are mapped into `.apm/`:
- `agents/` → `.apm/agents/`
- `skills/` → `.apm/skills/`
Expand Down Expand Up @@ -63,7 +63,7 @@ APM supports multiple plugin manifest locations to accommodate different platfor
plugin-repo/
├── .github/
│ └── plugin/
│ └── plugin.json # GitHub Copilot location (highest priority)
│ └── plugin.json # GitHub Copilot location
├── agents/
│ └── agent-name.agent.md
├── skills/
Expand All @@ -78,22 +78,7 @@ plugin-repo/
```
plugin-repo/
├── .claude-plugin/
│ └── plugin.json # Claude location (second priority)
├── agents/
│ └── agent-name.agent.md
├── skills/
│ └── skill-name/
│ └── SKILL.md
└── commands/
└── command-1.md
└── command-2.md
```

#### Legacy APM Format
```
plugin-repo/
├── plugins/
│ └── plugin.json # Legacy APM location (third priority)
│ └── plugin.json # Claude location
├── agents/
│ └── agent-name.agent.md
├── skills/
Expand All @@ -107,7 +92,7 @@ plugin-repo/
#### Root Format
```
plugin-repo/
├── plugin.json # Root location (lowest priority)
├── plugin.json # Root location (checked first)
├── agents/
│ └── agent-name.agent.md
├── skills/
Expand All @@ -127,7 +112,7 @@ plugin-repo/

### plugin.json Manifest

Required fields:
Only `name` is required. `version` and `description` are optional metadata:

```json
{
Expand Down Expand Up @@ -155,6 +140,25 @@ Optional fields:
}
```

#### Custom component paths

By default APM looks for `agents/`, `skills/`, `commands/`, and `hooks/` directories at the plugin root. You can override these with custom paths using strings or arrays:

```json
{
"name": "my-plugin",
"agents": ["./agents/planner.md", "./agents/coder.md"],
"skills": ["./skills/analysis", "./skills/review"],
"commands": "my-commands/",
"hooks": "hooks.json"
}
```

- **String** — single directory or file path
- **Array** — list of directories or individual files
- When an array contains directories, each is preserved as a named subdirectory (e.g., `./skills/analysis/` → `.apm/skills/analysis/SKILL.md`)
- `hooks` also accepts an inline object: `"hooks": {"hooks": {"PreToolUse": [...]}}`

## Examples

### Installing Plugins from GitHub
Expand Down Expand Up @@ -266,12 +270,12 @@ Once found, install them using the standard `apm install owner/repo/plugin-name`

If APM doesn't recognize your plugin:

1. Check `plugin.json` exists at the repository root or in a subdirectory:
- `plugin.json` (root )
1. Check `plugin.json` exists in one of the checked locations:
- `plugin.json` (root)
- `.github/plugin/plugin.json` (GitHub Copilot format)
- `.claude-plugin/plugin.json` (Claude format)
2. Verify JSON is valid: `cat plugin.json | jq .`
3. Ensure required fields are present: `name`, `version`, `description`
3. Ensure `name` field is present (only required field)
4. Verify primitives are at the repository root (`agents/`, `skills/`, `commands/`)

### Version Resolution Issues
Expand Down
1 change: 1 addition & 0 deletions docs/primitives.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ The APM CLI supports the following types of primitives:
- **Skills** (`SKILL.md`) - Package meta-guides that help AI agents understand what a package does
- **Context** (`.context.md`, `.memory.md`) - Supply background information and project context
- **Hooks** (`.json` in `.apm/hooks/` or `hooks/`) - Define lifecycle event handlers with script references
- **Plugins** (`plugin.json`) - Pre-packaged agent bundles auto-normalized into APM packages

> **Note**: Both `.agent.md` (new format) and `.chatmode.md` (legacy format) are fully supported. VSCode provides Quick Fix actions to help migrate from `.chatmode.md` to `.agent.md`.

Expand Down
29 changes: 26 additions & 3 deletions src/apm_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1849,6 +1849,7 @@ def _collect_descendants(node):
from apm_cli.deps.lockfile import LockFile, LockedDependency, get_lockfile_path
installed_packages: List[tuple] = [] # List of (dep_ref, resolved_commit, depth, resolved_by)
package_deployed_files: dict = {} # dep_key → list of relative deployed paths
package_types: dict = {} # dep_key → package type string

# Build managed_files from existing lockfile for collision detection
managed_files = builtins.set()
Expand Down Expand Up @@ -2003,6 +2004,7 @@ def _collect_descendants(node):
)
ref_str = f" @{dep_ref.reference}" if dep_ref.reference else ""
_rich_info(f"✓ {display_name}{ref_str} (cached)")
installed_count += 1

# Still need to integrate prompts for cached packages (zero-config behavior)
if integrate_vscode or integrate_claude:
Expand Down Expand Up @@ -2053,7 +2055,11 @@ def _collect_descendants(node):
# skill integration is not silently skipped
skill_md_exists = (install_path / "SKILL.md").exists()
apm_yml_exists = (install_path / "apm.yml").exists()
if skill_md_exists and apm_yml_exists:
from apm_cli.utils.helpers import find_plugin_json
plugin_json_exists = find_plugin_json(install_path) is not None
if plugin_json_exists and not apm_yml_exists:
cached_package_info.package_type = PackageType.MARKETPLACE_PLUGIN
elif skill_md_exists and apm_yml_exists:
cached_package_info.package_type = PackageType.HYBRID
elif skill_md_exists:
cached_package_info.package_type = PackageType.CLAUDE_SKILL
Expand All @@ -2076,6 +2082,10 @@ def _collect_descendants(node):
installed_packages.append((dep_ref, cached_commit, depth, resolved_by))
dep_deployed: list = [] # collect deployed paths for this package

# Track package type for lockfile
if hasattr(cached_package_info, 'package_type') and cached_package_info.package_type:
package_types[dep_key] = cached_package_info.package_type.value

# VSCode + Claude integration (prompts + agents)
if integrate_vscode or integrate_claude:
# Integrate prompts
Expand Down Expand Up @@ -2299,6 +2309,10 @@ def _collect_descendants(node):
installed_packages.append((dep_ref, resolved_commit, depth, resolved_by))
dep_deployed_fresh: list = [] # collect deployed paths for this package

# Track package type for lockfile
if hasattr(package_info, 'package_type') and package_info.package_type:
package_types[dep_ref.get_unique_key()] = package_info.package_type.value

# Show package type in verbose mode
if verbose and hasattr(package_info, "package_type"):
from apm_cli.models.apm_package import PackageType
Expand Down Expand Up @@ -2499,11 +2513,20 @@ def _collect_descendants(node):
if installed_packages:
try:
lockfile = LockFile.from_installed_packages(installed_packages, dependency_graph)
# Attach deployed_files to each LockedDependency
# Attach deployed_files and package_type to each LockedDependency
for dep_key, dep_files in package_deployed_files.items():
if dep_key in lockfile.dependencies:
lockfile.dependencies[dep_key].deployed_files = dep_files

for dep_key, pkg_type in package_types.items():
if dep_key in lockfile.dependencies:
lockfile.dependencies[dep_key].package_type = pkg_type
# Merge with existing lockfile to preserve entries for packages
# not processed in this run (e.g. `apm install X` only installs X).
# Skip merge when update_refs is set — stale entries must not survive.
if existing_lockfile and not update_refs:
for dep_key, dep in existing_lockfile.dependencies.items():
if dep_key not in lockfile.dependencies:
lockfile.dependencies[dep_key] = dep
Comment thread
danielmeppiel marked this conversation as resolved.
lockfile_path = get_lockfile_path(project_root)

# When installing a subset of packages (apm install <pkg>),
Expand Down
29 changes: 18 additions & 11 deletions src/apm_cli/commands/deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -473,18 +473,25 @@ def info(package: str):
_rich_info("Run 'apm install' to install dependencies first")
sys.exit(1)

# Find the package directory - handle org/repo structure
# Find the package directory - handle org/repo and deep sub-path structures
package_path = None
for org_dir in apm_modules_path.iterdir():
if org_dir.is_dir() and not org_dir.name.startswith('.'):
for package_dir in org_dir.iterdir():
if package_dir.is_dir() and not package_dir.name.startswith('.'):
# Check both package name and org/package format
if package_dir.name == package or f"{org_dir.name}/{package_dir.name}" == package:
package_path = package_dir
break
if package_path:
break
# First try direct path match (handles any depth: org/repo, org/repo/subdir/pkg)
direct_match = apm_modules_path / package
if direct_match.is_dir() and (
(direct_match / "apm.yml").exists() or (direct_match / "SKILL.md").exists()
):
package_path = direct_match
else:
# Fallback: scan org/repo structure (2-level) for short package names
for org_dir in apm_modules_path.iterdir():
if org_dir.is_dir() and not org_dir.name.startswith('.'):
for package_dir in org_dir.iterdir():
if package_dir.is_dir() and not package_dir.name.startswith('.'):
if package_dir.name == package or f"{org_dir.name}/{package_dir.name}" == package:
package_path = package_dir
break
if package_path:
break

if not package_path:
_rich_error(f"Package '{package}' not found in apm_modules/")
Expand Down
4 changes: 4 additions & 0 deletions src/apm_cli/deps/lockfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class LockedDependency:
is_virtual: bool = False
depth: int = 1
resolved_by: Optional[str] = None
package_type: Optional[str] = None
deployed_files: List[str] = field(default_factory=list)

def get_unique_key(self) -> str:
Expand Down Expand Up @@ -53,6 +54,8 @@ def to_dict(self) -> Dict[str, Any]:
result["depth"] = self.depth
if self.resolved_by:
result["resolved_by"] = self.resolved_by
if self.package_type:
result["package_type"] = self.package_type
if self.deployed_files:
result["deployed_files"] = sorted(self.deployed_files)
return result
Expand Down Expand Up @@ -84,6 +87,7 @@ def from_dict(cls, data: Dict[str, Any]) -> "LockedDependency":
is_virtual=data.get("is_virtual", False),
depth=data.get("depth", 1),
resolved_by=data.get("resolved_by"),
package_type=data.get("package_type"),
deployed_files=deployed_files,
)

Expand Down
Loading