Skip to content

Commit c04f68c

Browse files
committed
refactor(cli): unify CLI args + env-var bindings across every subcommand
Define a single `GlobalArgs` clap struct and `#[command(flatten)]` it into every subcommand's args. Every flag now has a matching `SOCKET_*` env var binding (precedence: CLI > env > default). Legacy `SOCKET_PATCH_PROXY_URL`, `SOCKET_PATCH_DEBUG`, `SOCKET_PATCH_TELEMETRY_DISABLED` are still honored at runtime via a one-shot deprecation warning that fires even under `--silent` / `--json`. Behavior changes: - `--offline` now means strict airgap on every command (was three different things across apply / repair / rollback). On `repair`, `--offline` and `--download-only` are mutually exclusive. - `repair --download-mode` default flipped from `file` to `diff` to match every other command. Users who need the legacy per-file blob behavior opt in with `--download-mode file`. - `apply` and `repair` gain `--api-url` / `--api-token` / `--org` for free via the flatten (previously only readable via env). - `--debug` and `--no-telemetry` promoted from env-only toggles to CLI flags. CLI_CONTRACT.md rewritten around a single global-args table plus a small per-subcommand section for local flags. New tests: `cli_global_args.rs` (compose test: every global flag × every subcommand) and `cli_env_deprecation.rs` (legacy-env warning fires under `--silent` / `--json`). Assisted-by: Claude Code:opus-4-7
1 parent 54bfb0a commit c04f68c

38 files changed

Lines changed: 1851 additions & 1255 deletions

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ repository = "https://github.com/SocketDev/socket-patch"
1010

1111
[workspace.dependencies]
1212
socket-patch-core = { path = "crates/socket-patch-core", version = "=3.0.0" }
13-
clap = { version = "=4.5.60", features = ["derive"] }
13+
clap = { version = "=4.5.60", features = ["derive", "env"] }
1414
serde = { version = "=1.0.228", features = ["derive"] }
1515
serde_json = "=1.0.149"
1616
sha2 = "=0.10.9"

crates/socket-patch-cli/CLI_CONTRACT.md

Lines changed: 81 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -19,129 +19,98 @@ This document defines the **public surface** of the `socket-patch` binary. Anyth
1919

2020
**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).
2121

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 |
22+
## Global arguments
23+
24+
In v3.0 every subcommand accepts the same set of "global" flags via a single shared `GlobalArgs` struct that's `#[command(flatten)]`-ed into each per-command struct (`crates/socket-patch-cli/src/args.rs`). Subcommands that don't actually consume a given flag accept it silently — e.g. `list --global` parses fine and is a no-op. Every flag also has an environment-variable binding; precedence is **CLI arg > env var > default**.
25+
26+
| Long | Short | Env var | Default | Type | Semantic |
27+
|---|---|---|---|---|---|
28+
| `--cwd` || `SOCKET_CWD` | `.` | path | Working directory |
29+
| `--manifest-path` | `-m` | `SOCKET_MANIFEST_PATH` | `.socket/manifest.json` | path | Manifest location (resolved relative to `--cwd`) |
30+
| `--api-url` || `SOCKET_API_URL` | `https://api.socket.dev` | string | Authenticated API endpoint |
31+
| `--api-token` || `SOCKET_API_TOKEN` | (none) | string | Auth token (absence selects the public proxy) |
32+
| `--org` | `-o` | `SOCKET_ORG_SLUG` | (auto-resolve) | string | Org slug |
33+
| `--proxy-url` || `SOCKET_PROXY_URL` | `https://patches-api.socket.dev` | string | Public proxy when no token |
34+
| `--ecosystems` | `-e` | `SOCKET_ECOSYSTEMS` | (all) | CSV → `Vec<String>` | Restrict to these ecosystems |
35+
| `--download-mode` || `SOCKET_DOWNLOAD_MODE` | **`diff`** | enum: `diff` \| `package` \| `file` | Patch artifact format |
36+
| `--offline` || `SOCKET_OFFLINE` | `false` | bool | **Strict airgap on every command** — never contact the network |
37+
| `--global` | `-g` | `SOCKET_GLOBAL` | `false` | bool | Operate on globally-installed packages |
38+
| `--global-prefix` || `SOCKET_GLOBAL_PREFIX` | (auto) | path | Override global packages root |
39+
| `--json` | `-j` | `SOCKET_JSON` | `false` | bool | Machine-readable output |
40+
| `--verbose` | `-v` | `SOCKET_VERBOSE` | `false` | bool | Extra detail |
41+
| `--silent` | `-s` | `SOCKET_SILENT` | `false` | bool | Errors only |
42+
| `--dry-run` | `-d` | `SOCKET_DRY_RUN` | `false` | bool | Preview, no mutations |
43+
| `--yes` | `-y` | `SOCKET_YES` | `false` | bool | Skip prompts |
44+
| `--debug` || `SOCKET_DEBUG` | `false` | bool | Verbose debug logs to stderr |
45+
| `--no-telemetry` || `SOCKET_TELEMETRY_DISABLED` | `false` | bool | Disable anonymous usage telemetry |
46+
47+
The `--offline` semantics unified in v3.0. Previously `apply` enforced strict airgap, `repair` skipped network ops, and `rollback` failed when blobs were missing. All three now mean the same thing: never contact the network, fail loudly when a required local source is missing. On `repair`, `--offline` and `--download-only` are mutually exclusive.
48+
49+
## Per-subcommand arguments
50+
51+
Beyond the globals above, each subcommand defines a small set of local arguments.
52+
53+
| Subcommand | Local arg | Env var | Purpose |
7454
|---|---|---|---|
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-
| `--apply` || `false` | bool |
87-
| `--prune` || `false` | bool |
88-
| `--sync` || `false` | bool |
89-
| `--dry-run` | `-d` | `false` | bool |
55+
| `apply` | `--force` / `-f` | `SOCKET_FORCE` | Bypass beforeHash check |
56+
| `scan` | `--apply` / `--prune` / `--sync` || Mode selectors (sync = apply + prune) |
57+
| `scan` | `--batch-size` | `SOCKET_BATCH_SIZE` | API batch chunk size (default `100`) |
58+
| `get` | positional `identifier`; `--id` / `--cve` / `--ghsa` / `--package` (`-p`); `--save-only` (alias `--no-apply`); `--one-off` | `SOCKET_SAVE_ONLY`, `SOCKET_ONE_OFF` | Patch lookup + save-vs-apply mode |
59+
| `remove` | positional `identifier`; `--skip-rollback` | `SOCKET_SKIP_ROLLBACK` | Manifest entry removal |
60+
| `rollback` | optional positional `identifier`; `--one-off` | `SOCKET_ONE_OFF` | Rollback target |
61+
| `repair` | `--download-only` | `SOCKET_DOWNLOAD_ONLY` | Repair-specific cleanup mode (mutually exclusive with `--offline`) |
62+
| `setup` | (none beyond globals) |||
9063

