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
20 changes: 11 additions & 9 deletions docs/src/content/docs/guides/registries.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,22 +291,24 @@ apm publish --package acme/my-skill
apm install acme/internal-tools#^1.0.0
```

[`apm publish`](../../reference/cli/publish/) reads `apm.yml`, builds a **flat registry archive** (`.tar.gz` with `apm.yml`, `.apm/`, and standard documentation files at the tarball root), and uploads via `PUT /v1/packages/{owner}/{repo}/versions/{version}`. Consumers with a default registry configured install with the same `owner/repo#version` shorthand they would use for GitHub.
[`apm publish`](../../reference/cli/publish/) reads `apm.yml`, builds a **flat registry archive** (`.zip` with `apm.yml`, `.apm/`, and standard documentation files at the archive root), and uploads via `PUT /v1/packages/{owner}/{repo}/versions/{version}`. Consumers with a default registry configured install with the same `owner/repo#version` shorthand they would use for GitHub.

Registry archives use the **APM source layout** that `apm install` and the [Registry HTTP API section 6](../../reference/registry-http-api/#6-server-validation-rules-publish) expect -- not the plugin bundle wrapper from `apm pack --archive` (`{name}-{version}/plugin.json`). If you already ship marketplace plugin bundles, either repack as a flat archive or pass `--tarball`.
Registry archives use the **APM source layout** that `apm install` and the [Registry HTTP API section 6](../../reference/registry-http-api/#6-server-validation-rules-publish) expect -- not the plugin bundle wrapper from `apm pack --archive` (`{name}-{version}/plugin.json`). If you already ship marketplace plugin bundles, either repack as a flat archive or pass `--zip`.

**Auto-pack requirements:**

- `apm.yml` with `name:` and `version:`
- A `.apm/` directory with your primitives (skills, instructions, hooks, etc.)

Auto-pack writes `{name}-{version}.tar.gz` in the project root, includes `README.md`, `CHANGELOG.md`, and `LICENSE` / `LICENCE` when present (case-insensitive, symlinks excluded — matching npm's behaviour), and skips macOS `._*` / `.DS_Store` sidecars.
Auto-pack writes `{name}-{version}.zip` in the project root, includes `README.md`, `CHANGELOG.md`, and `LICENSE` / `LICENCE` when present (case-insensitive, symlinks excluded), and skips macOS `._*` / `.DS_Store` sidecars.

**Skill-only or custom layouts** -- build the tarball yourself and pass `--tarball`:
**Custom layouts** -- build the zip yourself and pass `--zip`:

```bash
tar czf my-skill-0.0.1.tar.gz apm.yml SKILL.md
apm publish --tarball my-skill-0.0.1.tar.gz
# Cross-platform zip build (Python stdlib -- no extra tools needed)
python -m zipfile -c ./build/my-skill-0.0.1.zip apm.yml .apm/

apm publish --package acme/my-skill --zip ./build/my-skill-0.0.1.zip
```

Some registries accept archives without validating `apm.yml` on upload; APM still validates on install. Prefer a valid flat layout at publish time.
Expand All @@ -318,8 +320,8 @@ apm publish --package acme/my-skill
# Choose a registry when multiple are configured
apm publish --package acme/my-skill --registry corp-main

# Publish a pre-built flat tarball (skip auto-pack)
apm publish --package acme/my-skill --tarball ./build/my-package-1.0.0.tar.gz
# Publish a pre-built zip (skip auto-pack)
apm publish --package acme/my-skill --zip ./build/my-package-1.0.0.zip

# Preview what would be uploaded without uploading
apm publish --package acme/my-skill --dry-run
Expand All @@ -329,7 +331,7 @@ apm publish --package acme/my-skill --dry-run
|---|---|
| `--registry NAME` | Registry name from the `registries:` block. Required when multiple registries are configured. |
| `--package OWNER/REPO` | Package identity to publish as (required, e.g. `acme/my-skill`). |
| `--tarball PATH` | Path to a pre-built flat `.tar.gz` tarball. Skips auto-pack. |
| `--zip PATH` | Path to a pre-built flat `.zip` archive. Skips auto-pack. |
| `--dry-run` | Preview without uploading. |
| `--verbose` / `-v` | Show detailed output. |

Expand Down
45 changes: 22 additions & 23 deletions docs/src/content/docs/reference/cli/publish.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ apm publish [OPTIONS]

`apm publish` uploads a package version to a configured registry via `PUT /v1/packages/{owner}/{repo}/versions/{version}`.

By default the command **auto-packs** a flat registry archive in the project root (`{name}-{version}.tar.gz`) containing `apm.yml`, `.apm/`, and standard root-level documentation files (`README.md`, `CHANGELOG.md`, `LICENSE` / `LICENCE`, matched case-insensitively) at the tarball root. Symlinks are excluded. This is **not** the plugin bundle layout from [`apm pack`](../pack/) (`{name}-{version}/plugin.json`).
By default the command **auto-packs** a flat registry archive in the project root (`{name}-{version}.zip`) containing `apm.yml`, `.apm/`, and standard root-level documentation files (`README.md`, `CHANGELOG.md`, `LICENSE` / `LICENCE`, matched case-insensitively) at the archive root. Symlinks are excluded. This is **not** the plugin bundle layout from [`apm pack`](../pack/) (`{name}-{version}/plugin.json`).

Requires the experimental `registries` feature:

Expand All @@ -31,30 +31,29 @@ The project's `apm.yml` must declare a `registries:` block with at least one reg
|---|---|---|
| `--registry NAME` | _(required when multiple registries configured)_ | Registry name from the `registries:` block. |
| `--package OWNER/REPO` | _(required)_ | Package identity to publish as (e.g. `acme/my-skill`). |
| `--tarball PATH` | auto-pack | Path to a pre-built `.tar.gz`. Skips auto-pack. |
| `--zip PATH` | auto-pack | Path to a pre-built `.zip`. Skips auto-pack. (renamed from `--tarball` in v0.20.0) |
| `--dry-run` | off | Print what would be uploaded; do not call the registry. |
| `--verbose`, `-v` | off | Show auto-pack details (tarball path). |
| `--verbose`, `-v` | off | Show auto-pack details (archive path). |

## Examples

Auto-pack and publish when only one registry is configured:

```bash
apm publish
apm publish --package acme/my-skill
```

Choose a registry and preview first:

```bash
apm publish --registry corp-main --dry-run -v
apm publish --registry corp-main
apm publish --package acme/my-skill --registry corp-main --dry-run -v
apm publish --package acme/my-skill --registry corp-main
```

Publish a skill-only or custom tarball:
Publish a pre-built zip:

```bash
tar czf my-skill-0.0.1.tar.gz apm.yml SKILL.md
apm publish --tarball my-skill-0.0.1.tar.gz
apm publish --package acme/my-skill --zip ./build/my-skill-0.0.1.zip
```

Specify the registry package identity explicitly:
Expand All @@ -68,25 +67,25 @@ apm publish --package acme/my-package --registry corp-main
### Successful publish

```
[i] Publishing acme/my-package@1.2.3 to corp-main
[i] Publishing acme/my-package@1.2.3 to corp-main...
[+] Published acme/my-package@1.2.3
digest : sha256:abc123
digest : sha256:abc123...
published_at: 2026-05-26T10:15:00Z
registry : https://registry.example.com/apm/corp-main
```

With `--verbose`, auto-pack also prints:

```
[i] Packing flat registry archive -> my-package-1.2.3.tar.gz
[i] Packing flat registry archive -> my-package-1.2.3.zip
```

### Dry run

```
[i] Would publish acme/my-package@1.2.3 to corp-main (https://registry.example.com/apm/corp-main)
[i] tarball : /path/to/project/my-package-1.2.3.tar.gz (12,345 bytes)
[i] (dry-run nothing uploaded)
[i] archive : /path/to/project/my-package-1.2.3.zip (12,345 bytes)
[i] (dry-run -- nothing uploaded)
```

### Common errors
Expand All @@ -95,12 +94,12 @@ With `--verbose`, auto-pack also prints:
|---|---|
| `requires the experimental registries feature` | Run `apm experimental enable registries`. |
| `apm.yml not found` | Run from the package root. |
| `requires a flat APM package (.apm/ directory)` | Add `.apm/` or pass `--tarball`. |
| `requires a flat APM package (.apm/ directory)` | Add `.apm/` or pass `--zip`. |
| `Multiple registries configured` | Pass `--registry NAME`. |
| `Version '' already exists immutable` | HTTP 409 bump `version:` in `apm.yml`. |
| `Registry rejected the package (validation failed)` | HTTP 422 — tarball layout invalid for the server. |
| `Forbidden your token does not have publish permission` | HTTP 403 check `APM_REGISTRY_TOKEN_{NAME}`. |
| `401` / credentials remediation | HTTP 401 token missing or expired. |
| `Version '...' already exists ... immutable` | HTTP 409 -- bump `version:` in `apm.yml`. |
| `Registry rejected the package (validation failed)` | HTTP 422 -- archive layout invalid for the server. |
| `Forbidden -- your token does not have publish permission` | HTTP 403 -- check `APM_REGISTRY_TOKEN_{NAME}`. |
| `401` / credentials remediation | HTTP 401 -- token missing or expired. |

Some registries return `201` with an empty body; APM still treats the upload as successful when the HTTP status is success-class.

Expand All @@ -114,7 +113,7 @@ Some registries return `201` with an empty body; APM still treats the upload as

## Related

- [Registries (guide)](../../../guides/registries/) declare registries, auth, default routing, and policy.
- [`apm pack`](../pack/) plugin bundles and marketplace artifacts (different layout from registry publish).
- [`apm install`](../install/) consumer side; installs registry packages with `resolved_hash` verification.
- [Registry HTTP API](../../registry-http-api/) wire contract for `PUT /versions/{version}`.
- [Registries (guide)](../../../guides/registries/) -- declare registries, auth, default routing, and policy.
- [`apm pack`](../pack/) -- plugin bundles and marketplace artifacts (different layout from registry publish).
- [`apm install`](../install/) -- consumer side; installs registry packages with `resolved_hash` verification.
- [Registry HTTP API](../../registry-http-api/) -- wire contract for `PUT .../versions/{version}`.
4 changes: 2 additions & 2 deletions docs/src/content/docs/reference/registry-http-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ ETag: "sha256:abc123..."

**Body.** Raw archive bytes. The same bytes that hash to the `digest` advertised in `/versions`.

**Format selection at publish time.** APM publishes via `apm pack` (tar.gz). Anthropic skills publish via standard zip. Servers store and return whatever was uploaded; format conversion is NOT a server responsibility.
**Format selection at publish time.** `apm publish` sends `application/zip` by default (modern client). Legacy clients or manual uploads may use `application/gzip`. Servers store and replay whatever was uploaded; format conversion is NOT a server responsibility.

**Errors**

Expand All @@ -267,7 +267,7 @@ Uploads a new version. Versions are immutable: re-publishing returns `409`.
PUT /v1/packages/acme/web-skills/versions/1.2.0 HTTP/1.1
Host: registry.example.com
Authorization: Bearer <publish-token>
Content-Type: application/gzip
Content-Type: application/zip
Content-Length: 24576

<binary archive body>
Expand Down
14 changes: 6 additions & 8 deletions packages/apm-guide/.apm/skills/apm-usage/package-authoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -475,13 +475,13 @@ EOF
export APM_REGISTRY_TOKEN_CORP_MAIN=eyJ...

# 4. Preview then publish
apm publish --registry corp-main --dry-run -v
apm publish --registry corp-main
apm publish --package acme/my-skill --registry corp-main --dry-run -v
apm publish --package acme/my-skill --registry corp-main
```

`apm publish` auto-packs a **flat registry archive** in the project root
(`{name}-{version}.tar.gz`) containing `apm.yml` and `.apm/` at the
tarball root. This layout differs from the plugin bundle that
(`{name}-{version}.zip`) containing `apm.yml` and `.apm/` at the
archive root. This layout differs from the plugin bundle that
`apm pack` produces (`{name}-{version}/plugin.json`). Auto-pack skips
macOS `._*` / `.DS_Store` sidecars.

Expand All @@ -490,12 +490,10 @@ Auto-pack requires:
identity differs from the package name)
- A `.apm/` directory with at least one primitive

Skill-only or custom layouts: build the tarball yourself and pass
`--tarball`:
Custom layouts: build the zip yourself and pass `--zip`:

```bash
tar czf my-skill-0.0.1.tar.gz apm.yml SKILL.md
apm publish --tarball my-skill-0.0.1.tar.gz --registry corp-main
apm publish --package acme/my-skill --zip ./build/my-skill-0.0.1.zip --registry corp-main
```

Upload contract: `PUT /v1/packages/{owner}/{repo}/versions/{version}`.
Expand Down
2 changes: 1 addition & 1 deletion packages/apm-guide/.apm/skills/apm-usage/workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,4 @@ Use `apm install --update` to refresh to latest refs.

## Local bundle install

`apm install <bundle>` accepts a directory or `.tar.gz` produced by `apm pack` and deploys its contents into the consumer's resolved target. Bundles are target-agnostic; the project decides where files land (same precedence as registry installs: `--target` > `apm.yml` > directory detection). For compile-only targets (OpenCode, Codex, Gemini, Antigravity) instructions stage under `apm_modules/<slug>/.apm/instructions/` and the install prints a hint to run `apm compile` to merge them into the target's single-file format (`AGENTS.md`, `GEMINI.md`).
`apm install <bundle>` accepts a directory, `.zip` (default), or legacy `.tar.gz` produced by `apm pack` and deploys its contents into the consumer's resolved target. Bundles are target-agnostic; the project decides where files land (same precedence as registry installs: `--target` > `apm.yml` > directory detection). For compile-only targets (OpenCode, Codex, Gemini, Antigravity) instructions stage under `apm_modules/<slug>/.apm/instructions/` and the install prints a hint to run `apm compile` to merge them into the target's single-file format (`AGENTS.md`, `GEMINI.md`).
18 changes: 9 additions & 9 deletions src/apm_cli/commands/publish.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""``apm publish`` command upload a packed tarball to a registry.
"""``apm publish`` command -- upload a packed zip archive to a registry.

Implements docs/proposals/registry-api.md §5.3:
``PUT /v1/packages/{owner}/{repo}/versions/{version}``
Expand All @@ -23,7 +23,7 @@
Publish a package to a registry.

Reads apm.yml for the package name/version, packs a flat registry zip archive
(``apm.yml`` + ``.apm/`` at the archive root not ``apm pack`` plugin bundles),
(``apm.yml`` + ``.apm/`` at the archive root -- not ``apm pack`` plugin bundles),
or uses a pre-built zip via --zip, then uploads to the registry via
PUT /v1/packages/{owner}/{repo}/versions/{version}.

Expand All @@ -33,16 +33,16 @@
Examples:

# Auto-pack and publish to the only configured registry:
apm publish
apm publish --package acme/my-skill

# Choose a registry when multiple are configured:
apm publish --registry corp-main
apm publish --package acme/my-skill --registry corp-main

# Publish a pre-built zip (skip the pack step):
apm publish --zip ./build/my-package-1.0.0.zip
apm publish --package acme/my-skill --zip ./build/my-package-1.0.0.zip

# Preview what would be uploaded:
apm publish --dry-run
apm publish --package acme/my-skill --dry-run
"""


Expand Down Expand Up @@ -113,7 +113,7 @@ def publish_cmd(ctx, registry_name, package_id, zip_path, dry_run, verbose):
if dry_run:
logger.info(f"Would publish {owner}/{repo}@{version} to {registry_name} ({base_url})")
logger.info(f" archive : {archive} ({archive_size:,} bytes)")
logger.info("(dry-run nothing uploaded)")
logger.info("(dry-run -- nothing uploaded)")
return

# ----------------------------------------------------------- upload
Expand All @@ -123,7 +123,7 @@ def publish_cmd(ctx, registry_name, package_id, zip_path, dry_run, verbose):
auth = make_auth_context(registry_name)
client = RegistryClient(base_url, auth)

logger.info(f"Publishing {owner}/{repo}@{version} to {registry_name}")
logger.info(f"Publishing {owner}/{repo}@{version} to {registry_name}...")

archive_bytes = archive.read_bytes()
try:
Expand Down Expand Up @@ -269,7 +269,7 @@ def _handle_publish_error(
from ..deps.registry.auth import registry_token_env_var

raise click.ClickException(
f"Forbidden your token does not have publish permission for "
f"Forbidden -- your token does not have publish permission for "
f"{owner}/{repo} in {registry_name!r}.\n"
f"Check the token configured via {registry_token_env_var(registry_name)}."
)
Expand Down
2 changes: 1 addition & 1 deletion src/apm_cli/deps/registry/extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def verify_sha256(data: bytes, expected_digest: str) -> str:
actual = hashlib.sha256(data).hexdigest()
expected = _normalize_digest(expected_digest)
if actual != expected:
raise HashMismatchError(f"tarball sha256 mismatch: expected {expected}, got {actual}")
raise HashMismatchError(f"archive sha256 mismatch: expected {expected}, got {actual}")
return actual


Expand Down
12 changes: 6 additions & 6 deletions src/apm_cli/deps/registry/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,13 @@ def _package_info_from_extracted_registry_tree(
if not validation_result.is_valid:
errs = "\n - ".join(validation_result.errors)
raise RegistryResolutionError(
f"registry tarball for {dep_ref.repo_url!r} did not validate "
f"registry archive for {dep_ref.repo_url!r} did not validate "
f"as an APM package:\n - {errs}"
)
package = validation_result.package
if package is None:
raise RegistryResolutionError(
f"registry tarball for {dep_ref.repo_url!r} validated but produced no package metadata"
f"registry archive for {dep_ref.repo_url!r} validated but produced no package metadata"
)
resolved_url = client.archive_url(owner, repo, chosen.version)
package.source = resolved_url
Expand Down Expand Up @@ -290,8 +290,8 @@ def download_package(
_clear_install_target(target_path)

# extract_archive dispatches on Content-Type (with magic-bytes
# fallback) supports both tar.gz (default) and zip (Anthropic
# skills format). Hash check happens before any extraction.
# fallback) -- supports both zip (default) and legacy tar.gz.
# Hash check happens before any extraction.
actual_hash = extract_archive(
archive_bytes,
chosen.digest,
Expand Down Expand Up @@ -360,13 +360,13 @@ def download_from_lockfile(
if not validation_result.is_valid:
errs = "\n - ".join(validation_result.errors)
raise RegistryResolutionError(
f"registry tarball for {dep_ref.repo_url!r} did not validate "
f"registry archive for {dep_ref.repo_url!r} did not validate "
f"as an APM package:\n - {errs}"
)
package = validation_result.package
if package is None:
raise RegistryResolutionError(
f"registry tarball for {dep_ref.repo_url!r} validated but "
f"registry archive for {dep_ref.repo_url!r} validated but "
f"produced no package metadata"
)
package.source = resolved_url
Expand Down
Loading