|
| 1 | +# socket-patch CLI contract |
| 2 | + |
| 3 | +This document defines the **public surface** of the `socket-patch` binary. Anything listed here is part of the user-visible contract: third-party scripts, CI pipelines, and the npm/pypi/cargo wrappers depend on it. Changes are governed by the semver policy at the bottom of this file. |
| 4 | + |
| 5 | +> **Why this exists.** Until late 2026 the CLI crate had zero unit tests under `src/` — only network-dependent `tests/e2e_*.rs` suites that run with `--ignored`. A flag rename, a default-value change, or a JSON key rename could land green and break every shipped wrapper silently. The contract below is now backed by the unit tests under `crates/socket-patch-cli/src/**` (`#[cfg(test)] mod tests`) and the parser tests under `crates/socket-patch-cli/tests/cli_parse_*.rs`. Changes that violate the contract must update those tests in lock-step with a major version bump. |
| 6 | +
|
| 7 | +## Subcommands |
| 8 | + |
| 9 | +| Name | Visible alias(es) | Notes | |
| 10 | +|---|---|---| |
| 11 | +| `apply` | — | Apply patches from the local manifest | |
| 12 | +| `rollback` | — | Restore original files; takes optional positional `identifier` | |
| 13 | +| `get` | `download` | Fetch + apply patch; requires positional `identifier` | |
| 14 | +| `scan` | — | Crawl installed packages for available patches | |
| 15 | +| `list` | — | Print patches in the local manifest | |
| 16 | +| `remove` | — | Remove patch from manifest (rolls back first); requires positional `identifier` | |
| 17 | +| `setup` | — | Configure package.json postinstall scripts | |
| 18 | +| `repair` | `gc` | Download missing blobs + clean up unused ones | |
| 19 | + |
| 20 | +**Bare-UUID fallback.** `socket-patch <UUID>` is rewritten to `socket-patch get <UUID>`. The UUID shape checked is the standard 8-4-4-4-12 hex pattern (case-insensitive). See [`src/lib.rs::looks_like_uuid`](src/lib.rs). |
| 21 | + |
| 22 | +## Flags — long and short forms |
| 23 | + |
| 24 | +Every flag below is part of the contract. The default values are pinned by parser tests. |
| 25 | + |
| 26 | +### `apply` |
| 27 | + |
| 28 | +| Long | Short | Default | Type | |
| 29 | +|---|---|---|---| |
| 30 | +| `--cwd` | — | `.` | path | |
| 31 | +| `--dry-run` | `-d` | `false` | bool | |
| 32 | +| `--silent` | `-s` | `false` | bool | |
| 33 | +| `--manifest-path` | `-m` | `.socket/manifest.json` | string | |
| 34 | +| `--offline` | — | `false` | bool | |
| 35 | +| `--global` | `-g` | `false` | bool | |
| 36 | +| `--global-prefix` | — | (none) | path | |
| 37 | +| `--ecosystems` | — | (none) | CSV → `Vec<String>` | |
| 38 | +| `--force` | `-f` | `false` | bool | |
| 39 | +| `--json` | — | `false` | bool | |
| 40 | +| `--verbose` | `-v` | `false` | bool | |
| 41 | +| `--download-mode` | — | **`diff`** | string | |
| 42 | + |
| 43 | +### `rollback` |
| 44 | + |
| 45 | +Same as `apply` plus: `--one-off` (bool), `--org` (string), `--api-url` (string), `--api-token` (string). Positional `identifier` is **optional** (omit to rollback everything). |
| 46 | + |
| 47 | +### `get` |
| 48 | + |
| 49 | +Required positional `identifier`. Flags: |
| 50 | + |
| 51 | +| Long | Short | Alias | Default | Type | |
| 52 | +|---|---|---|---|---| |
| 53 | +| `--org` | — | — | (none) | string | |
| 54 | +| `--cwd` | — | — | `.` | path | |
| 55 | +| `--id` | — | — | `false` | bool | |
| 56 | +| `--cve` | — | — | `false` | bool | |
| 57 | +| `--ghsa` | — | — | `false` | bool | |
| 58 | +| `--package` | `-p` | — | `false` | bool | |
| 59 | +| `--yes` | `-y` | — | `false` | bool | |
| 60 | +| `--api-url` | — | — | (none) | string | |
| 61 | +| `--api-token` | — | — | (none) | string | |
| 62 | +| `--save-only` | — | **`--no-apply`** | `false` | bool | |
| 63 | +| `--global` | `-g` | — | `false` | bool | |
| 64 | +| `--global-prefix` | — | — | (none) | path | |
| 65 | +| `--one-off` | — | — | `false` | bool | |
| 66 | +| `--json` | — | — | `false` | bool | |
| 67 | +| `--download-mode` | — | — | **`diff`** | string | |
| 68 | + |
| 69 | +The hidden alias `--no-apply` on `--save-only` is **part of the contract** — it does not appear in `--help` but is widely used in existing scripts. |
| 70 | + |
| 71 | +### `scan` |
| 72 | + |
| 73 | +| Long | Short | Default | Type | |
| 74 | +|---|---|---|---| |
| 75 | +| `--cwd` | — | `.` | path | |
| 76 | +| `--org` | — | (none) | string | |
| 77 | +| `--json` | — | `false` | bool | |
| 78 | +| `--yes` | `-y` | `false` | bool | |
| 79 | +| `--global` | `-g` | `false` | bool | |
| 80 | +| `--global-prefix` | — | (none) | path | |
| 81 | +| `--batch-size` | — | **`100`** | usize | |
| 82 | +| `--api-url` | — | (none) | string | |
| 83 | +| `--api-token` | — | (none) | string | |
| 84 | +| `--ecosystems` | — | (none) | CSV → `Vec<String>` | |
| 85 | +| `--download-mode` | — | **`diff`** | string | |
| 86 | + |
| 87 | +### `list` |
| 88 | + |
| 89 | +| Long | Short | Default | Type | |
| 90 | +|---|---|---|---| |
| 91 | +| `--cwd` | — | `.` | path | |
| 92 | +| `--manifest-path` | `-m` | `.socket/manifest.json` | string | |
| 93 | +| `--json` | — | `false` | bool | |
| 94 | + |
| 95 | +### `remove` |
| 96 | + |
| 97 | +Required positional `identifier`. Flags: |
| 98 | + |
| 99 | +| Long | Short | Default | Type | |
| 100 | +|---|---|---|---| |
| 101 | +| `--cwd` | — | `.` | path | |
| 102 | +| `--manifest-path` | `-m` | `.socket/manifest.json` | string | |
| 103 | +| `--skip-rollback` | — | `false` | bool | |
| 104 | +| `--yes` | `-y` | `false` | bool | |
| 105 | +| `--global` | `-g` | `false` | bool | |
| 106 | +| `--global-prefix` | — | (none) | path | |
| 107 | +| `--json` | — | `false` | bool | |
| 108 | + |
| 109 | +### `setup` |
| 110 | + |
| 111 | +| Long | Short | Default | Type | |
| 112 | +|---|---|---|---| |
| 113 | +| `--cwd` | — | `.` | path | |
| 114 | +| `--dry-run` | `-d` | `false` | bool | |
| 115 | +| `--yes` | `-y` | `false` | bool | |
| 116 | +| `--json` | — | `false` | bool | |
| 117 | + |
| 118 | +### `repair` |
| 119 | + |
| 120 | +| Long | Short | Default | Type | |
| 121 | +|---|---|---|---| |
| 122 | +| `--cwd` | — | `.` | path | |
| 123 | +| `--manifest-path` | `-m` | `.socket/manifest.json` | string | |
| 124 | +| `--dry-run` | `-d` | `false` | bool | |
| 125 | +| `--offline` | — | `false` | bool | |
| 126 | +| `--download-only` | — | `false` | bool | |
| 127 | +| `--json` | — | `false` | bool | |
| 128 | +| `--download-mode` | — | **`file`** | string | |
| 129 | + |
| 130 | +**Note:** `repair`'s `--download-mode` default differs from every other command (`file` vs `diff`). This is intentional — repair restores legacy per-file blobs needed to apply any patch. |
| 131 | + |
| 132 | +## CSV value parsing |
| 133 | + |
| 134 | +`--ecosystems` on `apply`, `rollback`, and `scan` uses clap's `value_delimiter = ','`. Input `--ecosystems npm,pypi,cargo` becomes `vec!["npm", "pypi", "cargo"]`. Switching to space-separated or dropping the delimiter is a **breaking** change. |
| 135 | + |
| 136 | +## JSON output shapes |
| 137 | + |
| 138 | +When `--json` is set, commands print a single JSON object to stdout. The schemas below are stable. |
| 139 | + |
| 140 | +### Missing-manifest error (`apply`/`list`/`remove`/`repair`/`rollback`) |
| 141 | + |
| 142 | +```json |
| 143 | +{ |
| 144 | + "status": "error", |
| 145 | + "error": "Manifest not found", |
| 146 | + "path": "<absolute path that was looked up>" |
| 147 | +} |
| 148 | +``` |
| 149 | + |
| 150 | +### Invalid-manifest error |
| 151 | + |
| 152 | +```json |
| 153 | +{ "status": "error", "error": "Invalid manifest" } |
| 154 | +``` |
| 155 | + |
| 156 | +### Generic error |
| 157 | + |
| 158 | +```json |
| 159 | +{ "status": "error", "error": "<message>" } |
| 160 | +``` |
| 161 | + |
| 162 | +### `list` success — empty manifest |
| 163 | + |
| 164 | +```json |
| 165 | +{ "status": "success", "patches": [] } |
| 166 | +``` |
| 167 | + |
| 168 | +### `list` success — populated |
| 169 | + |
| 170 | +```json |
| 171 | +{ |
| 172 | + "status": "success", |
| 173 | + "patches": [ |
| 174 | + { |
| 175 | + "purl": "pkg:npm/foo@1.2.3", |
| 176 | + "uuid": "…", |
| 177 | + "exportedAt": "…", |
| 178 | + "tier": "free|paid", |
| 179 | + "license": "…", |
| 180 | + "description": "…", |
| 181 | + "files": ["…"], |
| 182 | + "vulnerabilities": [ |
| 183 | + { "id": "…", "cves": ["…"], "summary": "…", "severity": "…", "description": "…" } |
| 184 | + ] |
| 185 | + } |
| 186 | + ] |
| 187 | +} |
| 188 | +``` |
| 189 | + |
| 190 | +### `setup` — no package.json files found |
| 191 | + |
| 192 | +```json |
| 193 | +{ |
| 194 | + "status": "no_files", |
| 195 | + "updated": 0, |
| 196 | + "alreadyConfigured": 0, |
| 197 | + "errors": 0, |
| 198 | + "files": [] |
| 199 | +} |
| 200 | +``` |
| 201 | + |
| 202 | +### `get` — multiple-patch selection required (JSON mode) |
| 203 | + |
| 204 | +```json |
| 205 | +{ |
| 206 | + "status": "selection_required", |
| 207 | + "error": "Multiple patches available for <purl>. Specify --id <UUID> to select one.", |
| 208 | + "purl": "<purl>", |
| 209 | + "options": [ |
| 210 | + { "uuid": "…", "tier": "…", "published_at": "…", "description": "…", "vulnerabilities": [ … ] } |
| 211 | + ] |
| 212 | +} |
| 213 | +``` |
| 214 | + |
| 215 | +## Exit codes |
| 216 | + |
| 217 | +| Code | Meaning | |
| 218 | +|---|---| |
| 219 | +| `0` | Success | |
| 220 | +| `1` | Error (missing/invalid manifest, fetch failed, apply failed, selection cancelled in non-JSON mode, etc.) | |
| 221 | + |
| 222 | +`list` returns **`0`** for an empty manifest and **`1`** for a missing manifest — these are distinct and load-bearing. |
| 223 | + |
| 224 | +## Semver policy |
| 225 | + |
| 226 | +Versioning lives in **`Cargo.toml`** at the workspace root (`version = "..."`) and is propagated to npm, pypi, and cargo wrappers by **`scripts/version-sync.sh <new-version>`**. |
| 227 | + |
| 228 | +| Change | Bump | |
| 229 | +|---|---| |
| 230 | +| Rename or remove a subcommand | **MAJOR** | |
| 231 | +| Rename or remove a visible alias (`download`, `gc`) | **MAJOR** | |
| 232 | +| Rename or remove a hidden alias (`--no-apply`) | **MAJOR** | |
| 233 | +| Rename, remove, or change short form of a flag (`-d`, `-m`, etc.) | **MAJOR** | |
| 234 | +| Change a default value (`--download-mode`, `--batch-size`, `--manifest-path`, …) | **MAJOR** | |
| 235 | +| Change an exit code's meaning or add a new non-zero code with different semantics | **MAJOR** | |
| 236 | +| Rename a JSON output key or change a `status` string | **MAJOR** | |
| 237 | +| Remove a JSON output key | **MAJOR** | |
| 238 | +| Drop the bare-UUID fallback | **MAJOR** | |
| 239 | +| Add a *required* new flag | **MAJOR** | |
| 240 | +| Add a new subcommand | **MINOR** | |
| 241 | +| Add a new optional flag | **MINOR** | |
| 242 | +| Add a new optional JSON output key (additive) | **MINOR** | |
| 243 | +| Add a new visible alias to an existing subcommand | **MINOR** | |
| 244 | +| Fix a bug without changing any of the above | **PATCH** | |
| 245 | + |
| 246 | +After bumping `Cargo.toml`, run: |
| 247 | + |
| 248 | +```bash |
| 249 | +scripts/version-sync.sh <new-version> |
| 250 | +``` |
| 251 | + |
| 252 | +This syncs the workspace package version into: |
| 253 | + |
| 254 | +- `npm/socket-patch/package.json` (and its `optionalDependencies`) |
| 255 | +- every per-platform `npm/socket-patch-*/package.json` |
| 256 | +- `pypi/socket-patch/pyproject.toml` |
| 257 | + |
| 258 | +## How the contract is enforced |
| 259 | + |
| 260 | +Every item in this document is locked in by at least one of: |
| 261 | + |
| 262 | +- **clap parser snapshots** in `crates/socket-patch-cli/tests/cli_parse_*.rs` — assert flag names, short forms, defaults, aliases, and CSV delimiters by calling `socket_patch_cli::Cli::try_parse_from(...)`. |
| 263 | +- **Helper unit tests** in `crates/socket-patch-cli/src/**` (`#[cfg(test)] mod tests` blocks) — cover `looks_like_uuid`, `parse_with_uuid_fallback`, `detect_identifier_type`, `select_patches`, `find_patches_to_rollback`, `partition_purls`, `verify_status_str`, `format_severity`, `color`, and the JSON serializers. |
| 264 | +- **Async `run()` integration tests** in `tests/cli_parse_list.rs`, `tests/cli_parse_remove.rs`, `tests/cli_parse_setup.rs` — exercise the no-network error paths and assert JSON shape via `serde_json::from_str::<Value>` + per-key assertions. |
| 265 | + |
| 266 | +If you add a new flag/subcommand/JSON key, add a test here that locks the new surface in the same PR. |
0 commit comments