91-
`--apply` opts JSON callers into the full discover → select → apply pipeline. Without it, `scan --json` stays read-only (discovery + `updates` array only). No effect outside `--json` mode — the non-JSON path always prompts the user interactively. Designed for unattended workflows (cron jobs, bots that open PRs).
64+
`scan --apply` opts JSON callers into the full discover → select → apply pipeline. Without it, `scan --json` stays read-only (discovery + `updates` array only). No effect outside `--json` mode — the non-JSON path always prompts the user interactively.
9265

93-
`--prune` opts into garbage collection. When set, `scan` removes manifest entries for packages no longer present in the crawl, then deletes orphan blob, diff, and package-archive files from `.socket/`. Off by default (v3.0) so a temporary uninstall doesn't silently destroy manifest state. Pair with `--apply` (or use `--sync`) for the auto-update workflow.
66+
`scan --prune` opts into garbage collection. When set, `scan` removes manifest entries for packages no longer present in the crawl, then deletes orphan blob, diff, and package-archive files from `.socket/`. Off by default (v3.0) so a temporary uninstall doesn't silently destroy manifest state.
9467

95-
`--sync` is sugar for `--apply --prune` — the canonical single-flag bot invocation. `scan --json --sync --yes` discovers, applies, and reconciles state in one pass.
68+
`scan --sync` is sugar for `--apply --prune` — the canonical single-flag bot invocation. `scan --json --sync --yes` discovers, applies, and reconciles state in one pass.
9669

97-
`--dry-run` (`-d`) previews what `--apply` / `--prune` / `--sync` would do without mutating disk. In JSON mode, `apply.patches[*]` is populated with would-be actions (computed via `decide_patch_action` against the current manifest) and `gc.prunable*` / `gc.orphan*` fields report counts via the cleanup helpers' built-in dry-run mode. No effect without at least one of `--apply`, `--prune`, or `--sync`.
70+
`--dry-run` previews what `apply` / `rollback` / `scan --apply` / `repair` would do without mutating disk. In JSON mode, the envelope is populated with would-be actions and counts.
9871

99-
### `list`
72+
The hidden alias `--no-apply` on `get --save-only` is **part of the contract** — it does not appear in `--help` but is widely used in existing scripts.
10073

101-
| Long | Short | Default | Type |
102-
|---|---|---|---|
103-
| `--cwd` || `.` | path |
104-
| `--manifest-path` | `-m` | `.socket/manifest.json` | string |
105-
| `--json` || `false` | bool |
74+
`repair` keeps its `gc` visible alias.
10675

107-
### `remove`
76+
## Environment variables
10877

