From 46ecdab80611b34e0f93ecdc50857d19b77c5520 Mon Sep 17 00:00:00 2001
From: nadavy <nadavy@jfrog.com>
Date: Tue, 16 Jun 2026 15:40:07 +0300
Subject: [PATCH] fix: correct stale tar.gz/tarball refs and publish UX gaps
 after zip default (#1779)

apm publish and apm pack --archive now produce .zip by default (#1720).
This commit cleans all remaining stale references and fixes five UX gaps
surfaced in the post-merge panel review.

Stale reference cleanup:
- docs/reference/cli/publish.md: .tar.gz -> .zip, --tarball -> --zip,
  tarball root -> archive root, remove tar czf example, fix ASCII
- docs/guides/registries.md: same fixes + add cross-platform zip-build
  example in Custom layouts section
- docs/reference/registry-http-api.md: correct publish format note
- packages/apm-guide/.apm/skills/apm-usage/package-authoring.md:
  .tar.gz -> .zip, tarball -> archive, --tarball -> --zip
- packages/apm-guide/.apm/skills/apm-usage/workflow.md: .zip as default
- src/apm_cli/commands/publish.py: module docstring + em dash + tarball
- src/apm_cli/deps/registry/extractor.py: "tarball sha256 mismatch"
  -> "archive sha256 mismatch"
- src/apm_cli/deps/registry/resolver.py: em dash + tar.gz default

Panel review fixes (post-merge):
- publish.py _PUBLISH_HELP: add --package to all four examples (required=True
  flag was missing from every example -- copy-paste crash)
- publish.py line 272: replace em dash with -- in Forbidden ClickException
  (user-visible on 403 responses; prior commit missed this one)
- resolver.py lines 98, 104, 363, 369: replace "registry tarball for" with
  "registry archive for" in RegistryResolutionError strings (same class of
  fix as extractor.py, missed in first pass)
- package-authoring.md: add --package to all publish examples
- registries.md Custom layouts: add --package + python -m zipfile build step
  (Windows users had no actionable guidance after tar czf example was removed)
- publish.md --zip row: add "(renamed from --tarball in v0.20.0)" migration
  note so CI pipelines using --tarball get a recovery path

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---
 docs/src/content/docs/guides/registries.md    | 20 +++++----
 .../src/content/docs/reference/cli/publish.md | 45 +++++++++----------
 .../docs/reference/registry-http-api.md       |  4 +-
 .../skills/apm-usage/package-authoring.md     | 14 +++---
 .../.apm/skills/apm-usage/workflow.md         |  2 +-
 src/apm_cli/commands/publish.py               | 18 ++++----
 src/apm_cli/deps/registry/extractor.py        |  2 +-
 src/apm_cli/deps/registry/resolver.py         | 12 ++---
 8 files changed, 58 insertions(+), 59 deletions(-)

diff --git a/docs/src/content/docs/guides/registries.md b/docs/src/content/docs/guides/registries.md
index 8feef0af6..6ea6eafae 100644
--- a/docs/src/content/docs/guides/registries.md
+++ b/docs/src/content/docs/guides/registries.md
@@ -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.
@@ -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
@@ -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. |
 
diff --git a/docs/src/content/docs/reference/cli/publish.md b/docs/src/content/docs/reference/cli/publish.md
index 101c3b89b..d5ebabd33 100644
--- a/docs/src/content/docs/reference/cli/publish.md
+++ b/docs/src/content/docs/reference/cli/publish.md
@@ -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:
 
@@ -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:
@@ -68,9 +67,9 @@ 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
 ```
@@ -78,15 +77,15 @@ apm publish --package acme/my-package --registry 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
@@ -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.
 
@@ -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}`.
diff --git a/docs/src/content/docs/reference/registry-http-api.md b/docs/src/content/docs/reference/registry-http-api.md
index 533d156df..a02e2644d 100644
--- a/docs/src/content/docs/reference/registry-http-api.md
+++ b/docs/src/content/docs/reference/registry-http-api.md
@@ -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**
 
@@ -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>
diff --git a/packages/apm-guide/.apm/skills/apm-usage/package-authoring.md b/packages/apm-guide/.apm/skills/apm-usage/package-authoring.md
index 836d0c61c..186bda02d 100644
--- a/packages/apm-guide/.apm/skills/apm-usage/package-authoring.md
+++ b/packages/apm-guide/.apm/skills/apm-usage/package-authoring.md
@@ -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.
 
@@ -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}`.
diff --git a/packages/apm-guide/.apm/skills/apm-usage/workflow.md b/packages/apm-guide/.apm/skills/apm-usage/workflow.md
index c86a9876c..2d4bf2e7d 100644
--- a/packages/apm-guide/.apm/skills/apm-usage/workflow.md
+++ b/packages/apm-guide/.apm/skills/apm-usage/workflow.md
@@ -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`).
diff --git a/src/apm_cli/commands/publish.py b/src/apm_cli/commands/publish.py
index 72ae41e0b..dc93b772e 100644
--- a/src/apm_cli/commands/publish.py
+++ b/src/apm_cli/commands/publish.py
@@ -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}``
@@ -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}.
 
@@ -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
 """
 
 
@@ -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
@@ -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:
@@ -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)}."
         )
diff --git a/src/apm_cli/deps/registry/extractor.py b/src/apm_cli/deps/registry/extractor.py
index 69feb0d45..a2d34ca31 100644
--- a/src/apm_cli/deps/registry/extractor.py
+++ b/src/apm_cli/deps/registry/extractor.py
@@ -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
 
 
diff --git a/src/apm_cli/deps/registry/resolver.py b/src/apm_cli/deps/registry/resolver.py
index de0aac5c4..1387d687a 100644
--- a/src/apm_cli/deps/registry/resolver.py
+++ b/src/apm_cli/deps/registry/resolver.py
@@ -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
@@ -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,
@@ -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
