# metin-release — CLI reference Phase 1 commands. All subcommands share the top-level flags. ## Top-level flags | Flag | Description | |---|---| | `--version` | Print package version and exit. | | `--json` | Emit only the JSON envelope on stdout (stderr may still carry logs). | | `-v`, `--verbose` | Verbose stderr logging. | | `-q`, `--quiet` | Suppress stderr logging entirely. | ## Output envelope Every command writes a JSON envelope on stdout: ```json { "ok": true, "command": "release inspect", "status": "inspected", "stats": { "...": "..." } } ``` On failure: ```json { "ok": false, "command": "release inspect", "status": "failed", "error": { "code": "source_not_found", "message": "..." } } ``` ## Exit codes | Code | Meaning | |---|---| | 0 | Success | | 1 | Operator or validation error | | 2 | Remote / network error | | 3 | Signing or integrity error | | 4 | Reserved (ERP sync, Phase 2+) | ## `release inspect` Scan a client source root and report file counts plus launcher/main-exe detection. ``` metin-release release inspect --source /path/to/client ``` No writes. JSON stats: `source_path`, `file_count`, `total_bytes`, `launcher_present`, `main_exe_present`. ## `release build-manifest` Wraps `make-manifest.py` to produce `manifest.json` for a source tree. ``` metin-release release build-manifest \ --source /path/to/client \ --version 2026.04.14-1 \ --out /tmp/release/manifest.json \ [--previous 2026.04.13-3] \ [--notes release-notes.md] \ [--launcher Metin2Launcher.exe] \ [--created-at 2026-04-14T12:00:00Z] ``` Override the wrapped script path via the `METIN_RELEASE_MAKE_MANIFEST` env var. ## `release sign` Wraps `sign-manifest.py`. Requires an absolute path to a chmod-600 raw 32-byte Ed25519 key. ``` metin-release release sign \ --manifest /tmp/release/manifest.json \ --key /home/you/.config/metin/launcher-signing-key \ [--out /tmp/release/manifest.json.sig] ``` Override the wrapped script path via `METIN_RELEASE_SIGN_MANIFEST`. ## `release diff-remote` HEADs every unique blob hash from a manifest against `/files//`. ``` metin-release release diff-remote \ --manifest /tmp/release/manifest.json \ --base-url https://updates.example.com ``` ## `release upload-blobs` rsyncs the release directory (excluding `manifest.json` and `.sig`) to a target. ``` metin-release release upload-blobs \ --release-dir /tmp/release \ --rsync-target user@host:/var/www/updates/ \ [--dry-run] [--yes] ``` ## `release promote` Pushes only `manifest.json` + `manifest.json.sig` to the target top-level — makes the new release live. ``` metin-release release promote \ --release-dir /tmp/release \ --rsync-target user@host:/var/www/updates/ \ [--dry-run] [--yes] ``` ## `release verify-public` GETs `manifest.json` + `manifest.json.sig` from a public URL and verifies the Ed25519 signature. Optionally spot-checks random blobs with `--sample-blobs N`. ``` metin-release release verify-public \ --base-url https://updates.example.com \ --public-key \ [--sample-blobs 5] ``` ## `release publish` Composite: build-manifest → sign → stage blob tree → upload-blobs → promote → verify-public. Short-circuits on the first failure; the JSON envelope includes a `stages` array with `{name, status, duration_ms, error?}` per step. ``` metin-release release publish \ --source /path/to/client \ --version 2026.04.14-1 \ --out /tmp/release \ --key /home/you/.config/metin/launcher-signing-key \ --rsync-target user@host:/var/www/updates/ \ --base-url https://updates.example.com \ --public-key \ [--previous ...] [--notes ...] [--launcher ...] \ [--created-at ...] [--sample-blobs N] \ [--yes] [--force] [--dry-run-upload] ```