109-
Required positional `identifier`. Flags:
78+
All v3.0 env vars use the `SOCKET_*` prefix. Three legacy `SOCKET_PATCH_*` names are still honored at runtime for compatibility: on first read of any of the three the binary emits a one-shot deprecation warning to stderr (the warning fires unconditionally — even under `--silent` / `--json` — because it's a transition signal users need to see). The legacy names will be removed in the next major release.
11079

111-
| Long | Short | Default | Type |
80+
| Env var | CLI equivalent | Default | Notes |
11281
|---|---|---|---|
113-
| `--cwd` | | `.` | path |
114-
| `--manifest-path` | `-m` | `.socket/manifest.json` | string |
115-
| `--skip-rollback` | | `false` | bool |
116-
| `--yes` | `-y` | `false` | bool |
117-
| `--global` | `-g` | `false` | bool |
118-
| `--global-prefix` | | (none) | path |
119-
| `--json` | | `false` | bool |
120-
121-
### `setup`
122-
123-
| Long | Short | Default | Type |
124-
|---|---|---|---|
125-
| `--cwd` | | `.` | path |
126-
| `--dry-run` | `-d` | `false` | bool |
127-
| `--yes` | `-y` | `false` | bool |
128-
| `--json` | | `false` | bool |
129-
130-
### `repair`
131-
132-
`repair` (alias `gc`) is a first-class command for cleaning up the `.socket/` directory without running a scan. For the combined discover-and-apply workflow with GC, use `scan --sync --json --yes`; for cleanup alone, use `repair` (or `gc`) directly. The `gc` visible alias is part of the contract — removing or demoting it is a MAJOR bump.
133-
134-
| Long | Short | Default | Type |
135-
|---|---|---|---|
136-
| `--cwd` | | `.` | path |
137-
| `--manifest-path` | `-m` | `.socket/manifest.json` | string |
138-
| `--dry-run` | `-d` | `false` | bool |
139-
| `--offline` || `false` | bool |
140-
| `--download-only` | | `false` | bool |
141-
| `--json` || `false` | bool |
142-
| `--download-mode` | | **`file`** | string |
143-
144-
**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.
82+
| `SOCKET_CWD` | `--cwd` | `.` | |
83+
| `SOCKET_MANIFEST_PATH` | `--manifest-path` / `-m` | `.socket/manifest.json` | |
84+
| `SOCKET_API_URL` | `--api-url` | `https://api.socket.dev` | |
85+
| `SOCKET_API_TOKEN` | `--api-token` | (none) | Absence selects the public proxy. |
86+
| `SOCKET_ORG_SLUG` | `--org` / `-o` | (auto-resolve) | |
87+
| `SOCKET_PROXY_URL` | `--proxy-url` | `https://patches-api.socket.dev` | **Renamed in v3.0** (was `SOCKET_PATCH_PROXY_URL`). |
88+
| `SOCKET_ECOSYSTEMS` | `--ecosystems` / `-e` | (all) | Comma-separated list. |
89+
| `SOCKET_DOWNLOAD_MODE` | `--download-mode` | `diff` | One of `diff` / `package` / `file`. |
90+
| `SOCKET_OFFLINE` | `--offline` | `false` ||
91+
| `SOCKET_GLOBAL` | `--global` / `-g` | `false` ||
92+
| `SOCKET_GLOBAL_PREFIX` | `--global-prefix` | (auto) | |
93+
| `SOCKET_JSON` | `--json` / `-j` | `false` ||
94+
| `SOCKET_VERBOSE` | `--verbose` / `-v` | `false` | |
95+
| `SOCKET_SILENT` | `--silent` / `-s` | `false` | |
96+
| `SOCKET_DRY_RUN` | `--dry-run` / `-d` | `false` | |
97+
| `SOCKET_YES` | `--yes` / `-y` | `false` | |
98+
| `SOCKET_DEBUG` | `--debug` | `false` | **Renamed in v3.0** (was `SOCKET_PATCH_DEBUG`). |
99+
| `SOCKET_TELEMETRY_DISABLED` | `--no-telemetry` | `false` | **Renamed in v3.0** (was `SOCKET_PATCH_TELEMETRY_DISABLED`). |
100+
| `SOCKET_FORCE` | `apply --force` / `-f` | `false` | Local to `apply`. |
101+
| `SOCKET_BATCH_SIZE` | `scan --batch-size` | `100` | Local to `scan`. |
102+
| `SOCKET_SAVE_ONLY` | `get --save-only` | `false` | Local to `get`. |
103+
| `SOCKET_ONE_OFF` | `get --one-off` / `rollback --one-off` | `false` | Local to `get`/`rollback`. |
104+
| `SOCKET_SKIP_ROLLBACK` | `remove --skip-rollback` | `false` | Local to `remove`. |
105+
| `SOCKET_DOWNLOAD_ONLY` | `repair --download-only` | `false` | Local to `repair`. |
106+
107+
### Deprecated env vars
108+
109+
| Legacy | Renamed to | Status |
110+
|---|---|---|
111+
| `SOCKET_PATCH_PROXY_URL` | `SOCKET_PROXY_URL` | Honored with warning; remove in next major. |
112+
| `SOCKET_PATCH_DEBUG` | `SOCKET_DEBUG` | Honored with warning; remove in next major. |
113+
| `SOCKET_PATCH_TELEMETRY_DISABLED` | `SOCKET_TELEMETRY_DISABLED` | Honored with warning; remove in next major. |
145114

146115
## CSV value parsing
147116

0 commit comments

Comments
 (0)