diff --git a/crates/socket-patch-cli/CLI_CONTRACT.md b/crates/socket-patch-cli/CLI_CONTRACT.md index 6c34fac55..f576c1676 100644 --- a/crates/socket-patch-cli/CLI_CONTRACT.md +++ b/crates/socket-patch-cli/CLI_CONTRACT.md @@ -20,11 +20,11 @@ For task-oriented guidance, start with [usage](../../docs/usage.md), | `vex` | — | Emit an OpenVEX 0.2.0 attestation derived from the local manifest, the vendor ledger, and the hosted / vendored patch references the project's lockfiles wire (no manifest required; hosted records come from the API) | | `vendor` | — | Eject patched dependencies into committable `.socket/vendor/` and rewire lockfiles | | `list` | — | Print patches in the local manifest, plus the vendor ledger's records (v5.0) and the hosted pins the lockfiles wire (labeled; see the action matrix; an empty project exits 0) | -| `get` | `download` | Fetch a selected patch in hosted mode by default; `--mode agent` selects in-place application, also the default with `--save-only` or global targeting. Requires positional `identifier`. | +| `get` | — | Fetch a selected patch in hosted mode by default; `--mode agent` selects in-place application, also the default with `--save-only` or global targeting. Requires positional `identifier`. | | `apply` | — | Agent mode: apply patches from the local manifest | | `rollback` | — | **Full-state rollback (v5.0, MAJOR)**: restore original files AND unwind vendored lockfile wiring / restore hosted pins to their upstream registry entries, remove the rolled-back entries from the manifest, and GC their blobs/archives; takes optional variadic positional `targets` (PURL \| UUID \| path glob). See [Rollback command contract](#rollback-command-contract-v50) | | `remove` | — | Restore and remove one patch across hosted, vendored, and agent state; requires positional `identifier`. | -| `repair` | `gc` | Download missing agent blobs, redownload missing/corrupt vendored artifacts (never re-synthesizing a lost ledger), and clean up unused ones (refuses with `lock_held` when a live process holds the lock; see "Lock lifecycle" below) | +| `repair` | — | Download missing agent blobs, redownload missing/corrupt vendored artifacts (never re-synthesizing a lost ledger), and clean up unused ones (refuses with `lock_held` when a live process holds the lock; see "Lock lifecycle" below) | Rows are in `--help` order (v5.0): the hosted/vendored workflow (`scan` → `vex` → `vendor`, with `list` to inspect), then the agent-mode (in-place patching) commands. @@ -32,7 +32,7 @@ Rows are in `--help` order (v5.0): the hosted/vendored workflow (`scan` → `vex **Removed in v4.0:** the `unlock` subcommand (a leftover lock from a crashed run never blocks acquisition — the OS releases a dead holder's advisory lock — so there is no stale-lock state to inspect or clear before a mutating command; `repair` briefly owned lock-file cleanup in v4.x, and since v5.0 every lock-taking command removes its own lock file on exit). -**Lock lifecycle (v5.0).** `<.socket>/apply.lock` never outlives the command that took it: acquisition creates `.socket/` when it is missing, the guard's drop unlinks the file WHILE the lock is still held (so a waiter can never lock an orphaned inode), releases it, and then removes `.socket/` itself if that left the directory empty — a run that had nothing to persist leaves no `.socket/` behind, and there is nothing to `.gitignore`. An interrupted run (Ctrl-C, SIGTERM, SIGHUP; Ctrl-C, Ctrl-Break or console close on Windows) removes the file on its way out and still dies by that signal; only an uncatchable kill (SIGKILL, power loss) can leave `apply.lock`, and the next lock-taking command reclaims it in place and removes it. socket-patch never keeps a persistent lock file in the project. The lock is taken by `apply`, `rollback`, `remove`, `repair`, `vendor`, agent-mode `get` and `scan --apply`/`--sync` (download → manifest write → nested apply is ONE lock window — the nested apply never re-acquires), and `scan`/`get` in vendored **and hosted** mode — hosted acquires it before its first wet write (the staged takeover reverts), never on `--dry-run` and never when the run would write nothing, so hosted previews and no-op runs create no `.socket/`. Dry runs of the other commands may still take the lock; it is residue-free either way. A live holder is `lock_held` (exit 1); a directory or special file squatting on `.socket/` or on the lock path is a lock I/O error — `lock_io` (exit 1, `failed to open lock file at : …`; a read-only project root surfaces the same code at the acquire, before any ledger or manifest write) — never `lock_held`. +**Lock lifecycle (v5.0).** `<.socket>/apply.lock` never outlives the command that took it: acquisition creates `.socket/` when it is missing, the guard's drop unlinks the file WHILE the lock is still held (so a waiter can never lock an orphaned inode), releases it, and then removes `.socket/` itself if that left the directory empty — a run that had nothing to persist leaves no `.socket/` behind, and there is nothing to `.gitignore`. An interrupted run (Ctrl-C, SIGTERM, SIGHUP; Ctrl-C, Ctrl-Break or console close on Windows) removes the file on its way out and still dies by that signal; only an uncatchable kill (SIGKILL, power loss) can leave `apply.lock`, and the next lock-taking command reclaims it in place and removes it. socket-patch never keeps a persistent lock file in the project. The lock is taken by `apply`, `rollback`, `remove`, `repair`, `vendor`, agent-mode `get` and `scan --mode agent`/`--sync` (download → manifest write → nested apply is ONE lock window — the nested apply never re-acquires), and `scan`/`get` in vendored **and hosted** mode — hosted acquires it before its first wet write (the staged takeover reverts), never on `--dry-run` and never when the run would write nothing, so hosted previews and no-op runs create no `.socket/`. Dry runs of the other commands may still take the lock; it is residue-free either way. A live holder is `lock_held` (exit 1); a directory or special file squatting on `.socket/` or on the lock path is a lock I/O error — `lock_io` (exit 1, `failed to open lock file at : …`; a read-only project root surfaces the same code at the acquire, before any ledger or manifest write) — never `lock_held`. **Bare-UUID fallback.** `socket-patch ` is rewritten to `socket-patch get `. 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). @@ -74,9 +74,9 @@ Every subcommand accepts the same set of "global" flags via a single shared `Glo | `--no-npm-allow-remote-config` | — | `SOCKET_NO_NPM_ALLOW_REMOTE_CONFIG` | `false` | bool | Opt out of hosted mode's automatic `allow-remote=all` write to the project `.npmrc` (see the npm allow-remote note under the scan arguments). Read by `scan --mode hosted` and `get --mode hosted`; other subcommands accept it silently | | `--no-vlt-install-cleanup` | — | `SOCKET_NO_VLT_INSTALL_CLEANUP` | `false` | bool | Opt out of hosted mode's warm-tree heal for vlt: stale installed copies (`node_modules/.vlt-lock.json` and the stale `node_modules/.vlt/` entries) are left in place after `vlt-lock.json` is repointed (`scan`/`get --mode hosted`) or restored (`rollback`/`remove`), and the `redirect_vlt_reinstall_required` advisory tells you to run `vlt ci` instead. Stale copies of optional dependencies are always left in place (see `redirect_vlt_reinstall_required`). Other subcommands accept it silently | -`--offline` means the same thing on every command (v3.0): never contact the network, fail loudly when a required local source is missing. On `repair`, `--offline` and `--download-only` are mutually exclusive (exit 2). `scan` and `get` need remote data for their core function (patch discovery / patch fetch), so `--offline` refuses them up front — exit 1 with an error naming the offline gate (JSON: `status: "error"`), before any crawl, client build, or network contact. This covers `scan --vendor` too: offline vendored staging is `vendor --offline`'s job. +`--offline` means the same thing on every command (v3.0): never contact the network, fail loudly when a required local source is missing. On `repair`, `--offline` and `--download-only` are mutually exclusive (exit 2). `scan` and `get` need remote data for their core function (patch discovery / patch fetch), so `--offline` refuses them up front — exit 1 with an error naming the offline gate (JSON: `status: "error"`), before any crawl, client build, or network contact. This covers `scan --mode vendored` too: offline vendored staging is `vendor --offline`'s job. -The `--strict` mismatch policy applies to the in-place apply paths (apply/get/scan --apply/hook/go redirect). DEFAULT (v3.4): a file whose on-disk content matches neither the patch's beforeHash nor its afterHash is overwritten with the FULL verified patched content (the diff strategy self-disables on a wrong base; archive/blob writes are hash-gated to exactly afterHash; the missing blob is downloaded on demand) and surfaced as a `content_mismatch_overwritten` stderr warning + Skipped event. A file the patch adds (empty beforeHash) that already exists with other content is the same case. `--strict` turns that case into a hard error. Rollback of an added file deletes it; the content it replaced is not kept. `--force` overrides `--strict` and additionally skips missing files. Vendor staging is unaffected (it always auto-overwrites into its private stage). +The `--strict` mismatch policy applies to the in-place apply paths (apply/get/scan --mode agent/hook/go redirect). DEFAULT (v3.4): a file whose on-disk content matches neither the patch's beforeHash nor its afterHash is overwritten with the FULL verified patched content (the diff strategy self-disables on a wrong base; archive/blob writes are hash-gated to exactly afterHash; the missing blob is downloaded on demand) and surfaced as a `content_mismatch_overwritten` stderr warning + Skipped event. A file the patch adds (empty beforeHash) that already exists with other content is the same case. `--strict` turns that case into a hard error. Rollback of an added file deletes it; the content it replaced is not kept. `--force` overrides `--strict` and additionally skips missing files. Vendor staging is unaffected (it always auto-overwrites into its private stage). ## Per-subcommand arguments @@ -93,16 +93,16 @@ Beyond the globals above, each subcommand defines a small set of local arguments | `apply`, `scan`, `vendor` | `--vex` | `SOCKET_VEX` | Generate an OpenVEX 0.2.0 document at this path on a successful run; see "embedded VEX" below | | `apply`, `scan`, `vendor` | `--vex-product`, `--vex-no-verify`, `--vex-doc-id`, `--vex-compact` | `SOCKET_VEX_PRODUCT`, `SOCKET_VEX_NO_VERIFY`, `SOCKET_VEX_DOC_ID`, `SOCKET_VEX_COMPACT` | Passthrough to the embedded VEX builder; mirror the standalone `vex` knobs. Inert unless `--vex` is set | | `scan` | positional `[PATHS]...` | — | (v5.0) Meaning depends on the mode. **Hosted / vendored** (bare `scan` included): each PATH, or directory glob (`apps/*`), is a project directory scanned on its own as if it were `--cwd`. **Agent** (and a mode-less `--prune`/`--global` report): path globs scoping DISCOVERY to packages installed under matching paths (`packages/foo`, `apps/**`). See "Path-scoped scans" below | -| `scan` | `--mode ` | — | The documented selector for the three patch-application modes (v5.0 default: `hosted`, except that a `--prune` or `--global`/`--global-prefix` scan with no mode is report-only). v5.0 removes the hidden value aliases `host`/`redirect`/`vendor` (now an invalid-value usage error). `vendored` and `agent` each keep one hidden, deprecated boolean spelling: `vendored` == `--vendor`, `agent` == `--apply` (`--sync` counts as an agent spelling); hosted has none (v5.0 removes `--redirect`). Combining `--mode` with a boolean of a DIFFERENT mode is a usage error (exit 2, enforced in `resolve_mode_flags` — clap's `conflicts_with` is value-independent); the same mode spelled both ways is accepted. `--prune` is an orthogonal GC knob and never conflicts — but hosted mode runs no GC, so `--mode hosted --prune` emits an explicit `redirect_prune_ignored` warning (JSON `redirect.warnings[]` + stderr) instead of silently dropping the flag | -| `scan` | `--apply` / `--prune` / `--sync` | — | `--apply` == `--mode agent` (deprecated spelling); `--prune` = GC after the scan (ignored with a `redirect_prune_ignored` warning in hosted mode); `--sync` = `--mode agent --prune` | +| `scan` | `--mode ` | — | The documented selector for the three patch-application modes (v5.0 default: `hosted`, except that a `--prune` or `--global`/`--global-prefix` scan with no mode is report-only). v5.0 removes the hidden value aliases `host`/`redirect`/`vendor` (now an invalid-value usage error), and the hidden boolean spellings `--redirect`, `--vendor` and `--apply` (now unknown-argument usage errors). `--sync` selects agent mode, so `--sync` with any other `--mode` is a usage error (exit 2, enforced in `resolve_mode_flags` — clap's `conflicts_with` is value-independent); `--mode agent --sync` is accepted. `--prune` is an orthogonal GC knob and never conflicts — but hosted mode runs no GC, so `--mode hosted --prune` emits an explicit `redirect_prune_ignored` warning (JSON `redirect.warnings[]` + stderr) instead of silently dropping the flag | +| `scan` | `--prune` / `--sync` | — | `--prune` = GC after the scan (ignored with a `redirect_prune_ignored` warning in hosted mode); `--sync` = `--mode agent --prune` | | `scan` | `--package ` (repeatable or comma-separated) | `SOCKET_SCAN_PACKAGES` | (v5.0) Only scan these packages: a name (`lodash`, `@scope/pkg`, `requests`, `group:artifact`; matched against the full name or its last segment, case-insensitively; PyPI names compare by their PEP 503 canonical form, so `typing_extensions` matches `pkg:pypi/typing-extensions`) or a purl with or without a version (`pkg:npm/lodash` matches every version, `pkg:pypi/requests@2.31.0` only that one). Qualifiers are ignored. Filters the crawl like `--ecosystems`, after the prune universe is captured, so `--prune` still judges the full crawl | -| `scan` | `--vendor` | — | Vendor every patched dependency instead of applying in place (`--vendor` == `--mode vendored`; conflicts with `--apply`/`--sync`, combines with `--prune`). Vendored mode is manifest-free (v5.0): the vendor ledger embeds the patch records and `.socket/manifest.json` is never written. The former opt-in for exactly that, `--detached`, is removed in v5.0 (unknown-flag usage error) | +| `scan` | `--mode vendored` | — | Vendor every patched dependency instead of applying in place (conflicts with `--sync`, combines with `--prune`). Vendored mode is manifest-free (v5.0): the vendor ledger embeds the patch records and `.socket/manifest.json` is never written. The former opt-in for exactly that, `--detached`, is removed in v5.0 (unknown-flag usage error) | | `scan` | `--batch-size` | `SOCKET_BATCH_SIZE` | API batch chunk size. Unset (v5.0): `500` on the authenticated API (the server's per-request maximum), `100` on the public proxy; a given value applies on either endpoint (`0` is floored to `1`). A chunk whose request body would exceed 256 KiB (the public proxy's body cap) is split into consecutive smaller chunks, deterministically (greedy, in crawl order). A mid-run downgrade to the proxy keeps the chunks already formed | | `scan` | `--max-new-patches ` | `SOCKET_MAX_NEW_PATCHES` | (v5.0) Per-run cap on NEW patches (packages with no recorded patch in the project), most severe first; the rest are deferred to the next scan. `0` admits upgrades only, `none` (case-insensitive) is unlimited, absent is unlimited unless socket.yml sets `patches.maxNewPatches`. Precedence: flag > env > socket.yml > unlimited (`--no-socket-yml` drops the socket.yml layer). The env value is read at run time (the `rollout` block reports `flag` vs `env`): empty is unset, malformed is a usage error (exit 2, before any network access). Upgrades and already-applied patches are never capped. See "Per-run limit on new patches" below | | `scan` | `--no-socket-yml` | `SOCKET_NO_SOCKET_YML` | (v5.0) Ignore the repository's socket.yml patch policy (its `patches` block and `projectIgnorePaths`) for this run; the built-in test/fixture ignores still apply. The `policy` block reports `source: "bypassed"`. See "socket.yml patch policy". | | `scan` | `--min-severity ` | `SOCKET_MIN_SEVERITY` | (v5.0) Severity floor for the patch a package may receive (worst advisory severity; unknown severity is skipped whenever a floor is set). Beats `patches.minSeverity`; the flag beats the env; `none` lifts the floor. A malformed value is exit 2. | | `get`, `scan` | `--all-releases` | `SOCKET_ALL_RELEASES` | Download patches for every release/distribution variant of a matched package — PyPI wheel/sdist (`artifact_id`), RubyGems (`platform`), Maven (`classifier`) — not just the one(s) matching the locally-installed distribution. On `scan` this makes the stored manifest portable across environments (e.g. cross-platform CI caches). On `get` (v3.6) it ALSO disables the coarse installed-**version** narrowing of CVE/GHSA fan-outs (see "get --mode and installed narrowing"): every found version's patch is fetched, installed or not | -| `get` | positional `identifier`; `--id` / `--cve` / `--ghsa` / `--package` (`-p`); `--save-only` (alias `--no-apply`); `--mode ` | `SOCKET_SAVE_ONLY` | Patch lookup + consumption mode (v3.6). `--mode` reuses scan's value enum (same hidden value aliases `host`/`redirect`/`vendor`; deliberately no env binding, matching scan). Default (v5.0): `hosted`, like scan; `agent` (save + apply in place) when `--save-only` or `--global`/`--global-prefix` is given. An explicit `--mode hosted\|vendored` with `--global`/`--global-prefix` is a usage error (exit 2, scan's wording: global installs have no project lockfile). An explicit `--save-only` conflicts with `--mode hosted\|vendored` — rejected with **exit 1** via get's established self-enforced-conflict style (unlike scan's exit-2 mode conflicts; see the exit-code table) | +| `get` | positional `identifier`; `--id` / `--cve` / `--ghsa` / `--package` (`-p`); `--save-only`; `--mode ` | `SOCKET_SAVE_ONLY` | Patch lookup + consumption mode (v3.6). `--mode` reuses scan's value enum (same hidden value aliases `host`/`redirect`/`vendor`; deliberately no env binding, matching scan). Default (v5.0): `hosted`, like scan; `agent` (save + apply in place) when `--save-only` or `--global`/`--global-prefix` is given. An explicit `--mode hosted\|vendored` with `--global`/`--global-prefix` is a usage error (exit 2, scan's wording: global installs have no project lockfile). An explicit `--save-only` conflicts with `--mode hosted\|vendored` — rejected with **exit 1** via get's established self-enforced-conflict style (unlike scan's exit-2 mode conflicts; see the exit-code table) | | `remove` | positional `identifier`; `--skip-rollback`; `--preserve-state` (v5.0) | `SOCKET_SKIP_ROLLBACK`, `SOCKET_PRESERVE_STATE` | Manifest entry removal. The identifier matches like a `rollback` target (a PyPI name compares by its PEP 503 canonical form). `--preserve-state` is the single-patch twin of `rollback --preserve-state`: restore the tree and unwind the identifier's vendored/hosted wiring, but keep the manifest entry, the vendored artifact + ledger entry, and skip all GC. Combining it with `--skip-rollback` is a self-enforced usage error (exit 2): one flag keeps the tree and drops the state, the other restores the tree and keeps the state — together they select the do-nothing quadrant ("the combination would be a no-op: nothing would change"). The conflict fires whether either flag is spelled on the command line or sourced from its env var | | `rollback` | optional variadic positional `targets` (PURL \| UUID \| path glob); `--preserve-state` (v5.0) | `SOCKET_PRESERVE_STATE` | Rollback scope. Multiple targets union. A token becomes a path glob ONLY when it is path-SHAPED — contains a separator (`/` or `\`) or a glob metacharacter (`*?[`), or starts with `./`, or is absolute; a `pkg:` prefix is a PURL and every other bare word keeps identifier (PURL/UUID) semantics, so a mistyped identifier or truncated UUID stays a safe exit-1 "No patch found matching identifier: X" (with a hint suggesting `./X` or `X/**` for directory targeting) instead of silently becoming a path scope. An unparseable glob is a usage error (exit 2) | | `vex` | `--output` / `-O`, `--product`, `--no-verify`, `--doc-id`, `--compact` | `SOCKET_VEX_OUTPUT`, `SOCKET_VEX_PRODUCT`, `SOCKET_VEX_NO_VERIFY`, `SOCKET_VEX_DOC_ID`, `SOCKET_VEX_COMPACT` | OpenVEX 0.2.0 document generation; see "vex output channels" below | @@ -124,15 +124,15 @@ For a **9.0 root lock**, the CLI ensures `pnpm-workspace.yaml` carries `trustLoc ### Scan modes (v5.0) -**Mode resolution (`resolve_mode_flags`, MAJOR in v5.0).** `--mode`, or one of its legacy boolean spellings (`--vendor`, `--apply`/`--sync`), picks the mode. With none of them, `scan` runs **hosted** mode — JSON and human alike; the result nests under the JSON `redirect` sub-object (see the hosted paragraph below). The one exception: a `--prune` or `--global`/`--global-prefix` scan with no mode has no project lockfile to rewire, so it is **report-only** — discovery, the table, the `updates` array and the `redirectState` block below, plus the `--prune` GC — and, in human mode, ends with the hint `To apply these patches in place, run:` / ` socket-patch scan --mode agent [PATHS]` / ` socket-patch get `. A global scan's hint carries the run's scope, so it can be run verbatim: `-g` (`scan --mode agent -g` / `get -g <…>`), or `--global-prefix ` when a prefix was given (the directory shell-quoted when it needs it). An explicit `--mode hosted` or `--mode vendored` (or the hidden `--vendor`) with `--global`/`--global-prefix` is a usage error (exit 2: global installs have no project lockfile to redirect, or to wire vendored artifacts into); `get` enforces the same rule with the same wording. +**Mode resolution (`resolve_mode_flags`, MAJOR in v5.0).** `--mode`, or `--sync` (agent mode), picks the mode. With neither, `scan` runs **hosted** mode — JSON and human alike; the result nests under the JSON `redirect` sub-object (see the hosted paragraph below). The one exception: a `--prune` or `--global`/`--global-prefix` scan with no mode has no project lockfile to rewire, so it is **report-only** — discovery, the table, the `updates` array and the `redirectState` block below, plus the `--prune` GC — and, in human mode, ends with the hint `To apply these patches in place, run:` / ` socket-patch scan --mode agent [PATHS]` / ` socket-patch get `. A global scan's hint carries the run's scope, so it can be run verbatim: `-g` (`scan --mode agent -g` / `get -g <…>`), or `--global-prefix ` when a prefix was given (the directory shell-quoted when it needs it). An explicit `--mode hosted` or `--mode vendored` with `--global`/`--global-prefix` is a usage error (exit 2: global installs have no project lockfile to redirect, or to wire vendored artifacts into); `get` enforces the same rule with the same wording. **Global scope never touches the project's state (v5.0).** A `--global`/`--global-prefix` run that starts inside a project acts on the global installs only. The `--cwd` project's hosted pins and vendor ledger are not its target: `rollback` and `remove` run no hosted or vendored leg (they restore the global copies and drop their manifest records; `rollback` keeps the manifest records of purls the project vendors), a pre-v5 hosted ledger is never retired, and the project's vendor ledger does not own the global copies, so `apply` and `scan --mode agent` patch the global copy of a purl the project vendors (no `vendored` skip, no `vendored_ownership_retained` warning). The standalone `vendor` command acts only on the project, so every form of it (plain, `--revert`, `--check`) is a usage error under global scope: exit 2, human `Error: cannot be used with vendor[ --revert| --check]: global installs have no project lockfile to …`, JSON `{status: "error", error: {code: "global_scope_unsupported", message}}`, checked before the project is read or locked (#498). **scan never prompts, in any mode** (v5.0): no confirm, no free-tier patch menu (it always takes the top-ranked downloadable patch; see "Which patch gets selected"), and no `Non-interactive mode detected` note. `--yes` does not change a scan. `get` (agent mode only — hosted/vendored `get` never prompts either, v5.0), `rollback`, `remove` and `--update` keep their prompts. -**Hosted-state visibility (`redirectState`, additive/MINOR).** Every non-hosted-mode, non-vendored-mode `scan --json` SUCCESS envelope (report-only, `--mode agent`/`--apply`/`--sync`, and the zero-discovery envelope) carries an additive top-level `redirectState` object whenever the project's lockfiles pin ≥ 1 hosted patch: `{ mode, records: [{purl, uuid}], wiringLive: [purl] }`. It is a descriptive STATE block, not a warning. `mode` is the constant `"hosted"`. **v5.0 (MAJOR shape change)**: hosted mode keeps no ledger, so `records` lists the hosted pins lockfile discovery finds (one per `(purl, uuid)`, the same discovery `vex` uses: a hosted URL counts only on `https://patch.socket.dev` or the `--patch-server-url` origin), and the v4 `ledger` and `records[].ledgerKey` keys are gone. Each record's `purl` is CANONICALIZED (qualifiers stripped, percent-decoded — e.g. `pkg:npm/@scope/pkg@1.0.0`, `pkg:gem/nokogiri@1.13.3`) to the same spelling `wiringLive` carries, so the join is a plain string compare. `wiringLive` is the subset of those pins among this run's *counted* purls (post-`--ecosystems`-filter) — computed once per run, the same set that feeds `hosted_wiring_retained`. A record missing from `wiringLive` is still wired; it just was not crawled/queried this run (an `--ecosystems` filter, a zero discovery). The key is omitted when no lockfile pins a hosted patch (and under `--global`), and error envelopes (the `--offline` refusal, all-batches-failed) are deliberately minimal and never carry it. A pre-v5 `.socket/vendor/redirect-state.json` is not read. Hosted-mode runs carry the `redirect` sub-object instead (the run's own result), and vendored-mode runs carry the takeover warnings (their takeovers may restore pins mid-run) — neither duplicates a pre-run snapshot that could go stale. +**Hosted-state visibility (`redirectState`, additive/MINOR).** Every non-hosted-mode, non-vendored-mode `scan --json` SUCCESS envelope (report-only, `--mode agent`/`--sync`, and the zero-discovery envelope) carries an additive top-level `redirectState` object whenever the project's lockfiles pin ≥ 1 hosted patch: `{ mode, records: [{purl, uuid}], wiringLive: [purl] }`. It is a descriptive STATE block, not a warning. `mode` is the constant `"hosted"`. **v5.0 (MAJOR shape change)**: hosted mode keeps no ledger, so `records` lists the hosted pins lockfile discovery finds (one per `(purl, uuid)`, the same discovery `vex` uses: a hosted URL counts only on `https://patch.socket.dev` or the `--patch-server-url` origin), and the v4 `ledger` and `records[].ledgerKey` keys are gone. Each record's `purl` is CANONICALIZED (qualifiers stripped, percent-decoded — e.g. `pkg:npm/@scope/pkg@1.0.0`, `pkg:gem/nokogiri@1.13.3`) to the same spelling `wiringLive` carries, so the join is a plain string compare. `wiringLive` is the subset of those pins among this run's *counted* purls (post-`--ecosystems`-filter) — computed once per run, the same set that feeds `hosted_wiring_retained`. A record missing from `wiringLive` is still wired; it just was not crawled/queried this run (an `--ecosystems` filter, a zero discovery). The key is omitted when no lockfile pins a hosted patch (and under `--global`), and error envelopes (the `--offline` refusal, all-batches-failed) are deliberately minimal and never carry it. A pre-v5 `.socket/vendor/redirect-state.json` is not read. Hosted-mode runs carry the `redirect` sub-object instead (the run's own result), and vendored-mode runs carry the takeover warnings (their takeovers may restore pins mid-run) — neither duplicates a pre-run snapshot that could go stale. -**Agent-flow run-level warnings (additive).** An agent-mode apply (`--mode agent` / `--apply` / `--sync`, `--json`) may add a top-level `warnings[]` array of `{code, detail}` entries to the scan envelope (absent when none fired; each is also mirrored to stderr unless `--silent`). They surface cross-mode state the apply cannot change — never a status or exit-code change (hosted refusals set the precedent: exit 0 + warning). Codes (stable; new codes are additive/MINOR): `vendored_ownership_retained` — vendor-owned package(s) were skipped before download (the per-patch `skipped`/`vendored` records in `apply.patches[]` are unchanged); the detail names the purls and the migration path (`remove `, or `vendor --revert` which unwinds every vendored package, then re-run). `hosted_wiring_retained` — the lockfiles still pin scanned package(s) to a hosted patch (the agent run does not unwind hosted wiring — as of v5.0 that is `socket-patch rollback`'s job, which restores the upstream registry entries, or `remove ` per package); the detail names the purls and the options (stay `--mode hosted`, migrate via `scan --mode vendored`, or `socket-patch rollback`). The warning keys on the hosted pins lockfile discovery finds at scan time, so a flow that restored the upstream entries retires it. The human path prints the same `hosted_wiring_retained` text to stderr after an apply; the vendored counterpart is already covered by its per-package `[skip] … (vendored …)` lines. `ownership_not_restored` (v5.0; `apply` and `rollback` `warnings[]` alike) — a file WAS patched (or restored) but its ownership could not be put back to the original uid/gid (the mode is still restored last); the detail is `: : patched, but ownership could not be restored to uid N gid M: ` and the human line `Warning: ` (stderr, muted by `--silent`); never a status or exit change. +**Agent-flow run-level warnings (additive).** An agent-mode apply (`--mode agent` / `--sync`, `--json`) may add a top-level `warnings[]` array of `{code, detail}` entries to the scan envelope (absent when none fired; each is also mirrored to stderr unless `--silent`). They surface cross-mode state the apply cannot change — never a status or exit-code change (hosted refusals set the precedent: exit 0 + warning). Codes (stable; new codes are additive/MINOR): `vendored_ownership_retained` — vendor-owned package(s) were skipped before download (the per-patch `skipped`/`vendored` records in `apply.patches[]` are unchanged); the detail names the purls and the migration path (`remove `, or `vendor --revert` which unwinds every vendored package, then re-run). `hosted_wiring_retained` — the lockfiles still pin scanned package(s) to a hosted patch (the agent run does not unwind hosted wiring — as of v5.0 that is `socket-patch rollback`'s job, which restores the upstream registry entries, or `remove ` per package); the detail names the purls and the options (stay `--mode hosted`, migrate via `scan --mode vendored`, or `socket-patch rollback`). The warning keys on the hosted pins lockfile discovery finds at scan time, so a flow that restored the upstream entries retires it. The human path prints the same `hosted_wiring_retained` text to stderr after an apply; the vendored counterpart is already covered by its per-package `[skip] … (vendored …)` lines. `ownership_not_restored` (v5.0; `apply` and `rollback` `warnings[]` alike) — a file WAS patched (or restored) but its ownership could not be put back to the original uid/gid (the mode is still restored last); the detail is `: : patched, but ownership could not be restored to uid N gid M: ` and the human line `Warning: ` (stderr, muted by `--silent`); never a status or exit change. `scan --prune` opts into garbage collection. When set, `scan` removes manifest entries for packages no longer present in the crawl, then deletes orphan blob and diff-archive files, and every legacy package archive, from `.socket/`. Off by default (v3.0) so a temporary uninstall doesn't silently destroy manifest state. Only entries whose ecosystem this run actually crawled are eligible: a `pkg:/` with no crawler in this build (a newer CLI's ecosystem in the committed manifest) is exempt — the crawl never looked for them, so their absence is not evidence of removal (same fail-safe as the `--ecosystems` filter, which narrows the query but never the prune's installed set). The pass also reconciles vendored state (runs FIRST, under ONE apply-lock acquisition shared with the manifest prune — lock contention skips the whole pass without failing the scan; `--lock-timeout` is honored and a lock I/O error is reported rather than swallowed; the existence gate — a manifest file OR a vendor ledger file, both cheap stats; an emptied ledger is deleted on save, so its presence is its content proxy — runs BEFORE the lock, so a bare project never gets a `.socket/`; in the vendored scan arms the pass runs AFTER the vendor step): (a) ledger entries still tracked by a manifest record (manifest-mode entries written by standalone `vendor`) whose patch is gone from the manifest are reverted — `detached` entries (every `scan`/`get --mode vendored` entry, v5.0) have no manifest record to lose and are exempt from this leg; (b) EVERY ledger entry whose dependency is no longer in the lockfile graph is reverted and any manifest entry it still had dropped (v5.0: the check is about the lockfile, not the manifest, so embedded-record entries are no longer exempt; a missing or undeterminable lockfile keeps the entry, fail-safe); and (c) orphan `.socket/vendor//` dirs with no ledger entry are swept. The prune never deletes a zero-patch `.socket/manifest.json` (its `{"patches": {}}` + `setup` block stay). The JSON `gc` sub-object gains `revertedVendoredEntries` + `keptVendoredEntries` + `failedVendoredEntries` + `removedVendorOrphanDirs` (wet) / `revertableVendoredEntries` + `vendorOrphanDirs` (preview), plus two ADDITIVE wet-only keys: `skipped: {code, message}` — present exactly when the pass was skipped at the lock (`lock_held` | `lock_io`; every count is then zero) — and `warnings: [{code, detail}]` — `vendor_state_write_failed` / `manifest_write_failed` (entries were reverted but the ledger or manifest rewrite failed) and `cleanup_failed` (an orphan sweep failed mid-way). Human mode prints `GC: skipped (): .`, one `GC: .` line per warning, and `GC: failed to revert N vendored entries: …` (singular for one) for `failedVendoredEntries`. `keptVendoredEntries` lists drift-kept entries the revert deliberately preserved (`vendor_artifact_kept` — undo the drift and re-run `vendor --revert` to finish); the preview cannot see drift (backends return before the wiring replay on dry runs), so `revertableVendoredEntries` may over-promise what a wet run will actually reclaim. @@ -140,7 +140,7 @@ For a **9.0 root lock**, the CLI ensures `pnpm-workspace.yaml` carries `trustLoc **Throttling: bounded retry, then a reported failure.** Every patch-API JSON call (the batch query, the per-package patch lists, patch views and VEX record fetches, hosted package references) retries an HTTP `429` or `503` answer up to 3 times (`SOCKET_API_MAX_RETRIES=`, `0`-`10`; `0` = no retry). The wait honors `Retry-After` (delta-seconds or HTTP-date); a `Retry-After` over 30 s is not waited out — the answer is final at once — and one under the jittered first backoff step (`0`, a past date) waits that step instead. Without one it backs off 0.5 s / 1 s / 2 s (each step up to 8 s, with jitter in its upper half). All retries in one run share a 60 s wall-clock window that opens with the run's first retry: a retry whose wait would end after it closes is refused and the answer is final. Parallel requests wait in parallel, so each still gets its retries while the run adds at most about 60 s. Nothing else is retried (401/403 still drive the proxy fallback on the first answer; the public proxy's permanent `503 "Patch API is not configured"` is never retried on any path — the batch query still degrades to the per-package path at once, and a per-package lookup or patch view answering it is the same non-throttle failure it always was, so the legacy per-package path still skips that package), and a retried answer folds exactly where the first attempt's would have, so output is identical to an unthrottled run's. A request still throttled after that is a failure in the channel its siblings use: a failed batch is the human `Warning: API batch of failed: ` line and, under `--json`, a run-level `warnings[]` entry `{code: "api_batch_failed", detail: "API batch of failed: "}` (additive; `status` stays `success`, exit 0 — the other batches' packages are reported); a failed per-package patch-list query in the agent / hosted / vendored flows is the human `Warning: could not fetch details for : ` line and, under `--json`, `{code: "patch_details_failed", detail: "could not fetch details for : "}`. When every batch (or every patch-list query) fails, the existing all-failed error envelope and exit 1 apply. The error names the exhausted retry: `Rate limit exceeded (HTTP 429, gave up after 3 retries). Please try again later.` / `API request failed with status 503: (gave up after 3 retries)` (or `(Retry-After s exceeds the 30 s retry cap)` / `(the run's 60 s retry window has closed)`); with retries off it is the pre-retry text. On the token-less legacy per-package proxy path (a proxy without `POST /patch/batch`), a package still throttled (429 / over-capacity 503) after its retries fails its whole batch query, so every package in that batch goes unchecked and is reported through the batch-failure channel above (an unresolvable PURL, or a "not configured" 503, is still skipped individually). Pinned by `tests/scan_api_retry_e2e.rs` and the core crate's `tests/api_retry_e2e.rs`. -**Lockfile supplement (v3.4)**: `scan` discovery is no longer limited to installed trees. The project's lockfiles (`package-lock.json`/`npm-shrinkwrap.json`, `pnpm-lock.yaml` v9, `yarn.lock` classic + berry, `bun.lock`, `vlt-lock.json` (registry nodes, Socket-hosted pins included; vendored `file` nodes are left to the vendor ledger), `Cargo.lock`, `go.sum`, `composer.lock`, `Gemfile.lock`, `uv.lock`/`poetry.lock`/pinned `requirements.txt`) are inventoried and dependencies with NO installed copy join discovery — counts, the API lookup, the table (flagged ` [NOT INSTALLED]`, plus a stderr note), and the prune "scanned" set (a wiped node_modules no longer prunes lockfile-listed entries). JSON gains a top-level `lockfileOnlyPackages` count and an additive `notInstalled: true` on matching `packages[]` entries. `--apply` partitions lockfile-only patches out BEFORE download (calm `skipped`/`package_not_installed` records — never an error exit, never a manifest write); `--vendor` passes them through to the vendor engine's server download. Vendored-ledger entries likewise stay discoverable on a fresh clone (the committed artifact is the dependency). Global scans (`--global`) get no supplement. **Rush monorepos** (no root lockfile, `rush.json` present): the npm-lock inventory falls back to the Rush source-of-truth locks — `common/config/rush/pnpm-lock.yaml` plus every `common/config/subspaces/*/pnpm-lock.yaml` (`read_dir`-sorted, repo-relative paths preserved) — so a Rush repo's dependencies still join discovery. **Plug'n'Play layouts are an explicit refusal, not an empty inventory**: a `.pnp.*` loader means the npm packages are structurally unreachable in EVERY mode (under yarn PnP the installed-tree crawl is empty too — no `node_modules/`), so `scan` surfaces an additive top-level `warnings[]` array (`{code, detail}` objects, omitted when empty) carrying `yarn_pnp_unsupported` (same code as apply's refusal; remedy `yarn patch `) or `pnpm_pnp_unsupported` (pnpm's `node-linker=pnp` twin; pnpm remedies), plus a stderr `Warning: …` line on the human path. Exit code and `status` are deliberately unchanged (exit 0 / `success` — the same posture as hosted refusals, which exit 0 with `redirected: 0`); the warning is the machine-readable signal that nothing was checked. Pinned by `tests/e2e_safety_yarn_pnp.rs`. **A Bundler lock socket-patch cannot read is likewise a warning, not an empty inventory**: when the project holds gem files but bundler loads no `Gemfile.lock` / `gems.locked` socket-patch reads (`BUNDLE_GEMFILE` or bundler 4's `BUNDLE_LOCKFILE` naming another file, or a `Gemfile` + `gems.rb` twin, whose loaded pair depends on the bundler major that runs), `warnings[]` carries the additive `gem_lock_unsupported` (detail names the setting or the twin) and its lockfile-only gems are not discovered. Same exit-0 / `success` posture. +**Lockfile supplement (v3.4)**: `scan` discovery is no longer limited to installed trees. The project's lockfiles (`package-lock.json`/`npm-shrinkwrap.json`, `pnpm-lock.yaml` v9, `yarn.lock` classic + berry, `bun.lock`, `vlt-lock.json` (registry nodes, Socket-hosted pins included; vendored `file` nodes are left to the vendor ledger), `Cargo.lock`, `go.sum`, `composer.lock`, `Gemfile.lock`, `uv.lock`/`poetry.lock`/pinned `requirements.txt`) are inventoried and dependencies with NO installed copy join discovery — counts, the API lookup, the table (flagged ` [NOT INSTALLED]`, plus a stderr note), and the prune "scanned" set (a wiped node_modules no longer prunes lockfile-listed entries). JSON gains a top-level `lockfileOnlyPackages` count and an additive `notInstalled: true` on matching `packages[]` entries. Agent mode partitions lockfile-only patches out BEFORE download (calm `skipped`/`package_not_installed` records — never an error exit, never a manifest write); vendored mode passes them through to the vendor engine's server download. Vendored-ledger entries likewise stay discoverable on a fresh clone (the committed artifact is the dependency). Global scans (`--global`) get no supplement. **Rush monorepos** (no root lockfile, `rush.json` present): the npm-lock inventory falls back to the Rush source-of-truth locks — `common/config/rush/pnpm-lock.yaml` plus every `common/config/subspaces/*/pnpm-lock.yaml` (`read_dir`-sorted, repo-relative paths preserved) — so a Rush repo's dependencies still join discovery. **Plug'n'Play layouts are an explicit refusal, not an empty inventory**: a `.pnp.*` loader means the npm packages are structurally unreachable in EVERY mode (under yarn PnP the installed-tree crawl is empty too — no `node_modules/`), so `scan` surfaces an additive top-level `warnings[]` array (`{code, detail}` objects, omitted when empty) carrying `yarn_pnp_unsupported` (same code as apply's refusal; remedy `yarn patch `) or `pnpm_pnp_unsupported` (pnpm's `node-linker=pnp` twin; pnpm remedies), plus a stderr `Warning: …` line on the human path. Exit code and `status` are deliberately unchanged (exit 0 / `success` — the same posture as hosted refusals, which exit 0 with `redirected: 0`); the warning is the machine-readable signal that nothing was checked. Pinned by `tests/e2e_safety_yarn_pnp.rs`. **A Bundler lock socket-patch cannot read is likewise a warning, not an empty inventory**: when the project holds gem files but bundler loads no `Gemfile.lock` / `gems.locked` socket-patch reads (`BUNDLE_GEMFILE` or bundler 4's `BUNDLE_LOCKFILE` naming another file, or a `Gemfile` + `gems.rb` twin, whose loaded pair depends on the bundler major that runs), `warnings[]` carries the additive `gem_lock_unsupported` (detail names the setting or the twin) and its lockfile-only gems are not discovered. Same exit-0 / `success` posture. **Server artifact acquisition (v5.0)**: vendoring downloads the patched artifact for the selected UUID, including on a fresh checkout with no installed package. The CLI no longer downloads pristine packages, stages patch blobs, applies patches to vendor copies, or constructs archives. Backend lock and package-identity checks still run before acquisition; git and custom-registry Cargo sources are refused with `vendor_source_unsupported`. Healthy committed artifacts are reused offline. A changed UUID downloads a fresh server artifact; an unhealthy artifact at the recorded UUID uses the exact redownload procedure below, except that a directory copy whose ledger entry has no file inventory (vendored before v5.0) is rebuilt by its backend from a fresh verified download, and a missing or stale Bun workspace mirror over a healthy canonical tarball is rewritten from that tarball by the backend; those cases report the backend's own failure codes. @@ -155,9 +155,9 @@ For a **9.0 root lock**, the CLI ensures `pnpm-workspace.yaml` carries `trustLoc **Path-scoped scans (`scan [PATHS]...`, v5.0)**: what a PATH means depends on the mode. * **Hosted and vendored mode (bare `scan` included) — project directories** (`run_project_dirs`). Each PATH is a directory, or a glob (`*?[`) matching directories, relative to `--cwd`; the set is sorted and deduplicated, and each directory is scanned on its own exactly as if it were `--cwd` (its own lockfiles, ledgers and `.socket/`). With more than one directory, each run is headed `== ==` on stdout (unless `--silent`), and the exit code is the worst of the runs. Usage errors (exit 2, stderr only, before any scan): a PATH that is not a directory (`` `X` is not a directory``), a glob matching no directory (`` `X` matches no directory``), an invalid glob, and `--json` with more than one directory (`--json takes one project directory (N given); run one scan per directory`), so stdout stays one document. Likewise `--vex` with more than one directory (`--vex takes one project directory (N given); run one scan per directory`): the one output path would be overwritten by each run. -* **Agent mode (and a mode-less `--prune`/`--global` report) — installed-path globs** scoping DISCOVERY at the **purl level**: a package is in scope iff ANY of its crawled installed copies sits under a matching path, and a selected package is then handled with ALL its copies (scoping selects which packages are considered, never which copies). Glob semantics (shared with `rollback`'s path targets, `src/path_scope.rs`): Unix-shell globs with `require_literal_separator` — `*`/`?` never cross a `/`, `**` spans directories; a pattern matching any **ancestor** directory of the copy path also matches, so a bare `scan packages/foo` scopes the whole subtree without `/**`; relative patterns match against the copy path relativized to `--cwd`, absolute patterns against the absolute path (the ONLY way to reach paths outside the project tree, e.g. `--global` stores — a relative pattern never matches outside `--cwd`); leading `./` and trailing `/` are normalized away, matching is purely textual (no filesystem access or symlink resolution), case-sensitive except on Windows (whose filesystems are not); an unparseable or empty pattern is a usage error (exit 2). **The prune universe is never narrowed**: the path filter is applied strictly AFTER the `scanned_purls` capture (and after `--ecosystems`), so `scan PATHS --prune` prunes exactly what an unscoped `scan --prune` would — a scoped scan can never treat an out-of-scope package as uninstalled (the same fail-safe as the `--ecosystems` filter). Lockfile-only and vendor-ledger supplement records have no installed path and are EXCLUDED from a path-scoped scan, surfaced as one run-level `path_scope_excluded_supplements` warning carrying the count. A scope matching nothing is a normal empty scan — exit 0, zero packages, **no GC** (the zero-package early return fires before any GC). `PATHS` combine with `--apply`/`--sync`/`--prune`/`--global`. Every scan JSON shape (success, zero-package, and error alike) carries an always-present `paths` key echoing the patterns verbatim (empty array when unscoped; a hosted/vendored per-directory run is unscoped, so it is `[]`). One-sentence duality rule: **a target that selects nothing is an error on `rollback` (exit 1) and an empty scan on an agent-mode `scan` (exit 0)**. +* **Agent mode (and a mode-less `--prune`/`--global` report) — installed-path globs** scoping DISCOVERY at the **purl level**: a package is in scope iff ANY of its crawled installed copies sits under a matching path, and a selected package is then handled with ALL its copies (scoping selects which packages are considered, never which copies). Glob semantics (shared with `rollback`'s path targets, `src/path_scope.rs`): Unix-shell globs with `require_literal_separator` — `*`/`?` never cross a `/`, `**` spans directories; a pattern matching any **ancestor** directory of the copy path also matches, so a bare `scan packages/foo` scopes the whole subtree without `/**`; relative patterns match against the copy path relativized to `--cwd`, absolute patterns against the absolute path (the ONLY way to reach paths outside the project tree, e.g. `--global` stores — a relative pattern never matches outside `--cwd`); leading `./` and trailing `/` are normalized away, matching is purely textual (no filesystem access or symlink resolution), case-sensitive except on Windows (whose filesystems are not); an unparseable or empty pattern is a usage error (exit 2). **The prune universe is never narrowed**: the path filter is applied strictly AFTER the `scanned_purls` capture (and after `--ecosystems`), so `scan PATHS --prune` prunes exactly what an unscoped `scan --prune` would — a scoped scan can never treat an out-of-scope package as uninstalled (the same fail-safe as the `--ecosystems` filter). Lockfile-only and vendor-ledger supplement records have no installed path and are EXCLUDED from a path-scoped scan, surfaced as one run-level `path_scope_excluded_supplements` warning carrying the count. A scope matching nothing is a normal empty scan — exit 0, zero packages, **no GC** (the zero-package early return fires before any GC). `PATHS` combine with `--mode agent`/`--sync`/`--prune`/`--global`. Every scan JSON shape (success, zero-package, and error alike) carries an always-present `paths` key echoing the patterns verbatim (empty array when unscoped; a hosted/vendored per-directory run is unscoped, so it is `[]`). One-sentence duality rule: **a target that selects nothing is an error on `rollback` (exit 1) and an empty scan on an agent-mode `scan` (exit 0)**. -`scan --vendor` swaps the in-place apply for the vendor pipeline: discover → download the selected patch records **into memory** (no manifest write) → vendor every selected dependency via the same engine as the `vendor` command (under the same lock). Vendored mode is **manifest-free (v5.0)**: `.socket/manifest.json` is never written or read by a vendored run; each ledger entry carries `detached: true` plus an embedded copy of the patch record (`record`) as its verification source, and the run's footprint is `.socket/vendor/**` only. The vendor step's scope is what discovery selected — the former "whole manifest is vendored" re-vendor on an empty discovery is retired (`repair` verifies and redownloads committed vendored state; `scan --prune` reconciles ledger entries whose dependency left the lockfile). The vendor-ledger discovery supplement (the fresh-clone rule: a ledger entry with no installed copy stays discoverable because its committed artifact IS the dependency) holds only while the lockfile still resolves through that artifact: an entry the lockfile in-use probe (the one `--prune` reverts by) proves unwired, because the dependency was upgraded or removed, is NOT discovered and so is never re-vendored. A run without a non-hosted `--prune` reports it through the run-level `vendor_ledger_entry_unwired` warning; a `--prune` run reverts it in its GC and exits 0. That GC runs even when the crawl found no packages, as its vendored half alone (the manifest prune stays skipped there). A package the ledger holds at an older patch uuid is still **re-vendored automatically** when discovery selects the newer patch (its old uuid dir is removed — `vendor_stale_artifact_removed`); same-uuid re-runs reuse the embedded record, skip the patch-view fetch, and are `already_vendored` skips. **Legacy manifest-mode entries**: when a vendored run vendors a purl that also has a `.socket/manifest.json` record (a project vendored by a pre-5.0 binary, or by standalone `vendor` from an agent-mode manifest), that manifest record is dropped in the same run — the ledger becomes the owner (migration write); an emptied manifest is left as `{"patches": {}}`, never deleted. The migration is reported through the run-level `warnings[]` (stderr in human mode), never as a run error: `vendor_manifest_record_migrated` (`N manifest records moved to the vendor ledger (vendored mode is manifest-free): `) or `vendor_manifest_migration_failed` (the manifest or the ledger could not be read or rewritten; the legacy records were left in place) — so a corrupt `.socket/manifest.json` no longer fails a vendored run (standalone `vendor`, the one manifest-driven writer, still fails closed on it). With `--prune`, GC runs **after** the vendor step (the step never reads the manifest, and running the sweep last lets it reclaim what the run itself orphaned — a migrated legacy record's blobs, a superseded uuid dir). JSON output gains a `download` sub-object — the detached download envelope `{found, downloaded, skipped, failed, detached: true, patches: [{purl, uuid, action: "downloaded" | "skipped" | "failed", …}], warnings?}` (no `applied` field — nothing is applied in place; `detached: true` is pinned and always present; a `downloaded` record whose purl the ledger already holds at another uuid carries the additive `oldUuid` — the re-vendor the vendor step then performs — and its human `[fetch]` line reads ` (replacing )`) — and a `vendor` sub-object (a full vendor Envelope). Patch blobs are held in memory (see "Patch sources stay in memory" under the vendor contract). `--dry-run` previews per-patch `would_vendor` | `would_revendor` (+`oldUuid`) | `already_vendored` — plus, additive, `would_refuse` (+`errorCode`, `error`) for npm purls the wet run's Bun preflight (see the `get --mode vendored` bullet below) would refuse — without network downloads or disk writes; the preview never flips status or exit (the human path — `scan` and `get` alike, through one shared printer — prints `[would-refuse] (): ` lines behind the `--silent` gate). Interactive mode prompts "Download and vendor N patches?" (singular for one). +`scan --mode vendored` swaps the in-place apply for the vendor pipeline: discover → download the selected patch records **into memory** (no manifest write) → vendor every selected dependency via the same engine as the `vendor` command (under the same lock). Vendored mode is **manifest-free (v5.0)**: `.socket/manifest.json` is never written or read by a vendored run; each ledger entry carries `detached: true` plus an embedded copy of the patch record (`record`) as its verification source, and the run's footprint is `.socket/vendor/**` only. The vendor step's scope is what discovery selected — the former "whole manifest is vendored" re-vendor on an empty discovery is retired (`repair` verifies and redownloads committed vendored state; `scan --prune` reconciles ledger entries whose dependency left the lockfile). The vendor-ledger discovery supplement (the fresh-clone rule: a ledger entry with no installed copy stays discoverable because its committed artifact IS the dependency) holds only while the lockfile still resolves through that artifact: an entry the lockfile in-use probe (the one `--prune` reverts by) proves unwired, because the dependency was upgraded or removed, is NOT discovered and so is never re-vendored. A run without a non-hosted `--prune` reports it through the run-level `vendor_ledger_entry_unwired` warning; a `--prune` run reverts it in its GC and exits 0. That GC runs even when the crawl found no packages, as its vendored half alone (the manifest prune stays skipped there). A package the ledger holds at an older patch uuid is still **re-vendored automatically** when discovery selects the newer patch (its old uuid dir is removed — `vendor_stale_artifact_removed`); same-uuid re-runs reuse the embedded record, skip the patch-view fetch, and are `already_vendored` skips. **Legacy manifest-mode entries**: when a vendored run vendors a purl that also has a `.socket/manifest.json` record (a project vendored by a pre-5.0 binary, or by standalone `vendor` from an agent-mode manifest), that manifest record is dropped in the same run — the ledger becomes the owner (migration write); an emptied manifest is left as `{"patches": {}}`, never deleted. The migration is reported through the run-level `warnings[]` (stderr in human mode), never as a run error: `vendor_manifest_record_migrated` (`N manifest records moved to the vendor ledger (vendored mode is manifest-free): `) or `vendor_manifest_migration_failed` (the manifest or the ledger could not be read or rewritten; the legacy records were left in place) — so a corrupt `.socket/manifest.json` no longer fails a vendored run (standalone `vendor`, the one manifest-driven writer, still fails closed on it). With `--prune`, GC runs **after** the vendor step (the step never reads the manifest, and running the sweep last lets it reclaim what the run itself orphaned — a migrated legacy record's blobs, a superseded uuid dir). JSON output gains a `download` sub-object — the detached download envelope `{found, downloaded, skipped, failed, detached: true, patches: [{purl, uuid, action: "downloaded" | "skipped" | "failed", …}], warnings?}` (no `applied` field — nothing is applied in place; `detached: true` is pinned and always present; a `downloaded` record whose purl the ledger already holds at another uuid carries the additive `oldUuid` — the re-vendor the vendor step then performs — and its human `[fetch]` line reads ` (replacing )`) — and a `vendor` sub-object (a full vendor Envelope). Patch blobs are held in memory (see "Patch sources stay in memory" under the vendor contract). `--dry-run` previews per-patch `would_vendor` | `would_revendor` (+`oldUuid`) | `already_vendored` — plus, additive, `would_refuse` (+`errorCode`, `error`) for npm purls the wet run's Bun preflight (see the `get --mode vendored` bullet below) would refuse — without network downloads or disk writes; the preview never flips status or exit (the human path — `scan` and `get` alike, through one shared printer — prints `[would-refuse] (): ` lines behind the `--silent` gate). Interactive mode prompts "Download and vendor N patches?" (singular for one). **Vendored entries and the rest of the CLI.** Because nothing is in the manifest, vendored patches are invisible to `apply` (nothing to apply in place) but fully visible to `list` (listed from the ledger, labeled `Mode: vendored (recorded in .socket/vendor/state.json)` in human mode, exit 0 on a vendored-only project), `vex` (attested from the embedded records while a lockfile still wires the artifact — see "Manifest-less VEX"), `repair` (health-checked and rebuilt from the ledger), and `scan --prune` (lockfile-driven reconcile). They are exempt from standalone `vendor`'s manifest reconcile (`reconcile_dropped` never touches `detached` entries) and exit via `remove ` (which reverts them), `vendor --revert`, or `rollback`, whose vendored leg reverts every in-scope ledger entry (unscoped and identifier-scoped runs; path-scoped runs reach them only when an installed copy matches). @@ -183,15 +183,13 @@ The rewriter reads a fixed set of candidate files from the project root: the npm * **Hosted** (`get GHSA-… --mode hosted`): resolves the advisory, then hands the selected (purl, uuid) pairs to scan's hosted engine — reference grants, staged cross-mode takeover, lockfile rewrite (no ledger, v5.0), gem stale-install probe, warnings, confirmation rules (cargo via `confirmed_cargo_uuids`, golang via `confirmed_golang_uuids` only) all identical to `scan --mode hosted`, and (v5.0) under the same `apply.lock` acquisition — taken around the first wet write, never on `--dry-run` or when nothing would be written; a failed acquire folds as top-level `error: {code: "lock_held" | "lock_io", message}` (exit 1), and `--dry-run` under a held lock still exits 0. **No manifest write, no blobs, no ledger** — the lockfile edits are the persistence. JSON: get's legacy envelope gains the same nested `redirect` sub-object as scan's (`{mode:"hosted", redirected, rewrittenFiles, skipped, warnings, dryRun}`); the top-level shape is `{status, found, patches:[], warnings?}` — `downloaded`/`applied` are absent (nothing is downloaded into `.socket/`). Exit codes follow scan's hosted semantics: skipped grants and rewriter warnings never flip the exit; infra errors (reference fetch, file writes) exit 1. Human prompt: `Redirect N packages to the hosted patch server?` (singular for one; `--yes`/`--json`/non-TTY auto-accept as usual). This confirm is get's alone: `scan` never prompts. * **Vendored** (`get GHSA-… --mode vendored`): the download phase is scan's vendored posture — **manifest-free (v5.0)**: the selected records are fetched into memory (`download_patch_records`; no blob staging; nothing under `.socket/` is written; the nested apply never runs), then scan's vendor step runs under the apply lock over exactly the selected records, like `scan --mode vendored` (no whole-manifest scope and no `[note]` about other records — that blast radius is retired with the manifest; a legacy manifest record for a vendored purl is migrated out of `.socket/manifest.json` the same way scan does it). JSON: get's envelope takes the detached download envelope's shape — `{status, found, downloaded, skipped, failed, detached: true, patches: [{purl, uuid, action: "downloaded" | "skipped" | "failed", …}], warnings?}` (`applied` is absent; `detached: true` is pinned; a `downloaded` record for a purl the vendor ledger holds at another uuid carries the additive `oldUuid`, derived from the ledger — the human `[fetch]` line reads ` (replacing )`) — and gains the nested `vendor` Envelope exactly like scan's `result["vendor"]`; a vendor-step error folds the partial envelope + `{status:"error", error:{code,message}}` in (a pre-failure takeover reconcile may have already mutated the ledger — its events must reach the consumer). Exit: download failures or vendor `has_errors` → `partial_failure`/1. Human prompt: `Download and vendor N patches?`; `--dry-run` prints `[dry-run] Would download and vendor N patches. No changes made.` on both identifier paths (uuid and search). Telemetry mirrors scan's vendored arms (`track_outcomes_for_vendor` / `track_patch_vendor_failed`). **Bun vendored preflight (additive)** — shared by `get --mode vendored` on both its paths and `scan --mode vendored`: before ANY patch download, and only when the selection holds a `pkg:npm/` purl, the download phase reads `bun.lock`/`bun.lockb` once (`preflight_vendor`) and, when the vendor backend would refuse the project — a malformed, unreadable or unsupported `bun.lockb` → `vendor_bun_lockb_invalid`; an unreadable `bun.lock` → `vendor_lockfile_missing`; a `lockfileVersion` other than 0/1/2 or a non-canonical `packages` grammar → `vendor_lockfile_version_unsupported`; `workspace:` packages in a lock below version 2 → `vendor_bun_workspace_unsupported` — every `pkg:npm/` result becomes `{action:"failed", errorCode:, error:}` with NO fetch (the patch view is never requested) and no patch record; other ecosystems' results are untouched. **Search path** (`get --mode vendored`) and `scan --mode vendored`: the records ride `patches[]` / `download.patches[]` with `downloaded: 0`, the download phase writes nothing under `.socket/` (v5.0 — a pre-existing `.socket/manifest.json`, including a record seeded for another purl, is left byte-untouched), the vendor step still runs over the remaining records (no event for the refused purl), exit `partial_failure`/1. **uuid path** (`get --mode vendored`): the uuid lookup is the only fetch; the run exits 1 BEFORE the vendor step with exactly `{status:"error", found:1, downloaded:0, skipped:0, failed:1, error:{code, message}, patches:[{purl, uuid, action:"failed", errorCode, error}]}` (the `error` OBJECT is the vendored-mode error shape of the vendor-step fold-in above) and writes nothing — no `.socket/` on a fresh project; human mode prints `Error (): ` on stderr. **Already-vendored exemption**: a purl is exempt from the workspace refusal only when every instance of its `name@version` in `bun.lock` is already a `.socket/vendor/npm/…` local tuple (any uuid; the digest-less 2-tuple counts) — the engine's own criterion — so in-sync re-runs, `repair`, and a superseding patch uuid on a project vendored before it grew a workspace member all flow to the engine (re-pinning an already-local tuple adds no workspace-relative exposure); a wiped ledger alone is not a refusal (the engine path decides). UUID equality in the ledger alone never exempts a purl: `rollback --preserve-state` retains its record after unwiring. Dry-run refusal takes priority over `already_vendored`. **Unreadable vendor ledger**: a `.socket/vendor/state.json` the preflight cannot read or parse is itself the refusal — `vendor_state_unreadable` with the io/parse detail, fail-closed (nothing is exempt) — on the uuid path, the search / `scan` path and the `--dry-run` preview alike; never a Bun lock code. **`--silent`** is "errors only" and never mutes the refusal: the code-tagged `[error] (): ` (per-patch paths) / `Error (): …` (uuid path) line stays on stderr with an empty stdout. **`--dry-run`** previews the refusal as the additive `would_refuse` action (see `--dry-run` below). Agent-mode `get --save-only` is NOT preflighted (record-only intent has no consumption precondition). Pinned by `tests/vendor/in_process_vendor_bun.rs` (exact uuid-path envelope, seeded-manifest survival, `--silent`, `--dry-run`) and `tests/scan_vendor_e2e.rs`. -**Lock-text refusals before the download (v5.0)** — shared by `get --mode vendored` on both its paths and `scan --mode vendored`, after the Bun preflight above and the ledger's `already vendored` skip: a `pkg:npm/` result in a **pnpm, yarn classic or yarn berry** project, or a `pkg:cargo/` result, that its vendor backend refuses on the project's lock and manifest text alone is refused BEFORE its patch view is fetched — the pnpm / classic / berry gates the backend runs before it reads the package (coordinates, the lock and manifest reads and their line-ending / version / `cacheKey` / `.yarnrc.yml` gates, override and `resolutions` conflicts, the lock entry present and rewritable) and cargo's `locked_version_mismatch` (only when it is the crate's FIRST refusal; an in-tree `cargo vendor` copy still refuses in the loop as `already_vendored_in_tree`). **Scope:** only a package the vendor loop would hand to its backend is refused early — one installed on disk (the loop's own qualified-aware resolver plus the npm identity lookup), or one the lockfile inventory resolves to a verifiable registry source (a lock entry with an integrity, or the ledger-recovered pre-vendor resolution — exactly the entry the pristine fetch would use). A package absent from the lock and not installed never reached its backend and is untouched: its view is fetched, it downloads, and the vendor loop skips it `skipped` / `package_not_installed` as in v4.x (so cargo's `locked_version_mismatch` is refused early only for a crate installed at the unlocked version). The result becomes `{action:"failed", errorCode:, error:}` in `download.patches[]` / `patches[]` with the backend's exact code and detail, no view and no pristine fetch, no patch record, and therefore no vendor event: compared with v4.x, `download.downloaded` drops and `download.failed` rises by the number of such packages, `vendor.summary.failed` and `vendor.events` lose their `failed` events, and a lockfile-only package among them loses its `vendor_fetched_missing` event (it is never fetched). Exit code and top-level `status` are unchanged (`partial_failure`/1); the nested `vendor.status` becomes `success` when those refusals were the vendor step's only failures (observed on the depscan fixture: 3 refusals, `partialFailure` → `success`), and when every selected package is refused this way the human `scan --vendor` arm prints `Nothing was vendored: N patches failed (see above).`. **Precedence:** the lock-text refusal is decided before the view, so it wins over every view-derived outcome — a package that would also have been a paid-access 403 (`[PAID]`/no access), a failed view fetch, or a no-applicable-files skip reports the lock refusal instead (the Bun refusal and the ledger's `already vendored` skip still come first). The human `[error] (): ` line is printed during the download instead of the vendor step's failure line (the human (non-`--silent`) `scan --vendor` arm's baseline pre-check still fetches the views it verifies; only the download, the pristine fetch and the vendor step skip the package there). A purl the lockfiles pin hosted keeps the loop's refusal (its takeover restore rewrites the lock the gates read); other flavors (package-lock, pnpm-legacy, bun) and ecosystems are untouched, and `--dry-run` is unchanged. `vendor` (manifest-driven, no view fetch) keeps its per-package `failed` events but no longer fetches the pristine source of a lockfile-only package it refuses this way — the source is deferred to the backend, which refuses before reading it (no `vendor_fetched_missing` event and no registry request; a refused package whose registry is unreachable reports the gate's code instead of `vendor_fetch_failed`); only a package the lock resolves to a verifiable source is deferred, and one it does not resolve keeps its `package_not_installed` skip. Pinned by `tests/scan_vendor_e2e.rs` (`exact_download_plan`: scan and exact-purl get, pnpm and cargo scope), `tests/e2e_yarn_legacy_cachekey_refusal_build.rs` and `tests/vendor/vendor_rerun_no_network_e2e.rs`. +**Lock-text refusals before the download (v5.0)** — shared by `get --mode vendored` on both its paths and `scan --mode vendored`, after the Bun preflight above and the ledger's `already vendored` skip: a `pkg:npm/` result in a **pnpm, yarn classic or yarn berry** project, or a `pkg:cargo/` result, that its vendor backend refuses on the project's lock and manifest text alone is refused BEFORE its patch view is fetched — the pnpm / classic / berry gates the backend runs before it reads the package (coordinates, the lock and manifest reads and their line-ending / version / `cacheKey` / `.yarnrc.yml` gates, override and `resolutions` conflicts, the lock entry present and rewritable) and cargo's `locked_version_mismatch` (only when it is the crate's FIRST refusal; an in-tree `cargo vendor` copy still refuses in the loop as `already_vendored_in_tree`). **Scope:** only a package the vendor loop would hand to its backend is refused early — one installed on disk (the loop's own qualified-aware resolver plus the npm identity lookup), or one the lockfile inventory resolves to a verifiable registry source (a lock entry with an integrity, or the ledger-recovered pre-vendor resolution — exactly the entry the pristine fetch would use). A package absent from the lock and not installed never reached its backend and is untouched: its view is fetched, it downloads, and the vendor loop skips it `skipped` / `package_not_installed` as in v4.x (so cargo's `locked_version_mismatch` is refused early only for a crate installed at the unlocked version). The result becomes `{action:"failed", errorCode:, error:}` in `download.patches[]` / `patches[]` with the backend's exact code and detail, no view and no pristine fetch, no patch record, and therefore no vendor event: compared with v4.x, `download.downloaded` drops and `download.failed` rises by the number of such packages, `vendor.summary.failed` and `vendor.events` lose their `failed` events, and a lockfile-only package among them loses its `vendor_fetched_missing` event (it is never fetched). Exit code and top-level `status` are unchanged (`partial_failure`/1); the nested `vendor.status` becomes `success` when those refusals were the vendor step's only failures (observed on the depscan fixture: 3 refusals, `partialFailure` → `success`), and when every selected package is refused this way the human `scan --mode vendored` arm prints `Nothing was vendored: N patches failed (see above).`. **Precedence:** the lock-text refusal is decided before the view, so it wins over every view-derived outcome — a package that would also have been a paid-access 403 (`[PAID]`/no access), a failed view fetch, or a no-applicable-files skip reports the lock refusal instead (the Bun refusal and the ledger's `already vendored` skip still come first). The human `[error] (): ` line is printed during the download instead of the vendor step's failure line (the human (non-`--silent`) `scan --mode vendored` arm's baseline pre-check still fetches the views it verifies; only the download, the pristine fetch and the vendor step skip the package there). A purl the lockfiles pin hosted keeps the loop's refusal (its takeover restore rewrites the lock the gates read); other flavors (package-lock, pnpm-legacy, bun) and ecosystems are untouched, and `--dry-run` is unchanged. `vendor` (manifest-driven, no view fetch) keeps its per-package `failed` events but no longer fetches the pristine source of a lockfile-only package it refuses this way — the source is deferred to the backend, which refuses before reading it (no `vendor_fetched_missing` event and no registry request; a refused package whose registry is unreachable reports the gate's code instead of `vendor_fetch_failed`); only a package the lock resolves to a verifiable source is deferred, and one it does not resolve keeps its `package_not_installed` skip. Pinned by `tests/scan_vendor_e2e.rs` (`exact_download_plan`: scan and exact-purl get, pnpm and cargo scope), `tests/e2e_yarn_legacy_cachekey_refusal_build.rs` and `tests/vendor/vendor_rerun_no_network_e2e.rs`. * **Installed-version narrowing** (all modes, `get`'s search path): a CVE/GHSA fan-out returns one patch record per patched VERSION; get keeps only versions present here and emits calm `skipped` records (`errorCode: "package_not_installed"`) for the rest — never an error exit. Presence = installed on disk (qualified-aware resolver) ∪ already tracked in the manifest (record maintenance keeps working on hosts without an installed copy); hosted/vendored modes additionally count lockfile-resolved deps and vendor-ledger purls (mirroring scan's discovery supplements, including their `--global` gate). **Exempt** (no narrowing): UUID identifiers, exact-versioned PURL identifiers (explicit intent), `--save-only` runs (record-only has no installation precondition — the fresh-clone record→vendor flow keeps working), `--all-releases`, and the package-name path (already installed-derived). When EVERY found patch is filtered out, get exits 0 with the additive status **`not_installed`** (`{status:"not_installed", found:N, downloaded:0, applied:0, patches:[], warnings?}`) — never `no_match`, which remains pinned to the fuzzy package-name path. PnP layouts are surfaced, not misreported: yarn-PnP npm results skip with `errorCode: "yarn_pnp_unsupported"` in every mode; pnpm-PnP skips carry `pnpm_pnp_unsupported` in agent/vendored modes; hosted mode — the refusal's own remedy — keeps ONLY the versions the raw `pnpm-lock.yaml` text actually resolves (boundary-anchored probe over the v5/v6/v9 key spellings, so a large fan-out never requests grants for every version ever patched), labels a JUDGED miss `package_not_installed` exactly like a non-PnP project (the layout blocked nothing — the lock was read and the version isn't resolved), and reserves the layout code for an unreadable lock (no judgment possible). When EVERY narrowed-out result is a PnP refusal, the human terminal names the layout instead of claiming "not installed" and never advises `--all-releases` (which cannot make PnP patchable); the JSON status stays `not_installed` — consumers dispatch on the per-record `errorCode`. Hosted mode also runs the per-release VARIANT filter (`filter_to_installed_releases`) on its search path before requesting grants — agent/vendored runs get it inside the download engines — with the same keep-all-plus-warning fallbacks (surfaced as `(release_narrowing)`-prefixed strings in `warnings[]`). An ecosystem this binary has no crawler for is likewise never judged: its results are KEPT (absence from a crawl that never looked carries no information — the same fail-safe as scan's prune GC). The human `Found N patches:` listing shows only the patches whose package version survived the narrowing (the narrowing is judged over every result, so an installed package's paid fix a free user cannot download still lists as `[PAID] (no access)`, while skip records and counts cover only accessible patches), sorted by PURL in natural version order (`4.17.2` before `4.17.10`); the narrowed-out ones are summarized on stderr in one line per reason (`Skipped N patches for M package versions not installed here (use --all-releases to include them).`), and `--verbose` adds one `[skip] ()` line per skipped version after that summary, in natural version order. When the candidates hold more patches than were selected and the pick was made without a menu (a paid user's auto-pick, `--yes`, a non-TTY run), a `Selected:` block names the patch (purl, tier, short uuid, advisories) that will be installed before the prompt. Machine output (the prompt count, the JSON envelope) uses the kept set, unchanged. The finer per-release variant narrowing (`filter_to_installed_releases`) is unchanged and still runs inside the download engines (and before an agent-mode `--dry-run` preview, so the preview names only the variants a wet run would fetch). * **Deliberate divergences from scan** (documented, not drift): agent-mode get keeps its `selection_required` JSON posture for free multi-patch PURLs (scan and, v5.0, hosted/vendored get auto-pick); get has no `--vex` (an ambient `SOCKET_VEX` is ignored by get's modes), no `--prune`; get does not run scan's pre-vendor baseline annotation; and an all-narrowed-out run exits `not_installed` without entering the vendor step (heal-after-wipe re-vendoring stays `scan --mode vendored`'s job). Agent-mode `get` honors `--dry-run` too (v5.0): the search and uuid paths classify each selected patch against the manifest (read-only; an unreadable manifest fails closed like the wet run) and stop before the prompt, the download, any `.socket/` write and the apply — human `[would-add]` / `[would-update] … (replacing )` / `[skip] … (already in manifest)` lines then `[dry-run] Would download and apply N patches. No changes made.`; JSON `{status:"success", dryRun:true, found, downloaded:0, skipped, applied:0, patches:[{purl, uuid, action:"would_add"|"would_update"(+oldUuid)|"skipped"}, ], warnings?}`, exit 0. -`--dry-run` previews what `apply` / `rollback` / `scan --apply` / `repair` / `remove` — and `get` in every mode (hosted/vendored since v3.6, agent since v5.0) — would do without mutating disk. `get --mode hosted --dry-run` flows through the hosted engine's dry-run contract (no lock, no `.socket/`, no lockfile writes, `redirect.dryRun: true`); `get --mode vendored --dry-run` emits the same ledger-classification preview as scan's (`would_vendor` / `already_vendored` / `would_revendor`+`oldUuid` under the nested `vendor` key — plus, additive, `would_refuse` + `errorCode` + `error` for npm purls the wet run's Bun preflight would refuse: an in-sync `already_vendored` entry is exempt, as is a `would_revendor` entry whose `bun.lock` instances are all already local tuples; a purl the lock still resolves from the registry is refused like a fresh one, and the preview stays exit 0 / `status: "success"` with nothing written) before any download, and both skip the confirm prompt (nothing to confirm). In JSON mode, the envelope is populated with would-be actions and counts (`remove --dry-run` skips the confirmation prompt — there is nothing to confirm — and flips its would-be `Removed` events to `Verified` previews, so `summary.removed` stays "entries actually deleted"). `rollback --dry-run` (v5.0) previews every leg — the in-place restore verification, the vendored unwire (`Would revert/unwire vendoring for …`), the hosted upstream restore (every pin is resolved exactly like a wet run — registry lookups included, so a pin the wet run would refuse is previewed as that refusal — and nothing is flushed to disk), the manifest removals (simulated in memory), and the blob/archive GC — with no writes and no prompt. +`--dry-run` previews what `apply` / `rollback` / `scan --mode agent` / `repair` / `remove` — and `get` in every mode (hosted/vendored since v3.6, agent since v5.0) — would do without mutating disk. `get --mode hosted --dry-run` flows through the hosted engine's dry-run contract (no lock, no `.socket/`, no lockfile writes, `redirect.dryRun: true`); `get --mode vendored --dry-run` emits the same ledger-classification preview as scan's (`would_vendor` / `already_vendored` / `would_revendor`+`oldUuid` under the nested `vendor` key — plus, additive, `would_refuse` + `errorCode` + `error` for npm purls the wet run's Bun preflight would refuse: an in-sync `already_vendored` entry is exempt, as is a `would_revendor` entry whose `bun.lock` instances are all already local tuples; a purl the lock still resolves from the registry is refused like a fresh one, and the preview stays exit 0 / `status: "success"` with nothing written) before any download, and both skip the confirm prompt (nothing to confirm). In JSON mode, the envelope is populated with would-be actions and counts (`remove --dry-run` skips the confirmation prompt — there is nothing to confirm — and flips its would-be `Removed` events to `Verified` previews, so `summary.removed` stays "entries actually deleted"). `rollback --dry-run` (v5.0) previews every leg — the in-place restore verification, the vendored unwire (`Would revert/unwire vendoring for …`), the hosted upstream restore (every pin is resolved exactly like a wet run — registry lookups included, so a pin the wet run would refuse is previewed as that refusal — and nothing is flushed to disk), the manifest removals (simulated in memory), and the blob/archive GC — with no writes and no prompt. -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. - -`repair` keeps its `gc` visible alias. +v5.0 removed the v4 spellings `scan --apply` (use `--mode agent`), `scan --vendor` (use `--mode vendored`), `get --no-apply` (use `--save-only`), and the `download` (use `get`) and `gc` (use `repair`) subcommand aliases, with no deprecation release. Each is now an ordinary clap usage error (exit 2). **Python stale-install guard**: after a hosted redirect, `scan` / `get` use the Python crawler to inspect every matching installed package, including Poetry's out-of-tree virtualenvs, the project's Hatch environments (Hatch's data dir / `HATCH_DATA_DIR`, `[dirs.env] virtual`, explicit env `path`s) and `--global-prefix`. A readable file that differs from the patch's `afterHash` emits `redirect_pypi_stale_install` in JSON `redirect.warnings[]` and human stderr. The probe changes no installed files, re-runs on idempotent scans, and falls back to persisted patch records when fresh record fetching fails. Missing/unreadable files alone do not prove staleness; lock-only checkouts stay quiet. Dry runs skip the probe. Same-run VEX excludes positively stale Python packages (qualifier-insensitive), even with `--vex-no-verify` or a healthy copy in another interpreter; if nothing remains to attest, the command exits 1 with `no_applicable_patches`. Reinstall from the rewritten lock in the affected interpreter and verify with `socket-patch vex`. A stale Hatch environment instead names `hatch env remove ` / `hatch env prune`: Hatch's pip installer (and uv before Hatch 1.16) keeps a same-version release, so only a recreated env picks up the patch. The same Hatch envs are what agent mode patches and `vex` judges for a Hatch project. @@ -429,7 +427,7 @@ Human (non-`--json`) output is not a stable interface, but these rules hold: * npm's `allow-remote` notice prints as one line (`Note: set \`allow-remote=all\` in .npmrc …` or `Warning: npm >=12 refuses the hosted patches until …`); the full `redirect_npm_allow_remote` detail is in `--json` and under `--verbose`. * Hosted and vendored runs that change the project end with one shared `Next steps:` block (commit, reinstall + `socket-patch vex`, then any extra step). * A declined prompt prints `Cancelled; no changes made.`; the paid-plan upsell is `Upgrade to a paid Socket plan to access all patches: https://socket.dev/pricing`. -* `-h` lists about eight common options per command; `--help` lists all of them. `scan --apply` / `--vendor` are hidden (still accepted). +* `-h` lists about eight common options per command; `--help` lists all of them. ## Agent mode in CI (v5.0: `setup` removed) @@ -609,7 +607,7 @@ edge, never an alias copy of its own. socket-patch creates `.socket/` and everything under it itself and never writes a symlink or junction there, so a linked level below the project root is never its own. The rules below share one check (core `utils::containment`); each refusal names the linked path and carries the substring `is a symlink`. Levels at or above the project path (a symlinked home directory, `/tmp -> /private/tmp`) are never checked. -- **Blob and diff cache writes** (agent-mode `get`, `scan --apply`, `apply` and `repair` downloads, and `rollback`'s before-blob fetch): a linked `.socket/blobs` or `.socket/diffs`, or a linked `.socket/blobs/` entry, is refused before anything is written. That blob is reported failed (a `get` patch fails as a whole, and its own new blobs are unwound); nothing is written at the link's target. A project that pointed `.socket/blobs` at a shared cache must replace the link with a real directory. +- **Blob and diff cache writes** (agent-mode `get`, `scan --mode agent`, `apply` and `repair` downloads, and `rollback`'s before-blob fetch): a linked `.socket/blobs` or `.socket/diffs`, or a linked `.socket/blobs/` entry, is refused before anything is written. That blob is reported failed (a `get` patch fails as a whole, and its own new blobs are unwound); nothing is written at the link's target. A project that pointed `.socket/blobs` at a shared cache must replace the link with a real directory. - **Inline blobs in `get`**: a patch view's `blobContent` / `beforeBlobContent` must hash (git-sha256) to the `afterHash` / `beforeHash` it is stored under, or the patch fails with `content hash mismatch: content hashes to ` before anything is written, the same rule a downloaded blob is held to. An existing blob that already verifies is never rewritten; every blob is staged and renamed into place, never truncated in place. - **Ledgers**: the vendored ledger (`.socket/vendor/state.json`) and the pre-v5 hosted redirect ledger are refused when `.socket` itself, a directory below it or the ledger file is a link (`vendor_dir_symlink_unsupported` for the vendored flows), so a hosted run that still has to update a pre-v5 redirect ledger fails under a linked `.socket`. - **Agent-mode apply and rollback outside the install tree**: `apply` and `rollback` (dry run included) refuse a package whose written directories resolve outside the install tree they were found in: a Composer path repository (`vendor//` linked to first-party source), a `flit install --symlink` / editable-by-link package, or a package directory a package manager links into `site-packages` from its own prefix (Homebrew's Cellar, a Nix store path). The per-package error carries `outside the install tree`. For `apply` the remedy is to patch that source directly or install the package as a copy; for `rollback` it is to restore that source from version control, since socket-patch does not write into a tree it does not own. @@ -1221,12 +1219,12 @@ Every `--json` invocation emits a single JSON object that follows the **unified | Action | Emitted by | Meaning | |--------------|---------------------------------------|---------| | `discovered` | `scan`, `list` | Patch exists upstream / in the manifest — no work taken. | -| `downloaded` | `get`, `repair`, `scan --apply` | Patch bytes were fetched from the registry. `bytes` set. | +| `downloaded` | `get`, `repair`, `scan --mode agent` | Patch bytes were fetched from the registry. `bytes` set. | | `applied` | `apply`, `scan --sync` | Patch was written to disk. `files` enumerates what changed. | | `updated` | `apply`, `scan --sync`, `get` | A different UUID replaced an older one for this PURL. `oldUuid` set. | | `skipped` | every command | No-op — already patched, not in scope, filtered, etc. `errorCode` carries the reason. | | `failed` | every command | A specific patch attempt failed. `errorCode` + `error` set. | -| `removed` | `gc`/`repair`, `remove`, `rollback` | Data was removed from `.socket/` (or files rolled back). `bytes` optional. | +| `removed` | `repair`, `remove`, `rollback` | Data was removed from `.socket/` (or files rolled back). `bytes` optional. | | `verified` | `apply --dry-run`, `scan --dry-run` | The patch *would* apply cleanly. `files` lists previewed changes. | | `rebuilt` | `repair` | A missing/corrupt vendored artifact was restored from its exact server download (v5.0: never a lost ledger entry — see `vendor_ledger_missing`). `summary.rebuilt` counts these (the field is omitted while zero). | @@ -1243,7 +1241,7 @@ Every `--json` invocation emits a single JSON object that follows the **unified | `download_failed` | `failed` | repair/get: network or 404 on patch fetch. | | `cleanup_failed` | `skipped` (warning) | repair: an orphan-sweep pass (blobs, diff or package archives) failed mid-way (e.g. permission error). The run continues and exits 0; human mode carries the warning on stderr (not muted by `--silent`). v5.0: `rollback`'s default GC surfaces the same condition in its run-level `warnings[]` (and `remove`'s extended archive GC on stderr) — same posture, never affects the exit. | | `rollback_failed` | `failed` | remove/rollback: file restore could not complete. | -| `vendored` | `skipped` | apply (every ecosystem) + scan `--apply`: the package is managed by `socket-patch vendor`; the command yields ownership (scan also skips the download). v5.0: rollback no longer yields — its vendored leg reverts these entries by default, and its `vendored: []` array is reserved-empty (a corrupt vendor ledger surfaces via the `vendor_state_unreadable` warning + exit 1 — the skip cannot name purls, since naming them needs the ledger). Scan `--apply --json` additionally surfaces one run-level `vendored_ownership_retained` warning naming the skipped purls (additive; exit/status unchanged). | +| `vendored` | `skipped` | apply (every ecosystem) + scan `--mode agent`: the package is managed by `socket-patch vendor`; the command yields ownership (scan also skips the download). v5.0: rollback no longer yields — its vendored leg reverts these entries by default, and its `vendored: []` array is reserved-empty (a corrupt vendor ledger surfaces via the `vendor_state_unreadable` warning + exit 1 — the skip cannot name purls, since naming them needs the ledger). Scan `--mode agent --json` additionally surfaces one run-level `vendored_ownership_retained` warning naming the skipped purls (additive; exit/status unchanged). | | `vendor_reverted` | `removed` | remove: vendoring reverted (lock fragments restored, artifact + ledger entry gone) as part of removing the patch. | | `vendor_revert_failed` | top-level error | remove: the vendor revert failed; the manifest was NOT modified. | | `vendor_state_retained` | `skipped` | remove `--skip-rollback`: vendor wiring + artifact deliberately left in place (the next `vendor` run reconciles the dropped entry). Also the top-level error code when `--skip-rollback` targets a vendored patch with no manifest record (every `scan`/`get --mode vendored` entry — and, v5.0, the ledger-only leftover of an earlier `remove --skip-rollback` of a manifest-tracked vendored patch). | @@ -1266,7 +1264,7 @@ Every `--json` invocation emits a single JSON object that follows the **unified | `maven_trusted_checksums_left` / `nuget_default_config_left` / `upstream_uv_override_removed` | rollback/remove `warnings[]`; vendor advisory event (takeover) | upstream restore (v5.0): `.mvn` config keeps the trusted-checksums resolver lines because it holds more than hosted mode writes; `nuget.config` now holds only the nuget.org source (delete it if hosted mode created it); a transitive `override-dependencies` entry hosted mode added to `pyproject.toml` was removed. | | `upstream_registry_fallback` | rollback/remove `warnings[]`; vendor advisory event (takeover) | upstream restore: a yarn berry or vlt entry is restored from the version document of the registry the project resolves it against (`.yarnrc.yml` `npmRegistryServer`, vlt's node registry); that registry could not be read (e.g. it needs credentials), so the default registry's document was used and the restored tarball URL may not be the mirror's. | | `legacy_redirect_ledger_kept` | rollback `warnings[]` (+ remove stderr) | v5.0: a pre-v5 `.socket/vendor/redirect-state.json` could not be deleted once no hosted pin was left; the file is inert (never read for planning). Never flips the exit. | -| `vendor_stale_artifact_removed` | `removed` | vendor / scan `--vendor`: re-vendor under a newer patch uuid removed the previous uuid's orphaned artifact dir. | +| `vendor_stale_artifact_removed` | `removed` | vendor / scan `--mode vendored`: re-vendor under a newer patch uuid removed the previous uuid's orphaned artifact dir. | | `vendor_unsupported_ecosystem` | `skipped` | vendor: no vendor backend for this purl's ecosystem (jsr). | | `already_vendored` | `skipped` | vendor: artifact + wiring already in sync for this patch uuid. | | `unsafe_coordinates` | `failed` | vendor: purl/uuid would escape `.socket/vendor/` (tampered manifest/state); refused before any write. | @@ -1328,11 +1326,11 @@ Every `--json` invocation emits a single JSON object that follows the **unified | `vendor_variant_ambiguous` | `failed` | vendor / scan / get `--mode vendored` (pypi, gem): the package is not installed and the manifest holds several release variants of it (`?artifact_id=` / `?platform=`), none of which the vendor ledger records, so nothing says which distribution to vendor; install the package or keep one release variant. A variant the ledger records (at any patch uuid) is taken as the wired one and its siblings are left out without an event. | | `vendor_artifact_missing` | reason | The recorded artifact is missing; repair requires an online exact redownload. | | `vendor_artifact_corrupt` | reason | The artifact does not match the recorded hash or inventory; repair requires an online exact redownload. | -| `vendor_artifact_reused` | `skipped` (verbose note) | vendor / scan `--vendor` (pypi): the wiring was dropped by a relock but the committed wheel the ledger vouches for verified, so it was re-wired as-is — no service download, no rebuild; the lock pins the first run's sha again. | +| `vendor_artifact_reused` | `skipped` (verbose note) | vendor / scan `--mode vendored` (pypi): the wiring was dropped by a relock but the committed wheel the ledger vouches for verified, so it was re-wired as-is — no service download, no rebuild; the lock pins the first run's sha again. | | `vendor_redownload_failed` | `failed` | vendor: the same-UUID artifact could not be downloaded and verified against its original ledger. Existing files and fingerprints are preserved. | | `vendor_artifact_redownload_failed` | `failed` | repair: download unavailable, integrity mismatch, or downloaded bytes/inventory differ from the ledger. Existing files are preserved. | | `vendor_artifact_unrepairable` | `failed` | repair: the ledger identity or patch record cannot be trusted or recovered. | -| `vendor_uuid_mismatch` | `skipped` | repair: the manifest's patch uuid moved past the vendored artifact — a re-vendor (`vendor` / `scan --vendor`) is pending; repair does not cross patch generations. | +| `vendor_uuid_mismatch` | `skipped` | repair: the manifest's patch uuid moved past the vendored artifact — a re-vendor (`vendor` / `scan --mode vendored`) is pending; repair does not cross patch generations. | | `content_mismatch_overwritten` | `skipped` (warning) | apply (default policy): a file matched NEITHER beforeHash nor afterHash and was overwritten with the full verified patched content. This includes a file the patch adds (empty beforeHash) that already exists with other content. `--strict` turns this case into a `failed` event instead. | | `vendor_lock_checksums_unsupported` / `vendor_stale_lock_checksum` | `failed` | vendor (gem): an ambiguous/platform CHECKSUMS entry, or a v1-wired lock whose stale token blocks the hot path (run `vendor --revert` + re-vendor). | | `redirect_pypi_stale_install` | `redirect.warnings[]` (warning) | Hosted Python redirect: readable installed files differ from patched hashes. Read-only, repeated on re-scan, and excludes the package from same-run VEX. See the "Python stale-install guard" section. | @@ -1421,7 +1419,7 @@ Every `--json` invocation emits a single JSON object that follows the **unified | `apply` | `Applied` · `Updated` · `Skipped` (already_patched / package_not_installed / vendored) · `Failed` · `Verified` (dry-run) | | `vendor` | `Applied` (= vendored; `command` routes) · `Skipped` (refusals, warnings, unsupported ecosystems) · `Failed` · `Removed` (reconcile + `--revert`) · `Verified` (dry-run) | | `list` | `Discovered` (with `details.vulnerabilities`, `details.tier`, `details.license`, `details.description`, `details.exportedAt`; hosted pins (v5.0: one per `(purl, uuid)` the lockfiles wire) additionally carry `details.mode: "hosted"` and `details.lockfiles: []` (no `details.ledger` — hosted mode keeps no ledger; the human listing labels them `Mode: hosted (wired in )`), both additive and absent on manifest entries; v5.0: vendor-ledger records carry `details.mode: "vendored"` + `details.ledger: ".socket/vendor/state.json"` the same way, and the human listing labels them `Mode: vendored (recorded in .socket/vendor/state.json)`; a `state.json` that cannot be read or parsed degrades to nothing-to-consult with the stderr line `Warning: unreadable vendor ledger (); its vendored patches are not listed` — muted by `--silent`, exit unchanged) | -| `repair`/`gc`| `Downloaded` (or `Verified` on dry-run; a diff-mode repair adds a second one, `mode: "file"`, for the blobs of files the patches create) · `Rebuilt` (vendored artifacts; `Verified` previews on dry-run) · `Skipped` (vendor_uuid_mismatch) · `Removed` (or `Verified`) · `Failed` events | +| `repair` | `Downloaded` (or `Verified` on dry-run; a diff-mode repair adds a second one, `mode: "file"`, for the blobs of files the patches create) · `Rebuilt` (vendored artifacts; `Verified` previews on dry-run) · `Skipped` (vendor_uuid_mismatch) · `Removed` (or `Verified`) · `Failed` events | | `remove` | `Removed` (per purl; `Verified` on dry-run) · artifact-level `Removed`/`Verified` event (with `details.blobsRemoved`, `details.rolledBack`) | | `--update` | `Downloaded` → `Updated` (success) · `Skipped` (already_latest) · `Verified` (dry-run check, reason update_check) — see the Self-update contract section for details fields and top-level error codes | @@ -1431,7 +1429,7 @@ The unified envelope is the v3.0 contract. As of this release, these commands em - ✅ `apply` - ✅ `list` -- ✅ `repair` / `gc` +- ✅ `repair` - ✅ `remove` - ✅ `vendor` @@ -1445,9 +1443,9 @@ One command is **intentionally not** plain-envelope and will stay that way (not - `vex` — **hybrid**: the OpenVEX document is itself JSON and is the primary output; the envelope appears only under `--json --output `. See the [vex output channels](#vex-output-channels) table. -### `patches[]` entry shape for `get` and `scan --apply` +### `patches[]` entry shape for `get` and `scan --mode agent` -Per-patch records emitted in `patches[]` (and in `scan --apply`'s +Per-patch records emitted in `patches[]` (and in `scan --mode agent`'s `apply.patches[*]`) carry the same metadata regardless of which command produced them — both flow through `download_and_apply_patches_with` in `src/commands/agent_download.rs`. The shape is stable as of v3.0; consumers can @@ -1630,7 +1628,7 @@ per-package results). > **Known gap — batch responses without `publishedAt`.** `scan`'s > discovery (`packages[]`, and `updates[]` on a JSON report-only run) is > built from the **batch** endpoint, whose response shape currently omits `publishedAt`; -> the selection that `--apply` performs is built from the **by-package** +> the selection that `--mode agent` performs is built from the **by-package** > endpoint, which carries it. The two diverge wherever the date decides — > between patches of equal severity and advisory count — > where the batch side falls through to the tier/UUID tiebreak while apply @@ -1638,7 +1636,7 @@ per-package results). > > Live example: `pkg:npm/axios@1.6.0` has two free `HIGH` patches; > `packages[0].patches[0]` reports `0bc312a6…` (2026-03-27) while -> `--apply` installs the newer `83f5a654…` (2026-08-03), which is the +> `--mode agent` installs the newer `83f5a654…` (2026-08-03), which is the > correct choice. Only the reported ordering is affected — never which > patch lands on disk. > @@ -1704,7 +1702,7 @@ Exit `1` when `status` is `partialFailure` (any `events[*].action == "failed"`) |---|---| | `0` | Success | | `1` | Error (missing/invalid manifest, fetch failed, apply failed, selection cancelled in non-JSON mode, an invalid or ambiguous socket.yml on `scan` (v5.0), etc.) | -| `2` | Usage error: clap parse failures (unknown flag/value, missing required arg, an unknown subcommand such as the removed `setup`) and the conflicts the commands enforce themselves — `scan`'s cross-mode conflicts (`--mode` combined with a DIFFERENT mode's boolean spelling, rejected in `resolve_mode_flags`) and `--mode hosted\|vendored` with `--global`/`--global-prefix` (same enforcement point; `get` refuses the same combination); in hosted/vendored `scan` (bare `scan` included), a PATH that is not a directory, a PATH glob matching no directory, and `--json` with more than one project directory (`run_project_dirs`); on every project command (all but `--update`), a `--cwd` / `SOCKET_CWD` or `--global-prefix` / `SOCKET_GLOBAL_PREFIX` that is not an existing directory, and a non-default `--manifest-path` / `SOCKET_MANIFEST_PATH` that names a directory or sits in a project directory that does not exist (v5.0; checked once before the command runs, whether or not the command uses the flag — previously these read as an empty project and exited 0, and `remove` / `rollback` exited 1; a missing manifest FILE in an existing project stays legal, and `get` / `scan`, which create the manifest, still create a missing manifest directory); `remove --preserve-state --skip-rollback` (the no-op quadrant; flag- or env-sourced alike), an unparseable path glob on `scan`/`rollback`, a `scan` PATH outside the repository root and a malformed `SOCKET_MIN_SEVERITY` or `SOCKET_MAX_NEW_PATCHES` (v5.0), `repair --offline --download-only`. Under `--json`, every usage error a command enforces itself prints the coded error on stdout (v5.0, MAJOR: `scan`, `remove` and `rollback` printed nothing there): a full envelope for the envelope commands, `{status: "error", error: {code, message}}` for `scan`, `get` and `rollback`; clap's own parse errors, and the path-flag check that runs before every command (`--cwd` / `--global-prefix` / `--manifest-path` naming nothing), still print nothing on stdout. `vex` also exits `2` on hard errors before document generation (see its tri-state table below). v5.0: `get`'s self-enforced conflicts exit `2` too (`--id`/`--cve`/`--ghsa`/`--package` multi-select, `--mode hosted\|vendored --save-only`, a malformed identifier for a forced `--id`/`--cve`/`--ghsa`) — previously `1` (MAJOR). The never-implemented `get --one-off` / `rollback --one-off` (and `SOCKET_ONE_OFF`) are removed in v5.0; `--one-off` is now an ordinary unknown-flag clap error. | +| `2` | Usage error: clap parse failures (unknown flag/value, missing required arg, an unknown subcommand such as the removed `setup`) and the conflicts the commands enforce themselves — `scan`'s cross-mode conflict (`--sync` combined with a `--mode` other than `agent`, rejected in `resolve_mode_flags`) and `--mode hosted\|vendored` with `--global`/`--global-prefix` (same enforcement point; `get` refuses the same combination); in hosted/vendored `scan` (bare `scan` included), a PATH that is not a directory, a PATH glob matching no directory, and `--json` with more than one project directory (`run_project_dirs`); on every project command (all but `--update`), a `--cwd` / `SOCKET_CWD` or `--global-prefix` / `SOCKET_GLOBAL_PREFIX` that is not an existing directory, and a non-default `--manifest-path` / `SOCKET_MANIFEST_PATH` that names a directory or sits in a project directory that does not exist (v5.0; checked once before the command runs, whether or not the command uses the flag — previously these read as an empty project and exited 0, and `remove` / `rollback` exited 1; a missing manifest FILE in an existing project stays legal, and `get` / `scan`, which create the manifest, still create a missing manifest directory); `remove --preserve-state --skip-rollback` (the no-op quadrant; flag- or env-sourced alike), an unparseable path glob on `scan`/`rollback`, a `scan` PATH outside the repository root and a malformed `SOCKET_MIN_SEVERITY` or `SOCKET_MAX_NEW_PATCHES` (v5.0), `repair --offline --download-only`. Under `--json`, every usage error a command enforces itself prints the coded error on stdout (v5.0, MAJOR: `scan`, `remove` and `rollback` printed nothing there): a full envelope for the envelope commands, `{status: "error", error: {code, message}}` for `scan`, `get` and `rollback`; clap's own parse errors, and the path-flag check that runs before every command (`--cwd` / `--global-prefix` / `--manifest-path` naming nothing), still print nothing on stdout. `vex` also exits `2` on hard errors before document generation (see its tri-state table below). v5.0: `get`'s self-enforced conflicts exit `2` too (`--id`/`--cve`/`--ghsa`/`--package` multi-select, `--mode hosted\|vendored --save-only`, a malformed identifier for a forced `--id`/`--cve`/`--ghsa`) — previously `1` (MAJOR). The never-implemented `get --one-off` / `rollback --one-off` (and `SOCKET_ONE_OFF`) are removed in v5.0; `--one-off` is now an ordinary unknown-flag clap error. | `list` returns **`0`** for every project it can read, empty or not (**v5.0, BREAKING**: a project with no manifest and no ledger record — normal for hosted mode, which writes no manifest — used to exit `1` with `manifest_not_found`; it is now an empty list: `No patches in this project. Run \`socket-patch scan\`.` on stdout, and under `--json` the success envelope with `events: []`). Only an unreadable or invalid manifest (`manifest_unreadable` / `manifest_invalid`) exits `1`. Every lock-taking subcommand — including `scan`/`get --mode hosted` as of v5.0 — returns **`1`** with `error.code: lock_held` when another live socket-patch process holds `<.socket>/apply.lock`. @@ -1740,16 +1738,15 @@ Versioning lives in **`Cargo.toml`** at the workspace root (`version = "..."`) a | Change | Bump | |---|---| | Rename or remove a subcommand | **MAJOR** | -| Rename or remove a visible alias (`download`, `gc`) | **MAJOR** | -| Rename or remove a hidden alias (`--no-apply`) | **MAJOR** | +| Rename or remove a visible or hidden alias | **MAJOR** | | Rename, remove, or change short form of a flag (`-d`, `-m`, etc.) | **MAJOR** | | Change a default value (`--download-mode`, `--batch-size`, `--manifest-path`, …) | **MAJOR** | | Change an exit code's meaning or add a new non-zero code with different semantics | **MAJOR** | | Rename a JSON output key or change a `status` string | **MAJOR** | | Remove a JSON output key | **MAJOR** | | Rename or remove a per-patch `action` value (`added`/`updated`/`skipped`/`failed`) | **MAJOR** | -| Change `scan`'s default behavior (e.g. flipping `--prune` to opt-out, or making `--apply` default) | **MAJOR** | -| Demote `repair`'s `gc` from `visible_alias` to hidden, or remove the `repair` subcommand | **MAJOR** | +| Change `scan`'s default behavior (e.g. flipping `--prune` to opt-out, or making `--mode agent` the default) | **MAJOR** | +| Remove the `repair` subcommand | **MAJOR** | | Drop the bare-UUID fallback | **MAJOR** | | Add a *required* new flag | **MAJOR** | | Add a new subcommand | **MINOR** | diff --git a/crates/socket-patch-cli/src/commands/apply.rs b/crates/socket-patch-cli/src/commands/apply.rs index cf6a905a8..8bcb3722d 100644 --- a/crates/socket-patch-cli/src/commands/apply.rs +++ b/crates/socket-patch-cli/src/commands/apply.rs @@ -3249,7 +3249,7 @@ const LOCKFILE_ONLY_DETAIL: &str = /// this host — an `os`/`cpu`-gated optional dependency (`fsevents`, /// `@esbuild/-`), a devDependency under `npm ci --omit=dev`. The /// tree is in its correct end state, so they are calm skips, as `scan -/// --apply` treats lockfile-only packages. Global runs have no project +/// --mode agent` treats lockfile-only packages. Global runs have no project /// lock, so nothing is lockfile-resolved there. async fn lockfile_resolved(common: &GlobalArgs, unmatched: &[String]) -> HashSet { if unmatched.is_empty() || common.is_global() { diff --git a/crates/socket-patch-cli/src/commands/get.rs b/crates/socket-patch-cli/src/commands/get.rs index dd1315d99..77e287e2f 100644 --- a/crates/socket-patch-cli/src/commands/get.rs +++ b/crates/socket-patch-cli/src/commands/get.rs @@ -122,7 +122,6 @@ pub struct GetArgs { // exported-but-empty `SOCKET_SAVE_ONLY=`) would abort every `get`. #[arg( long = "save-only", - alias = "no-apply", env = "SOCKET_SAVE_ONLY", default_value_t = false, value_parser = crate::args::parse_bool_flag, diff --git a/crates/socket-patch-cli/src/commands/scan/discovery.rs b/crates/socket-patch-cli/src/commands/scan/discovery.rs index be697e93d..1661b0cf0 100644 --- a/crates/socket-patch-cli/src/commands/scan/discovery.rs +++ b/crates/socket-patch-cli/src/commands/scan/discovery.rs @@ -167,7 +167,7 @@ pub(crate) struct LedgerSupplement { /// Vendored-ledger packages with no crawled counterpart: on a fresh clone /// the committed artifact IS the dependency, so these stay discoverable -/// (updates[] detection, the table, and `scan --vendor` re-vendor/in-sync +/// (updates[] detection, the table, and `scan --mode vendored` re-vendor/in-sync /// runs all keep working before any install). They are NOT "lockfile-only" /// — nothing needs installing; the artifact satisfies the lock. `state` is /// the ledger `run` already loaded (`vendor::load_state`). diff --git a/crates/socket-patch-cli/src/commands/scan/hosted.rs b/crates/socket-patch-cli/src/commands/scan/hosted.rs index e99a7498e..5180ca3e2 100644 --- a/crates/socket-patch-cli/src/commands/scan/hosted.rs +++ b/crates/socket-patch-cli/src/commands/scan/hosted.rs @@ -553,7 +553,7 @@ pub(super) async fn run_redirect( batch_failed: bool, stage: &mut super::rollout::Stage, ) -> i32 { - // Same discovery/selection as `--apply`/`--vendor`. + // Same discovery/selection as agent and vendored mode. let discovered = match discover_selected( api_client, all_packages_with_patches, diff --git a/crates/socket-patch-cli/src/commands/scan/mod.rs b/crates/socket-patch-cli/src/commands/scan/mod.rs index 39dd6ddd3..6366f16ea 100644 --- a/crates/socket-patch-cli/src/commands/scan/mod.rs +++ b/crates/socket-patch-cli/src/commands/scan/mod.rs @@ -149,8 +149,7 @@ fn batch_chunks(purls: &[String], batch_size: usize, max_body_bytes: usize) -> V } /// The three patch-application modes `scan` can drive, selectable via -/// `--mode`. Vendored and agent also keep a hidden deprecated boolean -/// spelling (`--vendor`, `--apply`/`--sync`). +/// `--mode`. `--sync` is shorthand for `--mode agent --prune`. // // The `///` docs on the variants are user-facing `--help` text (shared // with `get --mode`); keep implementation notes in `//` comments. @@ -162,12 +161,10 @@ pub enum ScanMode { Hosted, /// Commit patched artifacts to `.socket/vendor/`: hermetic, /// offline-safe installs at the cost of repo size - // Equivalent to `--vendor`. Vendored, /// Record patches in `.socket/manifest.json` plus blobs and re-apply /// them in place (e.g. from CI): smallest repo footprint, but every /// install environment must run the agent - // Equivalent to `--apply`. Agent, } @@ -183,53 +180,34 @@ impl ScanMode { } } -/// Fold the boolean spellings (`--vendor` / `--apply` / `--sync`) into -/// `args.mode`, so `ScanMode` is the single -/// source of truth everything downstream reads (the booleans are input -/// spellings only, never consulted after this returns), and enforce the +/// Resolve `args.mode` from `--mode` and `--sync`, so `ScanMode` is the +/// single source of truth everything downstream reads, and enforce the /// cross-flag rules clap cannot express: /// -/// * `--mode X` combined with a boolean belonging to a DIFFERENT mode is a -/// contradiction → `Err`. Clap's `conflicts_with` is value-independent — -/// it could not allow `--mode vendored --vendor` while rejecting -/// `--mode hosted --vendor` — so the check lives here. -/// * The same mode spelled both ways (`--mode vendored --vendor`) is -/// redundant but accepted: both spellings mean one thing. -/// * `--sync` implies `--apply`, so it counts as an agent-mode spelling; -/// `--prune` is an orthogonal GC knob and never conflicts. (`--sync`'s +/// * `--sync` means `--mode agent --prune`, so `--mode X --sync` with any +/// mode other than agent is a contradiction → `Err`. Clap's +/// `conflicts_with` is value-independent — it could not allow +/// `--mode agent --sync` while rejecting `--mode hosted --sync` — so the +/// check lives here. `--mode agent --sync` is redundant but accepted. +/// * `--prune` is an orthogonal GC knob and never conflicts. (`--sync`'s /// prune half is orthogonal too, and stays a separate read in `run`.) /// Hosted mode runs no GC, so `--mode hosted --prune` stays accepted but /// emits an explicit `redirect_prune_ignored` warning in `run` rather /// than silently dropping the flag. /// /// Public (not `pub(crate)`) so the CLI-contract tests can exercise the -/// fold without driving a full `run()`. +/// resolution without driving a full `run()`. pub fn resolve_mode_flags(args: &mut ScanArgs) -> Result<(), String> { if let Some(mode) = args.mode { - // First boolean that selects a mode OTHER than the requested one. - let mut conflicting: Option<&'static str> = None; - if args.vendor && mode != ScanMode::Vendored { - conflicting = Some("--vendor"); - } - if args.apply && mode != ScanMode::Agent { - conflicting = Some("--apply"); - } if args.sync && mode != ScanMode::Agent { - conflicting = Some("--sync"); - } - if let Some(flag) = conflicting { // "cannot be used with" phrasing matches clap's conflict errors — // the scan_vendor_e2e contract test accepts exactly that shape. return Err(format!( - "--mode {} cannot be used with {flag}: the flags select different \ - modes (--vendor means --mode vendored; --apply and --sync mean \ - --mode agent)", + "--mode {} cannot be used with --sync: --sync means --mode agent --prune", mode.cli_name(), )); } - } else if args.vendor { - args.mode = Some(ScanMode::Vendored); - } else if args.apply || args.sync { + } else if args.sync { args.mode = Some(ScanMode::Agent); } else if !args.prune && !args.common.is_global() { // v5: hosted is the default. A `--prune` or global scan with no mode @@ -266,11 +244,6 @@ pub struct ScanArgs { #[arg(long = "batch-size", env = "SOCKET_BATCH_SIZE")] pub batch_size: Option, - // Hidden, deprecated spelling of `--mode agent`: download the selected - // patches and apply them in place. - #[arg(long, default_value_t = false, hide = true)] - pub apply: bool, - /// Garbage-collect after the scan: prune manifest entries for /// packages that are no longer installed, then delete orphan blob, /// diff and package-archive files from `.socket/`. Off by default so @@ -286,19 +259,10 @@ pub struct ScanArgs { #[arg(long, default_value_t = false)] pub sync: bool, - // Hidden, deprecated spelling of `--mode vendored`: vendor every - // patched dependency the scan selects into the committable - // `.socket/vendor/` tree instead of applying patches in place. - #[arg(long, default_value_t = false, hide = true, conflicts_with_all = ["apply", "sync"])] - pub vendor: bool, - /// How discovered patches are consumed [default: hosted]. A `--prune` /// or `--global` scan with no mode only reports - // The hidden `--vendor` and `--apply` are deprecated spellings of - // `--mode vendored` and `--mode agent` (`--sync` also selects agent); - // hosted has no boolean spelling. Combining `--mode` with a boolean - // from a DIFFERENT mode is rejected in `resolve_mode_flags`; the same - // mode spelled both ways is accepted. + // `--sync` also selects agent; combining it with a different `--mode` + // is rejected in `resolve_mode_flags`. #[arg(long = "mode", value_enum)] pub mode: Option, @@ -326,7 +290,7 @@ pub struct ScanArgs { /// On a successful scan, also generate an OpenVEX 0.2.0 document. /// `--vex ` is the trigger; the `--vex-*` knobs mirror the /// standalone `vex` command. The document is built from the manifest - /// as it stands after the scan (including any `--apply`/`--sync` + /// as it stands after the scan (including any `--mode agent`/`--sync` /// writes) and verified against on-disk state. A requested-but-failed /// VEX makes the command exit non-zero. #[command(flatten)] @@ -776,8 +740,8 @@ fn emit_discovery_error_json(result: &mut serde_json::Value, message: &str) { /// owned purls leave first (any uuid: the committed artifact IS the patch, /// and a manifest moved past the vendored uuid would break VEX verification /// until a vendor run refreshes the artifact — a newer patch still surfaces -/// in `updates[]`, the operator's signal to run `scan --vendor`), then -/// lockfile-only purls (nothing installed to patch in place; `scan --vendor` +/// in `updates[]`, the operator's signal to run `scan --mode vendored`), then +/// lockfile-only purls (nothing installed to patch in place; `scan --mode vendored` /// fetches them pristine). Both classes become calm `skipped` records — /// never an error. struct AgentSelection { @@ -864,7 +828,7 @@ fn download_params(args: &ScanArgs, save_only: bool, json: bool, silent: bool) - /// The run-level context the agent engine borrows from scan: the client /// `run` already built (proxy fallback included) and the flags the nested -/// apply inherits — so `scan --apply` honors `--lock-timeout` and never +/// apply inherits — so `scan --mode agent` honors `--lock-timeout` and never /// rebuilds the client. fn download_run<'a>(args: &ScanArgs, api_client: &'a ApiClient) -> DownloadRun<'a> { DownloadRun { @@ -1676,9 +1640,9 @@ async fn run_scan( ) -> i32 { apply_env_toggles(&args.common); - // Fold the legacy mode booleans into `args.mode` (see - // `resolve_mode_flags`). Cross-mode combinations are usage errors - // (exit 2); under --json they print the coded error on stdout. + // Resolve `--mode`/`--sync` into `args.mode` (see + // `resolve_mode_flags`). `--sync` with another mode is a usage error + // (exit 2); under --json it prints the coded error on stdout. if let Err(message) = resolve_mode_flags(&mut args) { // The global-install refusal is the one with its own code: it is // exactly `global_mode_conflict`'s message for the folded mode. @@ -2642,7 +2606,7 @@ async fn run_scan( } push_scan_json_warning(&mut result, HOSTED_WIRING_RETAINED, &detail); } - // --- Vendor path (if requested; conflicts with --apply/--sync) --- + // --- Vendor path (if requested; --sync selects agent instead) --- } else if vendor { // Must STAY a boxed fn: this branch's temporaries would otherwise // live in the enclosing poll frame in debug builds, which has to @@ -2672,7 +2636,7 @@ async fn run_scan( } // The GC and the VEX build below can write to stderr; the report- - // only arm has not flushed the scan event yet (the `--apply` arm + // only arm has not flushed the scan event yet (the agent arm // did, in `discover_selected`). telemetry.flush().await; diff --git a/crates/socket-patch-cli/src/commands/scan/vendor_flow.rs b/crates/socket-patch-cli/src/commands/scan/vendor_flow.rs index ea37492a5..cfe3423d0 100644 --- a/crates/socket-patch-cli/src/commands/scan/vendor_flow.rs +++ b/crates/socket-patch-cli/src/commands/scan/vendor_flow.rs @@ -1,4 +1,4 @@ -//! The vendored-mode (`--mode vendored` / `--vendor`) flow driven by +//! The vendored-mode (`--mode vendored`) flow driven by //! `scan`: the shared download → vendor-engine → GC step, its JSON and //! interactive arms, the pre-download skip partitions, and the `boxed_*` //! transient-frame constructors that keep the never-taken vendor branches @@ -66,7 +66,7 @@ type VendorStepError = (&'static str, String, Option>); /// [`VendorStepError`]. type VendorStepResult = Result<(bool, Envelope), VendorStepError>; -/// Dry-run preview for `scan --vendor` (and `get … --mode vendored +/// Dry-run preview for `scan --mode vendored` (and `get … --mode vendored /// --dry-run`): classify each selected patch against the vendor ledger /// without writing anything or touching the network beyond discovery. /// Action values are part of the CLI contract: `would_vendor` (no ledger @@ -523,7 +523,7 @@ async fn migrate_legacy_manifest_records( } } -/// The `scan --vendor` JSON path: discovery → (dry-run preview | download +/// The `scan --mode vendored` JSON path: discovery → (dry-run preview | download /// → vendor engine → GC → embedded VEX) → print `result` → exit code. /// The dry-run arm skips the VEX embed (emitting a `vex.skipped` marker /// instead): a dry run vendors nothing, so there is no state to attest. @@ -556,7 +556,7 @@ async fn run_vendor_json_path( // The npm half of scan's crawl, for the vendor engine to reuse. prior: Option<&NpmCrawlSnapshot>, ) -> i32 { - // Same discovery as `--apply`. Vendored purls are NOT filtered here — + // Same discovery as agent mode. Vendored purls are NOT filtered here — // re-vendoring a stale uuid is the point of the flag (same-uuid re-runs // land on the backend's `already_vendored` skip). let discovered = match discover_selected( @@ -600,7 +600,7 @@ async fn run_vendor_json_path( if args.common.dry_run { // No downloads, no backends: classify against the ledger - // and preview the GC, exactly like `--apply`'s dry run. + // and preview the GC, exactly like agent mode's dry run. let takeover = crate::commands::vendor::gem_takeover_preview_refusals( &args.common, selected.iter().map(|p| p.purl.as_str()), @@ -709,7 +709,7 @@ async fn run_vendor_json_path( final_code } -/// The `scan --vendor` interactive arm: download → vendor engine → GC, +/// The `scan --mode vendored` interactive arm: download → vendor engine → GC, /// with human-readable output. `prefetched` holds the views the pre-download /// baseline check already fetched (uuid-keyed), so the download phase /// serves those records from memory. Extracted + boxed for the same diff --git a/crates/socket-patch-cli/src/lib.rs b/crates/socket-patch-cli/src/lib.rs index a523287d6..9ba14529b 100644 --- a/crates/socket-patch-cli/src/lib.rs +++ b/crates/socket-patch-cli/src/lib.rs @@ -72,7 +72,6 @@ pub enum Commands { Scan(commands::scan::ScanArgs), /// Patch one package, CVE, GHSA or patch UUID (hosted mode by default) - #[command(visible_alias = "download")] Get(commands::get::GetArgs), /// List the patches in this project: hosted and vendored lockfile @@ -107,7 +106,6 @@ pub enum Commands { /// Restores missing blobs and diff/package archives, rebuilds missing /// or corrupt vendored artifacts, then deletes the artifacts nothing /// references. - #[command(visible_alias = "gc")] Repair(commands::repair::RepairArgs), // Internal parse target of the root `--update` flag (see the rewrite diff --git a/crates/socket-patch-cli/tests/apply/lockfile_only_skip.rs b/crates/socket-patch-cli/tests/apply/lockfile_only_skip.rs index 28a7494fe..ac00c57b9 100644 --- a/crates/socket-patch-cli/tests/apply/lockfile_only_skip.rs +++ b/crates/socket-patch-cli/tests/apply/lockfile_only_skip.rs @@ -5,7 +5,7 @@ //! //! The tree is in its correct end state, so such a purl is a calm //! `skipped`/`package_not_installed` that never fails the run — the same -//! treatment `scan --apply` gives lockfile-only packages. A purl with NO +//! treatment `scan --mode agent` gives lockfile-only packages. A purl with NO //! lock evidence still fails the all-miss run (the wrong-`--cwd` guard). use std::path::Path; diff --git a/crates/socket-patch-cli/tests/cli/output_modes_e2e.rs b/crates/socket-patch-cli/tests/cli/output_modes_e2e.rs index 37713fef5..9bfbd4efe 100644 --- a/crates/socket-patch-cli/tests/cli/output_modes_e2e.rs +++ b/crates/socket-patch-cli/tests/cli/output_modes_e2e.rs @@ -693,7 +693,7 @@ fn bare_uuid_fallback_treats_uuid_as_get_identifier() { fn each_subcommand_help_prints_usage() { let tmp = tempfile::tempdir().unwrap(); let subcommands = [ - "apply", "rollback", "get", "scan", "list", "remove", "repair", "gc", + "apply", "rollback", "get", "scan", "list", "remove", "repair", ]; for sub in subcommands { let (code, stdout, _stderr) = common::run_with_env(tmp.path(), &[sub, "--help"], &[]); @@ -718,8 +718,6 @@ fn top_level_help_prints_all_subcommands() { "top-level help missing {sub}; got: {stdout}" ); } - // `gc` is the visible alias. - assert!(stdout.contains("gc"), "top-level help missing `gc` alias"); } #[test] diff --git a/crates/socket-patch-cli/tests/cli_parse_get.rs b/crates/socket-patch-cli/tests/cli_parse_get.rs index 1393a60fd..b33dac655 100644 --- a/crates/socket-patch-cli/tests/cli_parse_get.rs +++ b/crates/socket-patch-cli/tests/cli_parse_get.rs @@ -1,8 +1,8 @@ //! Clap parser snapshot tests for the `get` subcommand. //! //! These tests pin the public CLI contract for `socket-patch get`: every -//! flag, every alias (including the hidden `--no-apply` and the visible -//! `download` alias), and every default. Changing any assertion here is a +//! flag and every default. The v4 spellings `--no-apply` and `download` were +//! removed in v5 and must stay parse errors. Changing any assertion here is a //! breaking change to the CLI surface — see //! `crates/socket-patch-cli/CLI_CONTRACT.md`. //! @@ -419,7 +419,7 @@ fn json_flag_sets_json() { assert_eq!(snapshot(&a), want); } -// --- save-only / --no-apply alias ------------------------------------------- +// --- save-only ------------------------------------------------------------------ #[test] #[serial_test::serial] @@ -432,20 +432,15 @@ fn save_only_flag_sets_save_only() { #[test] #[serial_test::serial] -fn no_apply_hidden_alias_sets_save_only() { - // `--no-apply` is a hidden alias for `--save-only`. It does not appear in - // `--help` but is widely used in existing scripts — this is part of the - // CLI contract. With the env scrubbed, this can only pass if the alias is - // actually wired to `save_only` (not because SOCKET_SAVE_ONLY was set). - let a = parse_get(&["some-id", "--no-apply"]); - let mut want = expected_defaults("some-id"); - want.save_only = true; - // The alias must set `save_only` and nothing else. - assert_eq!(snapshot(&a), want); - // ...and must be byte-for-byte equivalent to the canonical `--save-only` - // across the *entire* parsed surface, not just the `save_only` field. - let direct = parse_get(&["some-id", "--save-only"]); - assert_eq!(snapshot(&a), snapshot(&direct)); +fn removed_no_apply_alias_is_a_usage_error() { + // v5 removed the hidden `--no-apply` alias; `--save-only` (or + // SOCKET_SAVE_ONLY) is the only spelling. + let _scrub = EnvScrub::new(); + let err = match Cli::try_parse_from(["socket-patch", "get", "some-id", "--no-apply"]) { + Ok(_) => panic!("--no-apply should no longer parse"), + Err(e) => e, + }; + assert_eq!(err.kind(), clap::error::ErrorKind::UnknownArgument); } // --- download-mode ----------------------------------------------------------- @@ -480,7 +475,7 @@ fn download_mode_file() { // // `get --mode ` reuses scan's `ScanMode` value-enum // (see cli_parse_scan.rs) so the two commands can never drift on mode -// names. Unlike scan, `get` has NO legacy boolean spellings and no +// names. Unlike scan, `get` has no `--sync` shorthand and no // `resolve_mode_flags` fold — the parsed enum IS the source of truth // (`None` = agent, today's behavior; the `--save-only` conflict is // enforced inside `run()`, not by clap — pinned in get_modes_e2e.rs). @@ -574,22 +569,18 @@ fn mode_rejects_unknown_value() { } } -// --- `download` visible alias for `get` ------------------------------------- +// --- removed `download` alias ------------------------------------------------- #[test] #[serial_test::serial] -fn download_visible_alias_routes_to_get() { +fn removed_download_alias_is_a_usage_error() { + // v5 removed the `download` alias for `get`. let _scrub = EnvScrub::new(); - let cli = Cli::try_parse_from(["socket-patch", "download", "some-id"]).expect("parse"); - match cli.command { - Commands::Get(a) => { - // The alias must produce a `GetArgs` identical, across the entire - // parsed surface, to what bare `get some-id` produces — not some - // divergently-parsed command that merely happens to be `Get`. - assert_eq!(snapshot(&a), expected_defaults("some-id")); - } - _ => panic!("expected Get from `download` alias"), - } + let err = match Cli::try_parse_from(["socket-patch", "download", "some-id"]) { + Ok(_) => panic!("`download` should no longer parse"), + Err(e) => e, + }; + assert_eq!(err.kind(), clap::error::ErrorKind::InvalidSubcommand); } // --- Error paths ------------------------------------------------------------- diff --git a/crates/socket-patch-cli/tests/cli_parse_main.rs b/crates/socket-patch-cli/tests/cli_parse_main.rs index bc7c40c45..1663cda27 100644 --- a/crates/socket-patch-cli/tests/cli_parse_main.rs +++ b/crates/socket-patch-cli/tests/cli_parse_main.rs @@ -2,14 +2,18 @@ //! //! These tests cover the parser surface that doesn't fit in //! `src/lib.rs::tests` — clap's auto-generated help/version handling, the -//! "no subcommand" error kind, every subcommand name, and the -//! visible_alias values (`download` for `get`, `gc` for `repair`). +//! "no subcommand" error kind, every subcommand name, and the v4 +//! spellings v5 removed (`download`, `gc`, `scan --apply`/`--vendor`, +//! `get --no-apply`), which must stay usage errors. //! //! Each subcommand name and alias here is part of the CLI contract //! defined in `crates/socket-patch-cli/CLI_CONTRACT.md`. use socket_patch_cli::{parse_argv_with_shortcuts, Cli, Commands}; +#[path = "common/hermetic.rs"] +mod hermetic; + /// Parse through the **production** entry point. `main.rs` does not call /// `Cli::try_parse_from` directly — it calls `parse_argv_with_shortcuts`, which /// wraps clap with the bare-`` → `get ` rewrite. Driving these @@ -232,36 +236,59 @@ fn top_level_help() -> String { err.to_string() } +/// Spellings v5 removed with no deprecation release (#966). Each must be an +/// ordinary clap usage error, and the two former subcommand aliases must be +/// gone from `--help`. +const REMOVED_SPELLINGS: &[&[&str]] = &[ + &["scan", "--apply"], + &["scan", "--vendor"], + &["get", "some-id", "--no-apply"], + &["download", "some-id"], + &["gc"], +]; + #[test] -fn download_alias_parses_as_get() { - // `download` is the visible_alias for `get` — wrappers in the wild - // call this name directly, so it has to keep working. - let cli = parse(&["socket-patch", "download", "some-id"]) - .expect("`download` alias must parse as Get"); - match cli.command { - Commands::Get(args) => assert_eq!(args.identifier, "some-id"), - _ => panic!("expected Commands::Get via `download` alias"), +fn removed_spellings_are_usage_errors() { + for argv in REMOVED_SPELLINGS { + let mut full = vec!["socket-patch"]; + full.extend_from_slice(argv); + let err = expect_err(parse(&full)); + assert!( + matches!( + err.kind(), + clap::error::ErrorKind::UnknownArgument | clap::error::ErrorKind::InvalidSubcommand + ), + "{argv:?}: expected a usage error, got {:?}", + err.kind() + ); + assert_eq!(err.exit_code(), 2, "{argv:?}"); } - - // It must be a *visible* alias: clap lists visible aliases on the `get` - // row as `[aliases: download]`. A hidden alias would not appear here. let help = top_level_help(); assert!( - help.contains("[aliases: download]"), - "`download` must be a visible alias of `get` in --help; got:\n{help}" + !help.contains("aliases"), + "no subcommand aliases remain; got:\n{help}" ); + for line in help.lines() { + let first = line.split_whitespace().next(); + assert!( + first != Some("download") && first != Some("gc"), + "removed alias listed in --help: {line:?}" + ); + } } #[test] -fn gc_alias_parses_as_repair() { - // `gc` is the visible_alias for `repair`. - let cli = parse(&["socket-patch", "gc"]).expect("`gc` alias must parse as Repair"); - assert!(matches!(cli.command, Commands::Repair(_))); - - // As above: `gc` must remain a visible alias of `repair`. - let help = top_level_help(); - assert!( - help.contains("[aliases: gc]"), - "`gc` must be a visible alias of `repair` in --help; got:\n{help}" - ); +fn removed_spellings_exit_two_through_the_binary() { + let tmp = tempfile::tempdir().unwrap(); + for argv in REMOVED_SPELLINGS { + let out = hermetic::binary_command() + .args(*argv) + .current_dir(tmp.path()) + .env("SOCKET_TELEMETRY_DISABLED", "1") + .output() + .expect("run socket-patch"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert_eq!(out.status.code(), Some(2), "{argv:?}: {stderr}"); + assert!(stderr.contains("error:"), "{argv:?}: {stderr}"); + } } diff --git a/crates/socket-patch-cli/tests/cli_parse_repair.rs b/crates/socket-patch-cli/tests/cli_parse_repair.rs index 5204ace3f..2cfcc60de 100644 --- a/crates/socket-patch-cli/tests/cli_parse_repair.rs +++ b/crates/socket-patch-cli/tests/cli_parse_repair.rs @@ -1,11 +1,11 @@ -//! CLI contract tests for the `repair` subcommand (and its `gc` visible alias). +//! CLI contract tests for the `repair` subcommand. //! //! These tests pin the public clap parser surface for `RepairArgs`. In v3.0 //! `repair`'s `--download-mode` aligns with every other command (default //! `"diff"`); the legacy `"file"` default was retired so the surface stays //! uniform. Users that need legacy per-file blob downloads opt in with -//! `--download-mode file`. The `gc` visible alias is also exercised so a -//! refactor that drops it is caught immediately. +//! `--download-mode file`. The `gc` alias was removed in v5 and must stay +//! a parse error. //! //! See `crates/socket-patch-cli/CLI_CONTRACT.md` for the full repair table. //! @@ -111,17 +111,6 @@ fn parse_repair(extra: &[&str]) -> RepairArgs { } } -fn parse_gc(extra: &[&str]) -> RepairArgs { - let _scrub = EnvScrub::new(); - let mut argv = vec!["socket-patch", "gc"]; - argv.extend_from_slice(extra); - let cli = Cli::try_parse_from(&argv).expect("parse"); - match cli.command { - Commands::Repair(a) => a, - _ => panic!("expected Repair via gc alias"), - } -} - /// Owned, comparable snapshot of *every* parsed field in `RepairArgs` — its own /// `download_only` flag plus every field of the flattened `GlobalArgs`. /// `RepairArgs`/`GlobalArgs` are production types we may not touch and don't @@ -370,33 +359,6 @@ fn repair_download_mode_rejects_unknown_at_runtime() { ); } -#[test] -#[serial_test::serial] -fn repair_gc_alias_defaults_match_repair() { - let via_gc = parse_gc(&[]); - let via_repair = parse_repair(&[]); - - // The whole point of the alias: identical parsing. Compare the *entire* - // parsed surface, and independently anchor both to the contract defaults - // so the test isn't merely "the parser agrees with itself". - assert_eq!(snapshot(&via_gc), expected_defaults()); - assert_eq!(snapshot(&via_repair), expected_defaults()); - assert_eq!(snapshot(&via_gc), snapshot(&via_repair)); - assert_eq!( - DownloadMode::parse(&via_gc.common.download_mode), - Ok(DownloadMode::Diff) - ); -} - -#[test] -#[serial_test::serial] -fn repair_gc_alias_accepts_flags() { - let args = parse_gc(&["--dry-run"]); - let mut expected = expected_defaults(); - expected.dry_run = true; - assert_eq!(snapshot(&args), expected); -} - /// Regression: an exported-but-empty `SOCKET_DOWNLOAD_ONLY=` — the shell/CI /// idiom for blanking a variable without unsetting it — must mean "unset, /// fall back to the default (false)", not abort every `repair` invocation @@ -492,15 +454,11 @@ fn repair_unknown_flag_is_unknown_argument_error() { assert_eq!(err.kind(), clap::error::ErrorKind::UnknownArgument); } -// --- `gc` is a first-class visible alias for `repair` --------------------- +// --- `repair` in help; the removed `gc` alias ------------------------------- // // `scan --mode agent --prune` (or `--sync`) combines apply and GC in one -// pass, but `gc`/`repair` remain documented commands for users who want to -// clean up without an -// apply pass. These tests guard the `visible_alias = "gc"` attribute on -// `Commands::Repair` — if a future refactor demotes the alias (to -// `alias = "gc"` or removes it entirely), the help output check below -// will fail. +// pass, but `repair` remains a documented command for users who want to +// clean up without an apply pass. v5 removed its `gc` alias. fn top_level_help() -> String { let _scrub = EnvScrub::new(); @@ -524,29 +482,13 @@ fn repair_appears_in_top_level_help() { #[test] #[serial_test::serial] -fn gc_alias_is_visible_in_top_level_help() { +fn removed_gc_alias_is_a_usage_error() { let help = top_level_help(); - // clap renders a *visible* alias inline on the subcommand's help row as - // `[aliases: gc]`. A hidden `alias = "gc"` produces no such marker at all, - // so this fails loudly if the alias is demoted or dropped. Require the - // exact visible-alias marker — accepting a bare `gc` substring would match - // unrelated help text (e.g. the prose explaining the alias). - assert!( - help.contains("[aliases: gc]"), - "`gc` visible alias must be listed in --help output:\n{help}" - ); -} - -#[test] -#[serial_test::serial] -fn gc_alias_parses_as_repair() { + assert!(!help.contains("[aliases: gc]"), "{help}"); let _scrub = EnvScrub::new(); match Cli::try_parse_from(["socket-patch", "gc"]) { - Ok(cli) => assert!( - matches!(cli.command, Commands::Repair(_)), - "gc should resolve to Repair" - ), - Err(e) => panic!("gc alias should parse: {e}"), + Ok(_) => panic!("`gc` should no longer parse"), + Err(e) => assert_eq!(e.kind(), clap::error::ErrorKind::InvalidSubcommand), } } diff --git a/crates/socket-patch-cli/tests/cli_parse_scan.rs b/crates/socket-patch-cli/tests/cli_parse_scan.rs index 44d22d342..f3293b7d3 100644 --- a/crates/socket-patch-cli/tests/cli_parse_scan.rs +++ b/crates/socket-patch-cli/tests/cli_parse_scan.rs @@ -136,16 +136,11 @@ fn defaults_match_contract() { assert_eq!(args.common.api_url, None); // default applied in core resolver assert_eq!(args.common.api_token, None); assert_eq!(args.common.ecosystems, None); - assert!( - !args.apply, - "--apply default is false (scan --json stays read-only)" - ); assert!( !args.prune, "--prune default is false (GC is opt-in in v3.0)" ); assert!(!args.sync, "--sync default is false"); - assert!(!args.vendor, "--vendor default is false"); assert_eq!(args.mode, None, "--mode default is None (no mode selector)"); assert!(!args.common.dry_run, "--dry-run default is false"); assert!( @@ -385,9 +380,9 @@ fn unknown_flag_fails() { assert_eq!(err.kind(), clap::error::ErrorKind::UnknownArgument); } -// --- `--apply` flag and JSON shape ---------------------------------------- +// --- `--mode agent` and JSON shape ----------------------------------------- // -// `--apply` (== `--mode agent`) opts callers into the discover → select → +// `--mode agent` opts callers into the discover → select → // apply pipeline; a bare scan defaults to hosted mode, and only // `--prune`/global scans without a mode are report-only. The subprocess test // below also locks in the `updates` key that bots rely on to summarize what @@ -395,23 +390,23 @@ fn unknown_flag_fails() { #[test] #[serial_test::serial] -fn apply_flag_long_form() { - let args = parse_scan(&["--apply"]); - assert!(args.apply); +fn mode_agent_long_form() { + let args = parse_scan(&["--mode", "agent"]); + assert_eq!(args.mode, Some(ScanMode::Agent)); } #[test] #[serial_test::serial] -fn apply_flag_combines_with_json_and_yes() { - let args = parse_scan(&["--apply", "--json", "--yes"]); - assert!(args.apply); +fn mode_agent_combines_with_json_and_yes() { + let args = parse_scan(&["--mode", "agent", "--json", "--yes"]); + assert_eq!(args.mode, Some(ScanMode::Agent)); assert!(args.common.json); assert!(args.common.yes); } // --- `--prune` / `--sync` / `--dry-run` flags (v3.0 GC opt-in) ------------ // -// `--prune` opts into GC. `--sync` is sugar for `--apply --prune`. +// `--prune` opts into GC. `--sync` is sugar for `--mode agent --prune`. // `--dry-run` (`-d`) previews what those flags would do without mutating. #[test] @@ -423,9 +418,9 @@ fn prune_flag_long_form() { #[test] #[serial_test::serial] -fn prune_combines_with_apply_and_json() { - let args = parse_scan(&["--apply", "--json", "--yes", "--prune"]); - assert!(args.apply); +fn prune_combines_with_mode_agent_and_json() { + let args = parse_scan(&["--mode", "agent", "--json", "--yes", "--prune"]); + assert_eq!(args.mode, Some(ScanMode::Agent)); assert!(args.common.json); assert!(args.common.yes); assert!(args.prune); @@ -436,9 +431,9 @@ fn prune_combines_with_apply_and_json() { fn sync_flag_long_form() { let args = parse_scan(&["--sync"]); assert!(args.sync); - // --sync alone doesn't set --apply or --prune (the derivation + // --sync alone doesn't set --mode or --prune (the derivation // happens inside scan::run, not at parser time). - assert!(!args.apply); + assert_eq!(args.mode, None); assert!(!args.prune); } @@ -571,18 +566,17 @@ fn scan_json_empty_cwd_emits_updates_key() { ); assert!( v.get("apply").is_none(), - "no `apply` sub-object may appear when --apply was not passed" + "no `apply` sub-object may appear in a report-only scan" ); } -// --- `--mode` selector (documented spelling of the mode booleans) ---------- +// --- `--mode` selector --------------------------------------------------------- // -// `--mode ` is the RELEASED spelling of the three -// mode flags. `resolve_mode_flags` (run at the top of `scan::run`, -// exercised directly here) makes `args.mode` the single source of truth: -// the `--vendor`/`--apply`/`--sync` booleans fold INTO the enum (they are -// input spellings, never read downstream), and the cross-mode rules clap -// can't express (a value-dependent conflict) are enforced. +// `--mode ` selects the mode. `resolve_mode_flags` +// (run at the top of `scan::run`, exercised directly here) makes +// `args.mode` the single source of truth: `--sync` resolves to agent, and +// the cross-mode rule clap can't express (a value-dependent conflict +// between `--sync` and another `--mode`) is enforced. /// Parse `extra` (must parse cleanly at the clap level), then run the mode /// fold — mirroring exactly what `scan::run` does before it reads the @@ -597,7 +591,7 @@ fn parse_and_resolve(extra: &[&str]) -> Result { #[serial_test::serial] fn mode_hosted_is_the_source_of_truth() { // The parser records the enum verbatim and the fold leaves it as the - // single source of truth (the booleans are inputs, not outputs). + // single source of truth. let folded = parse_and_resolve(&["--mode", "hosted"]).expect("fold ok"); assert_eq!(folded.mode, Some(ScanMode::Hosted)); } @@ -607,12 +601,6 @@ fn mode_hosted_is_the_source_of_truth() { fn mode_vendored_is_the_source_of_truth() { let folded = parse_and_resolve(&["--mode", "vendored"]).expect("fold ok"); assert_eq!(folded.mode, Some(ScanMode::Vendored)); - let folded = parse_and_resolve(&["--vendor"]).expect("fold ok"); - assert_eq!( - folded.mode, - Some(ScanMode::Vendored), - "--vendor == --mode vendored" - ); } #[test] @@ -620,13 +608,7 @@ fn mode_vendored_is_the_source_of_truth() { fn mode_agent_is_the_source_of_truth() { let folded = parse_and_resolve(&["--mode", "agent"]).expect("fold ok"); assert_eq!(folded.mode, Some(ScanMode::Agent)); - let folded = parse_and_resolve(&["--apply"]).expect("fold ok"); - assert_eq!( - folded.mode, - Some(ScanMode::Agent), - "--apply == --mode agent" - ); - // --sync counts as an agent-mode spelling (its prune half is orthogonal). + // --sync selects agent mode (its prune half is orthogonal). let folded = parse_and_resolve(&["--sync"]).expect("fold ok"); assert_eq!( folded.mode, @@ -667,29 +649,21 @@ fn mode_rejects_unknown_value() { #[test] #[serial_test::serial] -fn mode_hosted_with_vendor_boolean_errors() { +fn mode_hosted_with_sync_errors() { // Clap ACCEPTS the combination (no value-dependent conflict is - // expressible), so the parse succeeds; the fold is what rejects a - // boolean belonging to a different mode. - let mut args = parse_scan(&["--mode", "hosted", "--vendor"]); - assert!( - resolve_mode_flags(&mut args).is_err(), - "--mode hosted + --vendor is a cross-mode contradiction" - ); -} - -#[test] -#[serial_test::serial] -fn mode_agent_with_apply_boolean_is_allowed() { - // Same mode spelled both ways is redundant but legal. - let folded = parse_and_resolve(&["--mode", "agent", "--apply"]).expect("same-mode ok"); - assert_eq!(folded.mode, Some(ScanMode::Agent)); + // expressible), so the parse succeeds; the fold is what rejects + // `--sync` (agent mode) next to a different `--mode`. + for mode in ["hosted", "vendored"] { + let mut args = parse_scan(&["--mode", mode, "--sync"]); + let err = resolve_mode_flags(&mut args).expect_err("cross-mode --sync"); + assert!(err.contains("cannot be used with --sync"), "{err}"); + } } #[test] #[serial_test::serial] fn mode_agent_with_sync_boolean_is_allowed() { - // --sync implies agent mode, so it counts as an agent-mode spelling. + // --sync implies agent mode, so `--mode agent` is redundant but legal. let folded = parse_and_resolve(&["--mode", "agent", "--sync"]).expect("same-mode ok"); assert_eq!(folded.mode, Some(ScanMode::Agent)); assert!(folded.sync); @@ -697,18 +671,20 @@ fn mode_agent_with_sync_boolean_is_allowed() { #[test] #[serial_test::serial] -fn legacy_mode_spellings_still_parse() { - // The boolean aliases keep working with no `--mode` given; the fold - // derives the mode enum from them (the inverse of the historical - // direction — `args.mode` is now the single source of truth). - assert!(parse_scan(&["--vendor"]).vendor); - assert!(parse_scan(&["--apply"]).apply); - let folded = parse_and_resolve(&["--vendor"]).expect("legacy fold ok"); - assert_eq!( - folded.mode, - Some(ScanMode::Vendored), - "legacy --vendor folds into the mode selector" - ); +fn removed_mode_booleans_are_usage_errors() { + // v5 removed the hidden `--apply` / `--vendor` spellings: each is now + // an ordinary clap unknown-argument error. + for flag in ["--apply", "--vendor"] { + let err = match try_parse_scan(&[flag]) { + Ok(_) => panic!("{flag} should no longer parse"), + Err(e) => e, + }; + assert_eq!( + err.kind(), + clap::error::ErrorKind::UnknownArgument, + "{flag}" + ); + } } // --- scrub-list completeness guard ----------------------------------------- @@ -720,16 +696,14 @@ fn legacy_mode_spellings_still_parse() { /// `Debug` derive) are formatted individually. fn snap(a: &ScanArgs) -> String { format!( - "{:?} paths={:?} batch_size={:?} apply={} prune={} sync={} vendor={} \ + "{:?} paths={:?} batch_size={:?} prune={} sync={} \ mode={:?} all_releases={} vex={:?} vex_product={:?} \ vex_no_verify={} vex_doc_id={:?} vex_compact={}", a.common, a.paths, a.batch_size, - a.apply, a.prune, a.sync, - a.vendor, a.mode, a.all_releases, a.vex.vex, diff --git a/crates/socket-patch-cli/tests/cli_scan_silent.rs b/crates/socket-patch-cli/tests/cli_scan_silent.rs index 5f36ab2b6..dbe392c89 100644 --- a/crates/socket-patch-cli/tests/cli_scan_silent.rs +++ b/crates/socket-patch-cli/tests/cli_scan_silent.rs @@ -307,7 +307,7 @@ async fn scan_silent_apply_flow_produces_no_output_but_still_applies() { } /// A v3 package-lock with a single registry-resolved dependency, so the -/// `--vendor` flow can rewire it to the vendored artifact (the npm vendor +/// `--mode vendored` flow can rewire it to the vendored artifact (the npm vendor /// backend keys off lock entries). fn write_npm_lock(root: &Path) { let lock = serde_json::json!({ @@ -365,7 +365,7 @@ fn seed_manifest_with_gone_entry(root: &Path) { } /// The vendored-mode GC line must honor `--silent` like the apply-mode one -/// does: `scan --vendor --prune --silent --yes` prints nothing when it +/// does: `scan --mode vendored --prune --silent --yes` prints nothing when it /// succeeds: `run_vendor_interactive_path` must not print "GC: pruned N /// manifest entries and removed …" (or the vendored-revert GC line). #[tokio::test] @@ -385,7 +385,8 @@ async fn scan_vendor_silent_gc_prints_nothing() { let (code, stdout, stderr) = run_scan( tmp.path(), &[ - "--vendor", + "--mode", + "vendored", "--prune", "--silent", "--yes", @@ -399,7 +400,7 @@ async fn scan_vendor_silent_gc_prints_nothing() { ); assert_eq!( code, 0, - "scan --vendor --prune must succeed; stdout={stdout:?} stderr={stderr:?}" + "scan --mode vendored --prune must succeed; stdout={stdout:?} stderr={stderr:?}" ); assert!( stdout.trim().is_empty(), @@ -455,7 +456,8 @@ async fn scan_vendor_silent_gc_prints_nothing() { let (loud_code, loud_stdout, loud_stderr) = run_scan( tmp2.path(), &[ - "--vendor", + "--mode", + "vendored", "--prune", "--yes", "--api-url", diff --git a/crates/socket-patch-cli/tests/covgap_commands_scan_mod.rs b/crates/socket-patch-cli/tests/covgap_commands_scan_mod.rs index 639766df3..48217a391 100644 --- a/crates/socket-patch-cli/tests/covgap_commands_scan_mod.rs +++ b/crates/socket-patch-cli/tests/covgap_commands_scan_mod.rs @@ -21,7 +21,7 @@ //! `hosted_wiring_retained` stderr warning after an in-place apply; //! * human runs: a bare scan runs hosted mode; a mode-less `--prune`/global //! scan is report-only (no download, no `.socket/`, prints the -//! `scan --mode agent` hint), while `--mode agent` / `--apply` apply +//! `scan --mode agent` hint), while `--mode agent` / `--sync` apply //! without prompting; //! * the hosted human arm's results table and `[UPDATE]` detection (parity //! with the agent/vendored arms); @@ -316,13 +316,11 @@ fn seed_manifest(root: &Path, entries: &[(&str, &str)]) { } // --------------------------------------------------------------------------- -// resolve_mode_flags — the remaining cross-mode conflict arms +// resolve_mode_flags — the `--sync` cross-mode conflict // --------------------------------------------------------------------------- -// Only the `--mode hosted --vendor` arm is pinned in cli_parse_scan.rs; -// these cover the --apply / --sync / --vendor booleans against a -// different --mode, plus ScanMode::Agent.cli_name() rendering into the -// message. Clap parses each combination fine (no value-dependent conflict -// is expressible); the fold is what rejects them. +// `--sync` means `--mode agent --prune`, so it contradicts any other +// `--mode`. Clap parses each combination fine (no value-dependent conflict +// is expressible); the fold is what rejects them, naming the mode. mod mode_fold { use clap::Parser; @@ -367,20 +365,10 @@ mod mode_fold { resolve_mode_flags(&mut args).expect_err("cross-mode contradiction must error") } - #[test] - #[serial_test::serial] - fn mode_vendored_with_apply_boolean_errors() { - let err = fold_err(&["--mode", "vendored", "--apply"]); - assert!( - err.contains("--mode vendored cannot be used with --apply"), - "the --apply arm must name the conflicting boolean: {err}" - ); - } - #[test] #[serial_test::serial] fn mode_hosted_with_sync_boolean_errors() { - // --sync counts as an agent-mode spelling, so it contradicts hosted. + // --sync means agent mode, so it contradicts hosted. let err = fold_err(&["--mode", "hosted", "--sync"]); assert!( err.contains("--mode hosted cannot be used with --sync"), @@ -390,13 +378,11 @@ mod mode_fold { #[test] #[serial_test::serial] - fn mode_agent_with_vendor_boolean_errors_naming_agent() { - // Pins ScanMode::Agent.cli_name(): "agent" must render into the - // message (the only user-visible spelling of the variant). - let err = fold_err(&["--mode", "agent", "--vendor"]); + fn mode_vendored_with_sync_boolean_errors() { + let err = fold_err(&["--mode", "vendored", "--sync"]); assert!( - err.contains("--mode agent cannot be used with --vendor"), - "the agent arm must render cli_name() and the boolean: {err}" + err.contains("--mode vendored cannot be used with --sync"), + "the vendored arm must render cli_name(): {err}" ); } } @@ -1348,10 +1334,10 @@ async fn scan_global_report_only_hint_keeps_the_global_scope() { ); } -/// Each spelling that folds to `--mode agent` applies without prompting. +/// Each spelling that selects `--mode agent` applies without prompting. #[tokio::test] async fn scan_human_agent_mode_applies_without_prompting() { - for flags in [&["--mode", "agent"][..], &["--apply"][..]] { + for flags in [&["--mode", "agent"][..], &["--sync"][..]] { let mock = MockServer::start().await; let purl = "pkg:npm/silent-target@1.0.0"; let before = b"before\n"; @@ -2320,10 +2306,10 @@ fn scan_hosted_rejects_global() { #[test] fn scan_mode_conflict_error_is_capitalized_and_names_no_hidden_flag() { let tmp = tempfile::tempdir().unwrap(); - let (code, _, stderr) = run_scan(tmp.path(), &["--mode", "hosted", "--vendor"]); + let (code, _, stderr) = run_scan(tmp.path(), &["--mode", "hosted", "--sync"]); assert_eq!(code, 2); assert!( - stderr.starts_with("Error: --mode hosted cannot be used with --vendor"), + stderr.starts_with("Error: --mode hosted cannot be used with --sync"), "{stderr:?}" ); assert!(!stderr.contains("--redirect"), "{stderr:?}"); diff --git a/crates/socket-patch-cli/tests/docker_e2e_npm.rs b/crates/socket-patch-cli/tests/docker_e2e_npm.rs index ea7169501..072880d53 100644 --- a/crates/socket-patch-cli/tests/docker_e2e_npm.rs +++ b/crates/socket-patch-cli/tests/docker_e2e_npm.rs @@ -133,7 +133,7 @@ async fn make_mock_server(after_hash: &str) -> MockServer { .mount(&server) .await; - // 2. By-package lookup (used by scan --apply for full PatchSearchResult). + // 2. By-package lookup (used by scan --mode agent for full PatchSearchResult). Mock::given(method("GET")) .and(path_regex(format!( "^/v0/orgs/{ORG}/patches/by-package/.+$" diff --git a/crates/socket-patch-cli/tests/docker_e2e_pypi.rs b/crates/socket-patch-cli/tests/docker_e2e_pypi.rs index c80e045c4..435177ea7 100644 --- a/crates/socket-patch-cli/tests/docker_e2e_pypi.rs +++ b/crates/socket-patch-cli/tests/docker_e2e_pypi.rs @@ -145,7 +145,7 @@ async fn make_mock_server(after_hash: &str) -> MockServer { .mount(&server) .await; - // 2. By-package lookup (used by scan --apply / --sync). + // 2. By-package lookup (used by scan --mode agent / --sync). Mock::given(method("GET")) .and(path_regex(format!( "^/v0/orgs/{ORG}/patches/by-package/.+$" diff --git a/crates/socket-patch-cli/tests/e2e_composer_version_identity.rs b/crates/socket-patch-cli/tests/e2e_composer_version_identity.rs index 3bfa212a2..80812dc0e 100644 --- a/crates/socket-patch-cli/tests/e2e_composer_version_identity.rs +++ b/crates/socket-patch-cli/tests/e2e_composer_version_identity.rs @@ -4,7 +4,7 @@ //! patch's base purl can say `pkg:composer/psr/log@3.0.2.0` while the //! project's `installed.json` and `composer.lock` say `3.0.2` or `v3.0.2`. //! Every mode must treat those as the same release: agent-mode `scan --sync` -//! patches the installed copy and its prune keeps the patch, `scan --vendor` +//! patches the installed copy and its prune keeps the patch, `scan --mode vendored` //! vendors and wires the lock entry (and `vex` attests it), and hosted //! `scan --redirect` repoints the lock entry. Each test runs the built binary //! against a wiremock API that serves only the padded spelling. @@ -260,7 +260,7 @@ async fn agent_sync_applies_and_keeps_a_padded_composer_patch() { assert_eq!(manifest["patches"][API_PURL]["uuid"], UUID, "{manifest:#}"); } -/// Vendored mode: `scan --vendor` finds the installed `3.0.2`, finds the +/// Vendored mode: `scan --mode vendored` finds the installed `3.0.2`, finds the /// lock's `3.0.2` entry for the `@3.0.2.0` patch, vendors the copy under the /// patch spelling and wires the entry; `vex` attests the vendored patch /// (lock `3.0.2` vs leaf `@3.0.2.0`); `vendor --revert` byte-restores the lock. @@ -275,9 +275,9 @@ async fn vendor_wires_and_attests_a_padded_composer_patch() { let (code, env) = run_json( root, &server.uri(), - &["scan", "--vendor", "--vendor-source", "service"], + &["scan", "--mode", "vendored", "--vendor-source", "service"], ); - assert_eq!(code, 0, "scan --vendor must succeed: {env:#}"); + assert_eq!(code, 0, "scan --mode vendored must succeed: {env:#}"); assert_eq!(env["vendor"]["summary"]["applied"], 1, "{env:#}"); assert_eq!(env["vendor"]["summary"]["failed"], 0, "{env:#}"); diff --git a/crates/socket-patch-cli/tests/e2e_gem.rs b/crates/socket-patch-cli/tests/e2e_gem.rs index f6f189113..320eb5dda 100644 --- a/crates/socket-patch-cli/tests/e2e_gem.rs +++ b/crates/socket-patch-cli/tests/e2e_gem.rs @@ -208,7 +208,7 @@ fn assert_before_hashes(gem_dir: &Path, files: &serde_json::Value) { } /// The "files are not patched" oracle used by `test_gem_dry_run` / -/// `test_gem_save_only` (after `get --no-apply` / `get --save-only`) must +/// `test_gem_save_only` (after `get --save-only`) must /// FAIL when the gem is actually in the applied state — otherwise a `get` /// that wrongly applies sails through the whole test. Hermetic stand-in for /// that masked regression: a gem dir whose files carry afterHash content @@ -243,7 +243,7 @@ fn not_patched_oracle_catches_applied_state() { assert!( oracle.is_err(), "the not-patched oracle passed on a fully applied gem — it cannot \ - catch a `get --no-apply`/`--save-only` that wrongly applies" + catch a `get --save-only` that wrongly applies" ); // And it must PASS on the pristine state (no false failures). @@ -565,7 +565,7 @@ fn test_gem_full_lifecycle() { ); } -/// `get --no-apply` + `apply --dry-run` should not modify files. +/// `get --save-only` + `apply --dry-run` should not modify files. #[test] #[ignore] fn test_gem_dry_run() { @@ -585,8 +585,8 @@ fn test_gem_dry_run() { // Download without applying. assert_run_ok( cwd, - &["get", GEM_UUID, "--mode", "agent", "--no-apply"], - "get --no-apply", + &["get", GEM_UUID, "--mode", "agent", "--save-only"], + "get --save-only", ); // Read manifest to get file list and expected hashes. @@ -595,7 +595,7 @@ fn test_gem_dry_run() { // Files should still be original (not patched) — checked against the // manifest's beforeHash, an oracle independent of the current disk - // state (a snapshot taken after `get --no-apply` would pass even if + // state (a snapshot taken after `get --save-only` would pass even if // the flag regressed and applied). assert_before_hashes(&gem_dir, &files); diff --git a/crates/socket-patch-cli/tests/e2e_npm.rs b/crates/socket-patch-cli/tests/e2e_npm.rs index 7486a85c9..c0ca6f4a1 100644 --- a/crates/socket-patch-cli/tests/e2e_npm.rs +++ b/crates/socket-patch-cli/tests/e2e_npm.rs @@ -288,15 +288,15 @@ fn test_npm_dry_run() { // Download the patch *without* applying. assert_run_ok( cwd, - &["get", NPM_UUID, "--mode", "agent", "--no-apply"], - "get --no-apply", + &["get", NPM_UUID, "--mode", "agent", "--save-only"], + "get --save-only", ); // File should still be original. assert_eq!( git_sha256_file(&index_js), BEFORE_HASH, - "file should not change after get --no-apply" + "file should not change after get --save-only" ); // Dry-run should report that the patch *would* apply, but leave the @@ -527,7 +527,7 @@ fn test_npm_save_only() { let index_js = cwd.join("node_modules/minimist/index.js"); assert_eq!(git_sha256_file(&index_js), BEFORE_HASH); - // Download with --save-only (new name for --no-apply). + // Download with --save-only. assert_run_ok(cwd, &["get", NPM_UUID, "--save-only"], "get --save-only"); // File should still be original. diff --git a/crates/socket-patch-cli/tests/e2e_pypi.rs b/crates/socket-patch-cli/tests/e2e_pypi.rs index d84c6db20..d4b6a2007 100644 --- a/crates/socket-patch-cli/tests/e2e_pypi.rs +++ b/crates/socket-patch-cli/tests/e2e_pypi.rs @@ -428,15 +428,15 @@ fn test_pypi_dry_run() { // Download without applying. assert_run_ok( cwd, - &["get", PYPI_UUID, "--mode", "agent", "--no-apply"], - "get --no-apply", + &["get", PYPI_UUID, "--mode", "agent", "--save-only"], + "get --save-only", ); // File should be unchanged. assert_eq!( git_sha256_file(&messages_py), original_hash, - "file should not change after get --no-apply" + "file should not change after get --save-only" ); // Read the manifest and snapshot the pre-apply on-disk state of EVERY diff --git a/crates/socket-patch-cli/tests/e2e_scan.rs b/crates/socket-patch-cli/tests/e2e_scan.rs index 7543490f6..6c9071807 100644 --- a/crates/socket-patch-cli/tests/e2e_scan.rs +++ b/crates/socket-patch-cli/tests/e2e_scan.rs @@ -1,8 +1,8 @@ //! End-to-end tests for the `scan` subcommand against the real Socket API. //! -//! Exercises the `scan --apply` + opt-in GC pipeline introduced in v3.0: +//! Exercises the `scan --mode agent` + opt-in GC pipeline introduced in v3.0: //! -//! * `scan --json --apply --yes` adds, updates, and skips patches based on +//! * `scan --json --mode agent --yes` adds, updates, and skips patches based on //! the existing manifest, emitting the `apply.patches[]` action vocabulary //! (`"added"`, `"updated"`, `"skipped"`). //! * A bare `scan --json` (hosted mode by default) emits the `updates` @@ -10,8 +10,8 @@ //! by default; it never writes `.socket/manifest.json` or node_modules. //! * `--prune` opts into garbage collection (manifest pruning + orphan //! file cleanup). Without it, scan leaves the manifest alone. -//! * `--sync` is sugar for `--apply --prune` — the canonical bot mode. -//! * `--dry-run` previews `--apply` / `--prune` / `--sync` actions +//! * `--sync` is sugar for `--mode agent --prune` — the canonical bot mode. +//! * `--dry-run` previews `--mode agent` / `--prune` / `--sync` actions //! without mutating disk. //! //! Uses the same minimist@1.2.2 patch fixture as `e2e_npm.rs`. Tests are @@ -54,7 +54,7 @@ const BEFORE_HASH: &str = "311f1e893e6eac502693fad8617dcf5353a043ccc0f7b4ba9fe38 /// the API would return. const FAKE_ORPHAN_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000"; -/// Fake UUID we plant in the manifest to force `scan --apply` into the +/// Fake UUID we plant in the manifest to force `scan --mode agent` into the /// `"updated"` branch. const FAKE_OLD_UUID: &str = "11111111-1111-4111-8111-111111111111"; @@ -109,7 +109,7 @@ fn run(cwd: &Path, args: &[&str]) -> (i32, String, String) { // The binary binds a wide `SOCKET_*` env surface (SOCKET_CWD, // SOCKET_DRY_RUN, SOCKET_GLOBAL, SOCKET_GLOBAL_PREFIX, SOCKET_PROXY_URL, // SOCKET_MANIFEST_PATH, ...). An ambient value silently changes what - // these tests exercise — SOCKET_DRY_RUN=true turns every `scan --apply` + // these tests exercise — SOCKET_DRY_RUN=true turns every `scan --mode agent` // into a no-op preview, and SOCKET_GLOBAL aims mutations at the host's // *real* global node_modules. Scrub the whole prefix so only the flags // each test passes are in effect; removing SOCKET_API_TOKEN also forces @@ -206,7 +206,7 @@ fn write_seed_manifest(cwd: &Path, purl: &str, uuid: &str) { // Tests // --------------------------------------------------------------------------- -/// `scan --json --apply --yes` against a fresh install should report a +/// `scan --json --mode agent --yes` against a fresh install should report a /// single `action: "added"` entry for the minimist patch, write the /// manifest, and patch the file on disk. The specific UUID/afterHash /// the upstream API serves can change over time (multiple free patches @@ -228,8 +228,8 @@ fn test_scan_apply_json_adds_new_patch() { let (stdout, _) = assert_run_ok( cwd, - &["scan", "--json", "--apply", "--yes"], - "scan --json --apply --yes (fresh)", + &["scan", "--json", "--mode", "agent", "--yes"], + "scan --json --mode agent --yes (fresh)", ); let v = parse_scan_json(&stdout); @@ -278,7 +278,7 @@ fn test_scan_apply_json_adds_new_patch() { ); } -/// Re-running `scan --json --apply --yes` after the patch is already in +/// Re-running `scan --json --mode agent --yes` after the patch is already in /// the manifest reports `action: "skipped"` and leaves the file alone. #[test] #[ignore] @@ -290,7 +290,11 @@ fn test_scan_apply_json_skips_existing() { npm_run(cwd, &["install", "minimist@1.2.2"]); let index_js = cwd.join("node_modules/minimist/index.js"); - assert_run_ok(cwd, &["scan", "--json", "--apply", "--yes"], "first run"); + assert_run_ok( + cwd, + &["scan", "--json", "--mode", "agent", "--yes"], + "first run", + ); // Capture the exact patched bytes after the first run. A correct // "skipped" re-run must leave the file *byte-for-byte identical*; merely // checking `!= BEFORE_HASH` would also pass if the second run re-applied @@ -301,7 +305,11 @@ fn test_scan_apply_json_skips_existing() { "first run should have patched the file", ); - let (stdout, _) = assert_run_ok(cwd, &["scan", "--json", "--apply", "--yes"], "second run"); + let (stdout, _) = assert_run_ok( + cwd, + &["scan", "--json", "--mode", "agent", "--yes"], + "second run", + ); let v = parse_scan_json(&stdout); let patches = v["apply"]["patches"] @@ -322,7 +330,7 @@ fn test_scan_apply_json_skips_existing() { } /// Seeding a manifest with a fake old UUID for the minimist PURL forces -/// `scan --apply` into the `"updated"` branch — the per-patch record +/// `scan --mode agent` into the `"updated"` branch — the per-patch record /// carries `oldUuid` matching the fake. #[test] #[ignore] @@ -336,7 +344,7 @@ fn test_scan_apply_json_updates_existing() { let (stdout, _) = assert_run_ok( cwd, - &["scan", "--json", "--apply", "--yes"], + &["scan", "--json", "--mode", "agent", "--yes"], "scan with seeded fake UUID", ); let v = parse_scan_json(&stdout); @@ -455,7 +463,7 @@ fn test_scan_json_read_only_no_mutation() { } /// When a previously-patched package is uninstalled, passing `--prune` -/// (or `--sync`) on the next `scan --apply --yes` prunes its manifest +/// (or `--sync`) on the next `scan --mode agent --yes` prunes its manifest /// entry and sweeps the orphan blobs. JSON output reports it in /// `gc.prunedManifestEntries`. #[test] @@ -470,7 +478,7 @@ fn test_scan_apply_prune_prunes_uninstalled_package() { // First run — patch is added (no --prune needed for the apply step). assert_run_ok( cwd, - &["scan", "--json", "--apply", "--yes"], + &["scan", "--json", "--mode", "agent", "--yes"], "initial apply", ); assert!(cwd.join(".socket/manifest.json").exists()); @@ -482,7 +490,7 @@ fn test_scan_apply_prune_prunes_uninstalled_package() { let (stdout, _) = assert_run_ok( cwd, - &["scan", "--json", "--apply", "--yes", "--prune"], + &["scan", "--json", "--mode", "agent", "--yes", "--prune"], "scan with --prune after uninstall", ); let v = parse_scan_json(&stdout); @@ -502,7 +510,7 @@ fn test_scan_apply_prune_prunes_uninstalled_package() { ); } -/// Default `scan --apply --yes` (no `--prune`) leaves manifest entries +/// Default `scan --mode agent --yes` (no `--prune`) leaves manifest entries /// for uninstalled packages alone. The `gc` field is omitted entirely /// from JSON output — users wanting cleanup must opt in. #[test] @@ -516,7 +524,7 @@ fn test_scan_apply_default_keeps_uninstalled_entries() { assert_run_ok( cwd, - &["scan", "--json", "--apply", "--yes"], + &["scan", "--json", "--mode", "agent", "--yes"], "initial apply", ); npm_run(cwd, &["uninstall", "minimist"]); @@ -524,7 +532,7 @@ fn test_scan_apply_default_keeps_uninstalled_entries() { let (stdout, _) = assert_run_ok( cwd, - &["scan", "--json", "--apply", "--yes"], + &["scan", "--json", "--mode", "agent", "--yes"], "scan without --prune", ); let v = parse_scan_json(&stdout); @@ -559,7 +567,7 @@ fn test_scan_apply_default_keeps_uninstalled_entries() { } /// Even without manifest changes, a stray orphan blob file in -/// `.socket/blobs/` is removed by the next `scan --apply --yes --prune` +/// `.socket/blobs/` is removed by the next `scan --mode agent --yes --prune` /// (GC must be opt-in via `--prune` or `--sync`). #[test] #[ignore] @@ -571,7 +579,7 @@ fn test_scan_apply_prune_cleans_orphan_blobs() { npm_run(cwd, &["install", "minimist@1.2.2"]); assert_run_ok( cwd, - &["scan", "--json", "--apply", "--yes"], + &["scan", "--json", "--mode", "agent", "--yes"], "initial apply", ); @@ -601,7 +609,7 @@ fn test_scan_apply_prune_cleans_orphan_blobs() { let (stdout, _) = assert_run_ok( cwd, - &["scan", "--json", "--apply", "--yes", "--prune"], + &["scan", "--json", "--mode", "agent", "--yes", "--prune"], "scan --prune with orphan blob present", ); let v = parse_scan_json(&stdout); @@ -654,7 +662,7 @@ fn test_scan_dry_run_sync_previews_apply_and_gc() { // an orphan so there's prune + cleanup work to preview. assert_run_ok( cwd, - &["scan", "--json", "--apply", "--yes"], + &["scan", "--json", "--mode", "agent", "--yes"], "initial apply", ); @@ -718,7 +726,7 @@ fn test_scan_json_no_gc_field_without_prune() { npm_run(cwd, &["install", "minimist@1.2.2"]); assert_run_ok( cwd, - &["scan", "--json", "--apply", "--yes"], + &["scan", "--json", "--mode", "agent", "--yes"], "initial apply", ); diff --git a/crates/socket-patch-cli/tests/e2e_vendor_composer_build.rs b/crates/socket-patch-cli/tests/e2e_vendor_composer_build.rs index 41a6f008a..648dd9c71 100644 --- a/crates/socket-patch-cli/tests/e2e_vendor_composer_build.rs +++ b/crates/socket-patch-cli/tests/e2e_vendor_composer_build.rs @@ -38,7 +38,7 @@ //! 7. **Revert proof**: `vendor --revert` restores composer.lock //! byte-for-byte and removes `.socket/vendor/` entirely. //! -//! A third twin drives `scan --vendor --vex` (the depscan-style +//! A third twin drives `scan --mode vendored --vex` (the depscan-style //! front door: batch discovery → vendored copy + lock wiring, NO manifest, //! embedded VEX in the same run) against the same mocked API, then the same //! fresh-checkout install and manifest-less VEX legs. @@ -250,7 +250,7 @@ fn run_vendored(driver: &VendorDriver<'_>, proj: &Path) -> (i32, String, String) } } -/// The discovery routes `scan --vendor` walks before the view fetch: batch +/// The discovery routes `scan --mode vendored` walks before the view fetch: batch /// search (the installed psr/log has one free patch) + the per-package /// search its selection consults. async fn mount_scan_mocks(server: &MockServer, purl: &str) { @@ -941,7 +941,7 @@ async fn composer_get_uuid_vendored_fresh_checkout_install() { }); } -/// `scan --vendor --vex` twin: batch discovery over the REAL +/// `scan --mode vendored --vex` twin: batch discovery over the REAL /// install → the vendored copy + composer.lock wiring with NO manifest /// (detached), the in-run embedded VEX attesting `(vendored)`, then the same /// fresh-checkout install and manifest-less VEX legs. @@ -977,7 +977,8 @@ async fn composer_scan_vendor_detached_vex_fresh_checkout_install() { &proj, &[ "scan", - "--vendor", + "--mode", + "vendored", "--vendor-source", "service", "--vex", @@ -998,7 +999,7 @@ async fn composer_scan_vendor_detached_vex_fresh_checkout_install() { ); assert_eq!( code, 0, - "scan --vendor --vex failed.\nstdout:\n{stdout}\nstderr:\n{stderr}" + "scan --mode vendored --vex failed.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); let env = parse_envelope(&stdout); assert_eq!(env["vex"]["statements"], 1, "in-run vex block: {env}"); @@ -1007,7 +1008,7 @@ async fn composer_scan_vendor_detached_vex_fresh_checkout_install() { assert_attested(&doc, &purl, UUID, Marker::Vendored, &[(GHSA, &[VEX_CVE])]); assert!( !proj.join(".socket/manifest.json").exists(), - "scan --vendor must not write a manifest: {env}" + "scan --mode vendored must not write a manifest: {env}" ); let copy_rel = format!(".socket/vendor/composer/{UUID}/{DEP}@{version}"); let entry = lock_entry(&lock_path, DEP); diff --git a/crates/socket-patch-cli/tests/e2e_vendor_composer_crlf.rs b/crates/socket-patch-cli/tests/e2e_vendor_composer_crlf.rs index 21ebe1460..8f4531caf 100644 --- a/crates/socket-patch-cli/tests/e2e_vendor_composer_crlf.rs +++ b/crates/socket-patch-cli/tests/e2e_vendor_composer_crlf.rs @@ -2,7 +2,7 @@ //! `core.autocrlf=true`, or a lock committed with CRLF). //! //! Every vendored front door — `vendor` over a staged manifest, `scan -//! --vendor`, and `get --mode vendored` — must write the wired lock +//! --mode vendored`, and `get --mode vendored` — must write the wired lock //! back in CRLF (so the diff is the one entry, not every line), an in-sync //! re-run must leave it byte-identical, and `vendor --revert` must restore //! the pre-vendor bytes exactly. Each test runs the built binary; the @@ -305,11 +305,11 @@ async fn scan_vendor_keeps_a_crlf_lock_and_reverts_it_byte_identically() { let root = tmp.path(); write_project(root); let uri = server.uri(); - let mut args = vec!["scan", "--vendor", "--vendor-source", "service"]; + let mut args = vec!["scan", "--mode", "vendored", "--vendor-source", "service"]; args.extend(api_args(&uri)); let (code, env) = run_json(root, &args); - assert_eq!(code, 0, "scan --vendor must succeed: {env:#}"); + assert_eq!(code, 0, "scan --mode vendored must succeed: {env:#}"); assert_eq!(env["vendor"]["summary"]["applied"], 1, "{env:#}"); let vendored = assert_wired_crlf(root); assert_rerun_and_revert(root, &args, &vendored); diff --git a/crates/socket-patch-cli/tests/e2e_vex_build/hatch.rs b/crates/socket-patch-cli/tests/e2e_vex_build/hatch.rs index b00e56480..b44febffd 100644 --- a/crates/socket-patch-cli/tests/e2e_vex_build/hatch.rs +++ b/crates/socket-patch-cli/tests/e2e_vex_build/hatch.rs @@ -14,7 +14,7 @@ //! hosted rewriter reads the exact pin from the declaration); //! 2. the synthetic patch appends a marker to `six.py`; a wiremock Socket //! API serves discovery, the grant, the view and the patched wheel; -//! 3. hosted: `scan --mode hosted --vex`; vendored: `scan --vendor +//! 3. hosted: `scan --mode hosted --vex`; vendored: `scan --mode vendored //! --vendor-source build --vex` — the same-run VEX attests, and the //! declaration becomes `six @ #sha256=…` / //! `six @ {root:uri}/.socket/vendor/pypi//#sha256=…`; @@ -201,7 +201,7 @@ fn hatch() -> Option { fn scan_mode_args(mode: Mode) -> Vec<&'static str> { match mode { Mode::Hosted => vec!["--mode=hosted"], - Mode::Vendored => vec!["--vendor", "--vendor-source", "service"], + Mode::Vendored => vec!["--mode", "vendored", "--vendor-source", "service"], } } diff --git a/crates/socket-patch-cli/tests/e2e_vex_build/pdm.rs b/crates/socket-patch-cli/tests/e2e_vex_build/pdm.rs index 978d0aaa2..c25f09af9 100644 --- a/crates/socket-patch-cli/tests/e2e_vex_build/pdm.rs +++ b/crates/socket-patch-cli/tests/e2e_vex_build/pdm.rs @@ -12,7 +12,7 @@ //! wiremock Socket API serves discovery, the grant, the view and the //! patched wheel itself; //! 3. hosted: `scan --mode hosted --vex` (same-run VEX attests); vendored: -//! `scan --vendor --vendor-source build --vex`; +//! `scan --mode vendored --vendor-source build --vex`; //! 4. a FRESH checkout of only the committable files (pyproject, pdm.lock, //! `.socket/` minus the manifest) is installed by the real `pdm sync` — //! from the mock patch server (hosted) or the committed wheel (vendored) @@ -25,7 +25,7 @@ //! `record_unavailable` with zero requests, and the lock reverted to the //! registry (vendor ledger + artifacts kept) → `vendor_unwired` / //! hosted: nothing names the patch, `--no-verify` included; plus a -//! manifest-less `scan --mode hosted|--vendor --vex` re-run. +//! manifest-less `scan --mode hosted|--mode vendored --vex` re-run. //! //! Releases whose lock format loses url/path identity (PDM 1.8 – 1.15 = //! 3.1, 2.0 – 2.7 = 4.0 – 4.2) must REFUSE both scans with the lock @@ -311,7 +311,7 @@ fn patched_of(pristine: &[u8]) -> Vec { fn scan_mode_args(mode: Mode) -> Vec<&'static str> { match mode { Mode::Hosted => vec!["--mode=hosted"], - Mode::Vendored => vec!["--vendor", "--vendor-source", "service"], + Mode::Vendored => vec!["--mode", "vendored", "--vendor-source", "service"], } } diff --git a/crates/socket-patch-cli/tests/e2e_vex_build/pip.rs b/crates/socket-patch-cli/tests/e2e_vex_build/pip.rs index 2adad6956..d163b9c6d 100644 --- a/crates/socket-patch-cli/tests/e2e_vex_build/pip.rs +++ b/crates/socket-patch-cli/tests/e2e_vex_build/pip.rs @@ -14,7 +14,7 @@ //! 1. `python -m pip install -r requirements.txt` with the real pip major //! from PyPI (the pristine install); //! 2. `socket-patch scan --mode hosted --vex` (hosted, on the lock-only -//! checkout) / `scan --vendor --vendor-source build --vex` (vendored, +//! checkout) / `scan --mode vendored --vendor-source build --vex` (vendored, //! from the pristine install) against a wiremock Socket API that also //! serves the patched wheel — the same-run document attests; //! 3. a FRESH checkout (requirements files + `.socket/`) into a new venv @@ -25,7 +25,7 @@ //! 4. the manifest-less VEX matrix (`vex_pipenv_pip_real`): manifest //! deleted, ledgers deleted, `--offline` (zero requests), requirements //! reverted to the registry pin (also `--no-verify`), `apply --vex`; -//! plus the embedded `scan --mode hosted --vex` / `scan --vendor --vex` +//! plus the embedded `scan --mode hosted --vex` / `scan --mode vendored --vex` //! re-run on the manifest-less checkout. //! //! `#[ignore]`d (network: PyPI) — run with `--ignored`; CI sets diff --git a/crates/socket-patch-cli/tests/e2e_vex_build/pipenv.rs b/crates/socket-patch-cli/tests/e2e_vex_build/pipenv.rs index d32bb582d..6566226d5 100644 --- a/crates/socket-patch-cli/tests/e2e_vex_build/pipenv.rs +++ b/crates/socket-patch-cli/tests/e2e_vex_build/pipenv.rs @@ -6,7 +6,7 @@ //! 1. `pipenv install six==1.16.0` from PyPI (in-project venv) — the native //! Pipfile.lock that release writes; //! 2. `socket-patch scan --mode hosted --vex` (hosted, on the lock-only -//! checkout: the CI shape) / `scan --vendor --vendor-source build --vex` +//! checkout: the CI shape) / `scan --mode vendored --vendor-source build --vex` //! (vendored, from the pristine install) against a wiremock Socket API //! that also serves the patched wheel — the same-run document attests; //! 3. a FRESH checkout of the committed state (Pipfile, Pipfile.lock, @@ -17,7 +17,7 @@ //! 4. the manifest-less VEX matrix (`vex_pipenv_pip_real`): manifest //! deleted, ledgers deleted, `--offline` (zero requests), lock reverted //! to the registry (also `--no-verify`), `apply --vex`; plus the -//! embedded `scan --mode hosted --vex` / `scan --vendor --vex` re-run on the +//! embedded `scan --mode hosted --vex` / `scan --mode vendored --vex` re-run on the //! manifest-less checkout. //! //! Versions: `SOCKET_PATCH_PIPENV_E2E_VERSIONS` (space / comma separated), diff --git a/crates/socket-patch-cli/tests/e2e_vex_build/poetry.rs b/crates/socket-patch-cli/tests/e2e_vex_build/poetry.rs index ff991145e..883d506fe 100644 --- a/crates/socket-patch-cli/tests/e2e_vex_build/poetry.rs +++ b/crates/socket-patch-cli/tests/e2e_vex_build/poetry.rs @@ -7,7 +7,7 @@ //! hosted = `scan --mode hosted --vex` on the lock-only checkout (the lock is //! repointed at a patched wheel the mock serves — v5 writes NO redirect //! ledger — the same-run VEX attests from the lock's sha256 pin); vendored -//! = `scan --vendor --vendor-source build --vex` over the pristine +//! = `scan --mode vendored --vendor-source build --vex` over the pristine //! install (the patched wheel is committed under //! `.socket/vendor/pypi//`, the lock is rewired to it, and only the //! ledger is written — vendored mode is manifest-free, so the ledger @@ -931,7 +931,8 @@ fn poetry_vendored_fresh_install_then_manifestless_vex() { &service, &[ "scan", - "--vendor", + "--mode", + "vendored", "--vendor-source", "service", "--vex", @@ -940,7 +941,7 @@ fn poetry_vendored_fresh_install_then_manifestless_vex() { PRODUCT, ], ); - assert_eq!(code, Some(0), "scan --vendor: {env}"); + assert_eq!(code, Some(0), "scan --mode vendored: {env}"); let rel = format!(".socket/vendor/pypi/{VENDORED_UUID}/{WHEEL}"); assert!( project.join(&rel).is_file(), @@ -954,7 +955,7 @@ fn poetry_vendored_fresh_install_then_manifestless_vex() { lock.contains(&wheel_sha), "the lock pins the committed wheel:\n{lock}" ); - // Vendored mode is manifest-free (v5.0, CLI_CONTRACT `scan --vendor`): + // Vendored mode is manifest-free (v5.0, CLI_CONTRACT `scan --mode vendored`): // the ledger entry is detached and embeds the patch record, and // `.socket/manifest.json` is never written. assert!( diff --git a/crates/socket-patch-cli/tests/e2e_vex_lockfile/pdm.rs b/crates/socket-patch-cli/tests/e2e_vex_lockfile/pdm.rs index 7de78ae5c..6eddaf611 100644 --- a/crates/socket-patch-cli/tests/e2e_vex_lockfile/pdm.rs +++ b/crates/socket-patch-cli/tests/e2e_vex_lockfile/pdm.rs @@ -27,7 +27,7 @@ //! root-escaping path and mismatched records never attest; g) hosted //! installed-tree states (not installed → pin, patched → hashed, pristine → //! `not_applied`), pinless hosted needs an install, vendored over a pristine -//! venv warns; plus the embedded `scan --mode hosted|--vendor --vex`, +//! venv warns; plus the embedded `scan --mode hosted|--mode vendored --vex`, //! `apply --vex` and `vendor --vex`. //! //! The real-PDM counterpart (real `pdm lock` / `pdm sync`, per PDM release) diff --git a/crates/socket-patch-cli/tests/e2e_vex_lockfile/pipenv.rs b/crates/socket-patch-cli/tests/e2e_vex_lockfile/pipenv.rs index dc9438058..002db0a5d 100644 --- a/crates/socket-patch-cli/tests/e2e_vex_lockfile/pipenv.rs +++ b/crates/socket-patch-cli/tests/e2e_vex_lockfile/pipenv.rs @@ -21,7 +21,7 @@ //! record-mismatched references never attest; hosted not-installed attests //! from the pin, a pristine install is `not_applied`, pinless needs an //! install; a vendored wheel over a pristine venv warns; and the embedded -//! forms (`scan --mode hosted --vex` / `scan --vendor --vex` re-runs, +//! forms (`scan --mode hosted --vex` / `scan --mode vendored --vex` re-runs, //! `apply --vex`, `vendor --vex`). //! //! Pipenv-specific cells below: a relock that re-serializes AROUND our diff --git a/crates/socket-patch-cli/tests/e2e_vex_lockfile/poetry.rs b/crates/socket-patch-cli/tests/e2e_vex_lockfile/poetry.rs index c37145a4a..607ef8b0c 100644 --- a/crates/socket-patch-cli/tests/e2e_vex_lockfile/poetry.rs +++ b/crates/socket-patch-cli/tests/e2e_vex_lockfile/poetry.rs @@ -12,7 +12,7 @@ //! locks of every Poetry release (`socket-patch-core/tests/fixtures/poetry/ //! <0.12.17..2.4.3>/`: lock formats "0" / "1.0" / "1.1" / "2.0" / "2.1"), and //! the writer-driven cells run the real `scan --mode hosted --vex` / `scan -//! --vendor --vex` / `apply --vex` / `vendor --vex` binaries against a +//! --mode vendored --vex` / `apply --vex` / `vendor --vex` binaries against a //! wiremock patch API. The package is renamed to a made-up `vexfixture`, so //! no interpreter's global site-packages on the test host can hold a copy. //! @@ -1240,7 +1240,7 @@ fn strip_pin(lock: &str, pin: &str) -> String { } // ════════════════════════════════════════════════════════════════════════ -// Writer-driven: the REAL `scan --mode hosted --vex` / `scan --vendor --vex` +// Writer-driven: the REAL `scan --mode hosted --vex` / `scan --mode vendored --vex` // write the wiring and the ledgers; then the manifest (and the ledgers) are // deleted and standalone + embedded VEX must still attest — and stop // attesting once the real revert unwinds the wiring with the ledger left @@ -1554,7 +1554,7 @@ fn scan_redirect_wiring_attests_without_manifest_or_ledger() { } } -/// `scan --vendor --vex --vendor-source build` over the installed (pristine) +/// `scan --mode vendored --vex --vendor-source build` over the installed (pristine) /// dist rebuilds the patched wheel into `.socket/vendor/pypi//`, wires /// the lock, writes the ledger (never a manifest: vendored mode is /// manifest-free) and attests in-run. A legacy manifest seeded beside the @@ -1568,7 +1568,7 @@ fn scan_redirect_wiring_attests_without_manifest_or_ledger() { #[test] fn scan_vendor_wiring_attests_without_manifest_or_ledger() { for release in WRITER_RELEASES { - let what = format!("poetry {release} scan --vendor"); + let what = format!("poetry {release} scan --mode vendored"); let p = Proj::new(); p.write_files(&native_files(release)); p.install(PRISTINE); @@ -1580,7 +1580,8 @@ fn scan_vendor_wiring_attests_without_manifest_or_ledger() { let embedded_doc = p.root.join("embedded.vex.json"); let args = vec![ "scan", - "--vendor", + "--mode", + "vendored", "--vendor-source", "service", "--vex", diff --git a/crates/socket-patch-cli/tests/e2e_vex_lockfile/uv.rs b/crates/socket-patch-cli/tests/e2e_vex_lockfile/uv.rs index a2b120afb..0a1aff5de 100644 --- a/crates/socket-patch-cli/tests/e2e_vex_lockfile/uv.rs +++ b/crates/socket-patch-cli/tests/e2e_vex_lockfile/uv.rs @@ -10,7 +10,7 @@ //! rewriters (`rewrite_python_lock`, `rewrite_project_metadata`, //! `rewrite_script_metadata`) over real uv output (uv 0.11 `uv lock` / //! `uv lock --script` / `uv export --format pylock.toml` grammar), and the -//! writer-driven cells run the real `scan --mode hosted --vex` / `scan --vendor +//! writer-driven cells run the real `scan --mode hosted --vex` / `scan --mode vendored //! --vex` binaries against a wiremock patch API. The package is a made-up //! `vexfixture`, so no interpreter's global site-packages on the test host //! can hold a copy (a no-venv python project falls back to the global @@ -36,7 +36,7 @@ //! installed + patched → attests after hashing; installed pristine → //! `not_applied`; a pin-less hosted entry needs an installed tree. //! -//! Plus the embedded entry points (`scan --mode hosted --vex`, `scan --vendor +//! Plus the embedded entry points (`scan --mode hosted --vex`, `scan --mode vendored //! --vex`, `apply --vex`). use crate::vex_e2e_common; @@ -1368,7 +1368,7 @@ fn pinless_hosted_entry_needs_an_installed_tree() { } // ════════════════════════════════════════════════════════════════════════ -// Writer-driven: the REAL `scan --mode hosted --vex` / `scan --vendor --vex` +// Writer-driven: the REAL `scan --mode hosted --vex` / `scan --mode vendored --vex` // write the wiring and the ledgers; then the manifest (and the ledgers) are // deleted and standalone `vex` must still attest — and stop attesting once // the real revert unwinds the wiring with the ledger left behind. @@ -1631,7 +1631,7 @@ fn scan_redirect_wiring_attests_without_manifest_or_ledger() { } } -/// `scan --vendor --vex --vendor-source build` over the installed (pristine) +/// `scan --mode vendored --vex --vendor-source build` over the installed (pristine) /// dist rebuilds the patched wheel into `.socket/vendor/pypi//`, wires /// the lock, writes the ledger (never a manifest: vendored mode is /// manifest-free) and attests in-run. A legacy manifest seeded beside the @@ -1648,7 +1648,7 @@ fn scan_vendor_wiring_attests_without_manifest_or_ledger() { // uv vendoring always edits the pyproject/lock pair. continue; } - let what = format!("{} scan --vendor", flavor.label()); + let what = format!("{} scan --mode vendored", flavor.label()); let p = Proj::new(); p.write_files(&flavor.native_files()); p.install(flavor.version(), PRISTINE); @@ -1662,7 +1662,8 @@ fn scan_vendor_wiring_attests_without_manifest_or_ledger() { let embedded = p.root.join("embedded.vex.json"); let args = vec![ "scan", - "--vendor", + "--mode", + "vendored", "--vendor-source", "service", "--vex", diff --git a/crates/socket-patch-cli/tests/e2e_vex_lockfile/yarn.rs b/crates/socket-patch-cli/tests/e2e_vex_lockfile/yarn.rs index f40652d88..dc87b825a 100644 --- a/crates/socket-patch-cli/tests/e2e_vex_lockfile/yarn.rs +++ b/crates/socket-patch-cli/tests/e2e_vex_lockfile/yarn.rs @@ -458,7 +458,7 @@ fn write_redirect_ledger(cwd: &Path, flavor: Flavor, rec: PatchRecord) { ); } -/// The vendor ledger `scan --vendor` persists for a yarn project: embedded +/// The vendor ledger `scan --mode vendored` persists for a yarn project: embedded /// record (D9) and the backend's own wiring records. fn write_vendor_ledger(cwd: &Path, flavor: Flavor, rel: &str, rec: PatchRecord) { let wiring_record = |file: &str, kind: &str| WiringRecord { @@ -1322,7 +1322,7 @@ fn pnp_loader_naming_hosted_and_registry_copies_is_not_attested() { } // ────────────────────────────────────────────────────────────────────── -// EMBEDDED — scan --vex / scan --mode hosted --vex / scan --vendor --vex / +// EMBEDDED — scan --vex / scan --mode hosted --vex / scan --mode vendored --vex / // apply --vex, manifest-less // ────────────────────────────────────────────────────────────────────── @@ -1368,7 +1368,7 @@ fn assert_embedded_attested(doc: Option, env: &Value, marker: &str, cell: } /// The in-run VEX of `scan` (a bare scan, which runs hosted mode; the legacy -/// `--mode hosted`; `--vendor`) on an +/// `--mode hosted`; `--mode vendored`) on an /// already-wired, manifest-less checkout attests the lock's patch like the /// standalone command, never rewrites the wiring, never writes a manifest, /// and still refuses a tampered installed tree. @@ -1376,7 +1376,7 @@ fn assert_embedded_attested(doc: Option, env: &Value, marker: &str, cell: fn embedded_scan_vex_attests_manifest_less_wiring() { for flavor in [Flavor::Classic, Flavor::Berry4] { for mode in ["hosted", "vendored"] { - for scan_mode in [None, Some("--mode=hosted"), Some("--vendor")] { + for scan_mode in [None, Some("--mode=hosted"), Some("--mode=vendored")] { let tmp = tempfile::tempdir().unwrap(); let cwd = tmp.path(); if mode == "hosted" { diff --git a/crates/socket-patch-cli/tests/global_scope_project_state.rs b/crates/socket-patch-cli/tests/global_scope_project_state.rs index 8f0f9b945..307287ded 100644 --- a/crates/socket-patch-cli/tests/global_scope_project_state.rs +++ b/crates/socket-patch-cli/tests/global_scope_project_state.rs @@ -99,7 +99,7 @@ fn get_refuses_project_modes_under_global_scope() { assert!(!tmp.path().join(".socket").exists(), "nothing written"); } -/// `scan -g --mode vendored` (and the hidden `--vendor` spelling) is a +/// `scan -g --mode vendored` is a /// usage error like `--mode hosted` already is. #[test] fn scan_refuses_vendored_mode_under_global_scope() { @@ -108,7 +108,6 @@ fn scan_refuses_vendored_mode_under_global_scope() { let prefix = prefix.path().to_str().unwrap(); for (args, flag) in [ (vec!["scan", "--mode", "vendored", "--global"], "--global"), - (vec!["scan", "--vendor", "--global"], "--global"), ( vec!["scan", "--mode", "vendored", "--global-prefix", prefix], "--global-prefix", diff --git a/crates/socket-patch-cli/tests/help_text_hygiene.rs b/crates/socket-patch-cli/tests/help_text_hygiene.rs index 467ed46c5..369297994 100644 --- a/crates/socket-patch-cli/tests/help_text_hygiene.rs +++ b/crates/socket-patch-cli/tests/help_text_hygiene.rs @@ -202,7 +202,7 @@ fn vendor_and_repair_summaries_read_as_one_line() { ); assert!( text.lines().any(|l| l - == " repair Agent mode: download missing patch artifacts and clean up unused ones [aliases: gc]"), + == " repair Agent mode: download missing patch artifacts and clean up unused ones"), "{text}" ); let repair = long_help(&["repair"]); @@ -244,7 +244,7 @@ fn lock_timeout_help_names_get_and_scan() { } /// `-h` stays short (about eight options per page); `--help` still lists -/// every option, and the deprecated `scan --apply`/`--vendor` spellings +/// every option, and the removed `scan --apply`/`--vendor` spellings /// are in neither. #[test] fn short_help_lists_about_eight_options_and_long_help_lists_all() { diff --git a/crates/socket-patch-cli/tests/in_process_agent_reapply.rs b/crates/socket-patch-cli/tests/in_process_agent_reapply.rs index 23a9f0c25..3cfaaa972 100644 --- a/crates/socket-patch-cli/tests/in_process_agent_reapply.rs +++ b/crates/socket-patch-cli/tests/in_process_agent_reapply.rs @@ -52,10 +52,8 @@ fn scan_args(cwd: &Path, server: &MockServer) -> ScanArgs { packages: Vec::new(), common: common(cwd, server), batch_size: Some(100), - apply: false, prune: false, sync: false, - vendor: false, mode: None, all_releases: false, vex: Default::default(), diff --git a/crates/socket-patch-cli/tests/in_process_cargo_apply.rs b/crates/socket-patch-cli/tests/in_process_cargo_apply.rs index fa9021fc3..10c1d0c60 100644 --- a/crates/socket-patch-cli/tests/in_process_cargo_apply.rs +++ b/crates/socket-patch-cli/tests/in_process_cargo_apply.rs @@ -239,10 +239,8 @@ async fn cargo_fetch_scan_sync_patches_real_file() { ..socket_patch_cli::args::GlobalArgs::default() }, batch_size: Some(100), - apply: false, prune: false, sync: true, - vendor: false, mode: None, all_releases: false, vex: Default::default(), @@ -359,10 +357,8 @@ async fn cargo_apply_refuses_on_before_hash_mismatch() { ..socket_patch_cli::args::GlobalArgs::default() }, batch_size: Some(100), - apply: false, prune: false, sync: true, - vendor: false, mode: None, all_releases: false, vex: Default::default(), @@ -458,10 +454,8 @@ async fn cargo_crawler_finds_real_fetched_crate() { ..socket_patch_cli::args::GlobalArgs::default() }, batch_size: Some(100), - apply: false, prune: false, sync: false, - vendor: false, mode: None, all_releases: false, vex: Default::default(), diff --git a/crates/socket-patch-cli/tests/in_process_gem_apply.rs b/crates/socket-patch-cli/tests/in_process_gem_apply.rs index 8efb73927..f9b3d3dcd 100644 --- a/crates/socket-patch-cli/tests/in_process_gem_apply.rs +++ b/crates/socket-patch-cli/tests/in_process_gem_apply.rs @@ -217,10 +217,8 @@ async fn gem_install_scan_sync_patches_real_file() { ..socket_patch_cli::args::GlobalArgs::default() }, batch_size: Some(100), - apply: false, prune: false, sync: true, - vendor: false, mode: None, all_releases: false, vex: Default::default(), @@ -330,10 +328,8 @@ async fn gem_crawler_finds_real_installed_gem() { ..socket_patch_cli::args::GlobalArgs::default() }, batch_size: Some(100), - apply: false, prune: false, sync: false, - vendor: false, mode: None, all_releases: false, vex: Default::default(), diff --git a/crates/socket-patch-cli/tests/in_process_gem_multi_platform.rs b/crates/socket-patch-cli/tests/in_process_gem_multi_platform.rs index be197eec6..68317b9cb 100644 --- a/crates/socket-patch-cli/tests/in_process_gem_multi_platform.rs +++ b/crates/socket-patch-cli/tests/in_process_gem_multi_platform.rs @@ -237,11 +237,9 @@ fn scan_args(cwd: &Path, api_url: String, all_releases: bool) -> ScanArgs { batch_size: Some(100), // apply (not sync) so the post-sync GC doesn't sweep beforeHash // blobs the later rollback/remove needs offline. - apply: true, prune: false, sync: false, - vendor: false, - mode: None, + mode: Some(socket_patch_cli::commands::scan::ScanMode::Agent), all_releases, vex: Default::default(), rollout: Default::default(), diff --git a/crates/socket-patch-cli/tests/in_process_get_modes.rs b/crates/socket-patch-cli/tests/in_process_get_modes.rs index 733ef0f05..248bb1366 100644 --- a/crates/socket-patch-cli/tests/in_process_get_modes.rs +++ b/crates/socket-patch-cli/tests/in_process_get_modes.rs @@ -739,7 +739,7 @@ async fn get_ghsa_hosted_counts_lockfile_resolved_as_present() { } /// The SAME fresh clone in AGENT mode skips the lockfile-only version -/// (scan parity: `--apply` partitions lockfile-only purls out as +/// (scan parity: `--mode agent` partitions lockfile-only purls out as /// `package_not_installed`) — nothing recorded, nothing fetched. #[tokio::test] #[serial] diff --git a/crates/socket-patch-cli/tests/in_process_pypi_apply.rs b/crates/socket-patch-cli/tests/in_process_pypi_apply.rs index 1595f7596..f69383b50 100644 --- a/crates/socket-patch-cli/tests/in_process_pypi_apply.rs +++ b/crates/socket-patch-cli/tests/in_process_pypi_apply.rs @@ -266,10 +266,8 @@ async fn pypi_install_scan_sync_patches_real_file() { ..socket_patch_cli::args::GlobalArgs::default() }, batch_size: Some(100), - apply: false, prune: false, sync: true, - vendor: false, mode: None, all_releases: false, vex: Default::default(), @@ -343,10 +341,8 @@ async fn pypi_scan_then_apply_force_patches_real_file() { ..socket_patch_cli::args::GlobalArgs::default() }, batch_size: Some(100), - apply: false, prune: false, sync: true, - vendor: false, mode: None, all_releases: false, vex: Default::default(), @@ -453,11 +449,9 @@ async fn pypi_apply_dry_run_does_not_modify_file() { ..socket_patch_cli::args::GlobalArgs::default() }, batch_size: Some(100), - apply: true, prune: false, sync: false, - vendor: false, - mode: None, + mode: Some(socket_patch_cli::commands::scan::ScanMode::Agent), all_releases: false, vex: Default::default(), rollout: Default::default(), @@ -468,7 +462,7 @@ async fn pypi_apply_dry_run_does_not_modify_file() { let dry_code = scan_run(scan_args).await; assert_eq!( dry_code, 0, - "scan --apply --dry-run should succeed (exit 0)" + "scan --mode agent --dry-run should succeed (exit 0)" ); let after = std::fs::read(&six_path).expect("read after dry-run"); @@ -510,7 +504,7 @@ async fn pypi_apply_dry_run_does_not_modify_file() { // per-package fetch (`discover_selected`, which runs before the dry-run // gate) proves a real patch was selected before dry-run declined to // write. Hosted mode's `run_redirect` also calls `discover_selected`, so - // this does NOT tell a broken `--apply` → agent fold (which would fall + // this does NOT tell a broken `--mode agent` → agent fold (which would fall // into the hosted default) apart from a working one. assert!( requests.iter().any(|r| r @@ -583,10 +577,8 @@ async fn pypi_crawler_finds_real_installed_six() { ..socket_patch_cli::args::GlobalArgs::default() }, batch_size: Some(100), - apply: false, prune: false, sync: false, - vendor: false, mode: None, all_releases: false, vex: Default::default(), @@ -682,10 +674,8 @@ async fn pypi_scan_sync_patches_egg_info_install() { ..socket_patch_cli::args::GlobalArgs::default() }, batch_size: Some(100), - apply: false, prune: false, sync: true, - vendor: false, mode: None, all_releases: false, vex: Default::default(), diff --git a/crates/socket-patch-cli/tests/in_process_pypi_multi_release.rs b/crates/socket-patch-cli/tests/in_process_pypi_multi_release.rs index bdc63edb6..a6ed3ecec 100644 --- a/crates/socket-patch-cli/tests/in_process_pypi_multi_release.rs +++ b/crates/socket-patch-cli/tests/in_process_pypi_multi_release.rs @@ -315,11 +315,9 @@ fn scan_args(tmp: &Path, api_url: String, all_releases: bool) -> ScanArgs { // from the API. Keeping GC off leaves the before-blobs on disk so // rollback restores offline. (Prune's base-vs-qualified handling // is covered by `detect_prunable` unit tests.) - apply: true, prune: false, sync: false, - vendor: false, - mode: None, + mode: Some(socket_patch_cli::commands::scan::ScanMode::Agent), all_releases, vex: Default::default(), rollout: Default::default(), diff --git a/crates/socket-patch-cli/tests/in_process_python_envs.rs b/crates/socket-patch-cli/tests/in_process_python_envs.rs index a22587fab..38b7fdc1a 100644 --- a/crates/socket-patch-cli/tests/in_process_python_envs.rs +++ b/crates/socket-patch-cli/tests/in_process_python_envs.rs @@ -133,10 +133,8 @@ fn default_args(cwd: &Path, api_url: String) -> ScanArgs { ..socket_patch_cli::args::GlobalArgs::default() }, batch_size: Some(100), - apply: false, prune: false, sync: false, - vendor: false, mode: None, all_releases: false, vex: Default::default(), diff --git a/crates/socket-patch-cli/tests/in_process_redirect.rs b/crates/socket-patch-cli/tests/in_process_redirect.rs index 155219196..50990021e 100644 --- a/crates/socket-patch-cli/tests/in_process_redirect.rs +++ b/crates/socket-patch-cli/tests/in_process_redirect.rs @@ -58,10 +58,8 @@ fn redirect_args(cwd: &Path, api_url: String) -> ScanArgs { ..socket_patch_cli::args::GlobalArgs::default() }, batch_size: Some(100), - apply: false, prune: false, sync: false, - vendor: false, mode: Some(socket_patch_cli::commands::scan::ScanMode::Hosted), all_releases: false, vex: Default::default(), diff --git a/crates/socket-patch-cli/tests/in_process_redirect_pdm.rs b/crates/socket-patch-cli/tests/in_process_redirect_pdm.rs index f74c5c90c..07dc9c557 100644 --- a/crates/socket-patch-cli/tests/in_process_redirect_pdm.rs +++ b/crates/socket-patch-cli/tests/in_process_redirect_pdm.rs @@ -156,10 +156,8 @@ fn hosted_args(cwd: &Path, api_url: String, vex: Option<&Path>) -> ScanArgs { packages: Vec::new(), common: global(cwd, api_url), batch_size: Some(100), - apply: false, prune: false, sync: false, - vendor: false, mode: Some(socket_patch_cli::commands::scan::ScanMode::Hosted), all_releases: false, vex: VexEmbedArgs { diff --git a/crates/socket-patch-cli/tests/in_process_redirect_pipenv.rs b/crates/socket-patch-cli/tests/in_process_redirect_pipenv.rs index 051eaf6c5..f52c6e69d 100644 --- a/crates/socket-patch-cli/tests/in_process_redirect_pipenv.rs +++ b/crates/socket-patch-cli/tests/in_process_redirect_pipenv.rs @@ -91,10 +91,8 @@ fn hosted_args(cwd: &Path, api_url: String, vex: Option<&Path>) -> ScanArgs { packages: Vec::new(), common: global(cwd, api_url), batch_size: Some(100), - apply: false, prune: false, sync: false, - vendor: false, mode: Some(socket_patch_cli::commands::scan::ScanMode::Hosted), all_releases: false, vex: VexEmbedArgs { diff --git a/crates/socket-patch-cli/tests/in_process_redirect_pnpm.rs b/crates/socket-patch-cli/tests/in_process_redirect_pnpm.rs index d6d04d121..d9e701976 100644 --- a/crates/socket-patch-cli/tests/in_process_redirect_pnpm.rs +++ b/crates/socket-patch-cli/tests/in_process_redirect_pnpm.rs @@ -93,10 +93,8 @@ fn hosted_args(cwd: &Path, api_url: String) -> ScanArgs { ..socket_patch_cli::args::GlobalArgs::default() }, batch_size: Some(100), - apply: false, prune: false, sync: false, - vendor: false, mode: Some(ScanMode::Hosted), all_releases: false, vex: Default::default(), diff --git a/crates/socket-patch-cli/tests/in_process_redirect_poetry.rs b/crates/socket-patch-cli/tests/in_process_redirect_poetry.rs index ec348334b..b37795ca1 100644 --- a/crates/socket-patch-cli/tests/in_process_redirect_poetry.rs +++ b/crates/socket-patch-cli/tests/in_process_redirect_poetry.rs @@ -125,10 +125,8 @@ fn hosted_args(cwd: &Path, api_url: String, vex: Option<&Path>) -> ScanArgs { packages: Vec::new(), common: global(cwd, api_url), batch_size: Some(100), - apply: false, prune: false, sync: false, - vendor: false, mode: Some(socket_patch_cli::commands::scan::ScanMode::Hosted), all_releases: false, vex: VexEmbedArgs { diff --git a/crates/socket-patch-cli/tests/in_process_remote_ecosystems_apply.rs b/crates/socket-patch-cli/tests/in_process_remote_ecosystems_apply.rs index b875701c2..e4c951f15 100644 --- a/crates/socket-patch-cli/tests/in_process_remote_ecosystems_apply.rs +++ b/crates/socket-patch-cli/tests/in_process_remote_ecosystems_apply.rs @@ -133,10 +133,8 @@ fn default_scan_args(cwd: &Path, eco: &str, api_url: String) -> ScanArgs { ..socket_patch_cli::args::GlobalArgs::default() }, batch_size: Some(100), - apply: false, prune: false, sync: true, - vendor: false, mode: None, all_releases: false, vex: Default::default(), @@ -211,14 +209,18 @@ async fn setup_apply_mock( /// Read `/.socket/manifest.json`, parse it, and assert the agent-mode /// scan recorded `purl` with `uuid` and at least one patched-file entry. -/// This is the signature the AGENT-mode `--apply`/`--sync` paths must leave +/// This is the signature the AGENT-mode `--mode agent`/`--sync` paths must leave /// behind (manifest + blobs, applied by CI) — a test that only checks the /// on-disk bytes would miss a regression that patches the file but forgets to /// persist the manifest the CI re-apply depends on. fn assert_manifest_records(cwd: &Path, purl: &str, uuid: &str) { let manifest_path = cwd.join(".socket/manifest.json"); - let raw = std::fs::read_to_string(&manifest_path) - .unwrap_or_else(|e| panic!("scan --apply must write {}: {e}", manifest_path.display())); + let raw = std::fs::read_to_string(&manifest_path).unwrap_or_else(|e| { + panic!( + "scan --mode agent must write {}: {e}", + manifest_path.display() + ) + }); let manifest: socket_patch_core::manifest::schema::PatchManifest = serde_json::from_str(&raw).expect("manifest.json parses"); let record = manifest @@ -696,11 +698,11 @@ async fn nuget_handcrafted_discovery() { } // --------------------------------------------------------------------------- -// AGENT-mode `scan --apply` (+ manifest-written) coverage +// AGENT-mode `scan --mode agent` (+ manifest-written) coverage // -// The tests above exercise `scan --sync` (== `--apply --prune`) and assert +// The tests above exercise `scan --sync` (== `--mode agent --prune`) and assert // only the on-disk bytes. These add the missing agent-mode signature checks -// for npm/composer/maven/nuget: the pure `--apply` spelling (no prune) AND an +// for npm/composer/maven/nuget: the pure `--mode agent` spelling (no prune) AND an // explicit assertion that `.socket/manifest.json` was written with the patch // record — the artifact a CI re-apply consumes. Each mock advertises a // `beforeHash` that MATCHES the handcrafted on-disk bytes, so the default @@ -749,15 +751,15 @@ async fn npm_handcrafted_scan_apply_writes_manifest_and_patches() { ) .await; - // `--apply` (NOT `--sync`): the pure agent-mode apply spelling. + // `--mode agent` (NOT `--sync`): the pure agent-mode apply spelling. let mut args = default_scan_args(tmp.path(), "npm", server.uri()); args.common.global = false; // scan cwd-relative node_modules, not $(npm root -g) args.sync = false; - args.apply = true; + args.mode = Some(socket_patch_cli::commands::scan::ScanMode::Agent); let code = scan_run(args).await; assert_eq!( code, 0, - "scan --apply should fully apply the npm patch (exit 0)" + "scan --mode agent should fully apply the npm patch (exit 0)" ); let after = std::fs::read(&index).expect("read after"); @@ -815,11 +817,11 @@ async fn composer_handcrafted_scan_apply_writes_manifest() { let mut args = default_scan_args(tmp.path(), "composer", server.uri()); args.common.global = false; args.sync = false; - args.apply = true; + args.mode = Some(socket_patch_cli::commands::scan::ScanMode::Agent); let code = scan_run(args).await; assert_eq!( code, 0, - "scan --apply should fully apply the composer patch (exit 0)" + "scan --mode agent should fully apply the composer patch (exit 0)" ); let after = std::fs::read(&payload).expect("read after"); @@ -869,11 +871,11 @@ async fn maven_handcrafted_scan_apply_writes_manifest() { // Maven probes MAVEN_REPO_LOCAL under the default `global` bypass. let mut args = default_scan_args(tmp.path(), "maven", server.uri()); args.sync = false; - args.apply = true; + args.mode = Some(socket_patch_cli::commands::scan::ScanMode::Agent); let code = scan_run(args).await; assert_eq!( code, 0, - "scan --apply should fully apply the maven patch (exit 0)" + "scan --mode agent should fully apply the maven patch (exit 0)" ); let after = std::fs::read(&payload_file).expect("read after"); @@ -925,11 +927,11 @@ async fn nuget_handcrafted_scan_apply_writes_manifest() { let mut args = default_scan_args(tmp.path(), "nuget", server.uri()); args.sync = false; - args.apply = true; + args.mode = Some(socket_patch_cli::commands::scan::ScanMode::Agent); let code = scan_run(args).await; assert_eq!( code, 0, - "scan --apply should fully apply the nuget patch (exit 0)" + "scan --mode agent should fully apply the nuget patch (exit 0)" ); let after = std::fs::read(&payload).expect("read after"); diff --git a/crates/socket-patch-cli/tests/in_process_rollback_hosted.rs b/crates/socket-patch-cli/tests/in_process_rollback_hosted.rs index 5db246ae0..6ec4c8e86 100644 --- a/crates/socket-patch-cli/tests/in_process_rollback_hosted.rs +++ b/crates/socket-patch-cli/tests/in_process_rollback_hosted.rs @@ -83,10 +83,8 @@ fn hosted_scan_args(cwd: &Path, api_url: String) -> ScanArgs { ..socket_patch_cli::args::GlobalArgs::default() }, batch_size: Some(100), - apply: false, prune: false, sync: false, - vendor: false, mode: Some(ScanMode::Hosted), all_releases: false, vex: Default::default(), diff --git a/crates/socket-patch-cli/tests/in_process_scan.rs b/crates/socket-patch-cli/tests/in_process_scan.rs index 9fc5cb279..2089818f3 100644 --- a/crates/socket-patch-cli/tests/in_process_scan.rs +++ b/crates/socket-patch-cli/tests/in_process_scan.rs @@ -3,8 +3,8 @@ //! Calls `socket_patch_cli::commands::scan::run` directly so coverage //! is fully instrumented. Mocks the API via wiremock. Hits every flag //! combination that the subprocess-based tests don't explicitly -//! exercise (non-JSON paths, --apply without --prune, --prune without -//! --apply, --batch-size variations). +//! exercise (non-JSON paths, --mode agent without --prune, --prune without +//! --mode agent, --batch-size variations). use std::path::Path; @@ -79,10 +79,8 @@ fn default_args(cwd: &Path) -> ScanArgs { ..socket_patch_cli::args::GlobalArgs::default() }, batch_size: Some(100), - apply: false, prune: false, sync: false, - vendor: false, mode: None, all_releases: false, vex: Default::default(), @@ -292,7 +290,7 @@ async fn scan_installed_package_discovers_patch() { } // --------------------------------------------------------------------------- -// --apply (without --prune) +// --mode agent (without --prune) // --------------------------------------------------------------------------- #[tokio::test] @@ -307,7 +305,7 @@ async fn scan_apply_dry_run_does_not_write() { write_npm_package(tmp.path(), "in-proc-scan", "1.0.0"); let mut args = default_args(tmp.path()); args.common.api_url = Some(server.uri()); - args.apply = true; + args.mode = Some(socket_patch_cli::commands::scan::ScanMode::Agent); args.common.dry_run = true; assert_eq!(run_scrubbed(args).await, 0); @@ -320,12 +318,12 @@ async fn scan_apply_dry_run_does_not_write() { "dry-run must not download/write any blobs" ); // Prove the apply path was actually entered (not short-circuited before - // --apply did anything): a dry-run --apply still fetches patch details + // --mode agent did anything): a dry-run --mode agent still fetches patch details // via the by-package endpoint to synthesize the preview. let reqs = recorded(&server).await; assert!( batch_posts(&reqs).len() == 1 && by_package_gets(&reqs) >= 1, - "dry-run --apply must query batch + patch details; \ + "dry-run --mode agent must query batch + patch details; \ batch={}, by_package={}", batch_posts(&reqs).len(), by_package_gets(&reqs), @@ -345,7 +343,7 @@ async fn scan_apply_wet_writes_manifest_and_blob() { write_npm_package(tmp.path(), "in-proc-scan", "1.0.0"); let mut args = default_args(tmp.path()); args.common.api_url = Some(server.uri()); - args.apply = true; + args.mode = Some(socket_patch_cli::commands::scan::ScanMode::Agent); let code = run_scrubbed(args).await; // Apply over our handcrafted node_modules deterministically reports @@ -484,7 +482,7 @@ async fn scan_apply_picks_critical_over_more_recent_low_for_paid_user() { write_npm_package(tmp.path(), "in-proc-scan", "1.0.0"); let mut args = default_args(tmp.path()); args.common.api_url = Some(server.uri()); - args.apply = true; + args.mode = Some(socket_patch_cli::commands::scan::ScanMode::Agent); run_scrubbed(args).await; @@ -524,7 +522,7 @@ async fn scan_apply_picks_critical_for_free_user_via_ranked_prompt_order() { write_npm_package(tmp.path(), "in-proc-scan", "1.0.0"); let mut args = default_args(tmp.path()); args.common.api_url = Some(server.uri()); - args.apply = true; + args.mode = Some(socket_patch_cli::commands::scan::ScanMode::Agent); run_scrubbed(args).await; @@ -632,7 +630,7 @@ async fn scan_apply_picks_the_more_recently_published_patch_when_severity_ties() write_npm_package(tmp.path(), "in-proc-scan", "1.0.0"); let mut args = default_args(tmp.path()); args.common.api_url = Some(server.uri()); - args.apply = true; + args.mode = Some(socket_patch_cli::commands::scan::ScanMode::Agent); run_scrubbed(args).await; @@ -668,7 +666,7 @@ async fn scan_apply_picks_the_more_recently_published_patch_when_severity_ties() // `scan_invariants::scan_update_candidate_is_the_highest_ranked_patch`. // --------------------------------------------------------------------------- -// --prune (without --apply) +// --prune (without --mode agent) // --------------------------------------------------------------------------- #[tokio::test] @@ -778,7 +776,7 @@ async fn scan_prune_only_wet_removes_orphans() { } // --------------------------------------------------------------------------- -// --sync (== --apply --prune) +// --sync (== --mode agent --prune) // --------------------------------------------------------------------------- #[tokio::test] @@ -797,7 +795,7 @@ async fn scan_sync_full_cycle_against_clean_project() { args.sync = true; let code = run_scrubbed(args).await; - // --sync == --apply --prune; apply over the hash-mismatched fixture file + // --sync == --mode agent --prune; apply over the hash-mismatched fixture file // deterministically partial-fails (exit 1) just like the apply-wet case. assert_eq!( code, 1, @@ -1071,7 +1069,7 @@ async fn scan_apply_all_detail_queries_failed_is_an_error() { write_npm_package(tmp.path(), "in-proc-scan", "1.0.0"); let mut args = default_args(tmp.path()); args.common.api_url = Some(server.uri()); - args.apply = true; + args.mode = Some(socket_patch_cli::commands::scan::ScanMode::Agent); let code = run_scrubbed(args).await; @@ -1081,7 +1079,7 @@ async fn scan_apply_all_detail_queries_failed_is_an_error() { ); assert_ne!( code, 0, - "scan --json --apply must report failure when EVERY patch-detail \ + "scan --json --mode agent must report failure when EVERY patch-detail \ query errors; exit 0 masks a total API outage as 'no patches'" ); } @@ -1477,7 +1475,7 @@ async fn scan_discovers_maven_and_nuget_in_every_mode() { } // --------------------------------------------------------------------------- -// Regression: `scan --vendor --dry-run --vex` must skip the embedded VEX. +// Regression: `scan --mode vendored --dry-run --vex` must skip the embedded VEX. // // The vendor JSON dry-run arm handed base_code 0 straight to // `embed_vex_into_json`, which generated the document for real: on a @@ -1501,7 +1499,7 @@ async fn scan_vendor_dry_run_with_vex_does_not_fail_on_not_yet_vendored() { let vex_path = tmp.path().join("vendor-dry.vex.json"); let mut args = default_args(tmp.path()); args.common.api_url = Some(server.uri()); - args.vendor = true; + args.mode = Some(socket_patch_cli::commands::scan::ScanMode::Vendored); args.common.dry_run = true; args.vex.vex = Some(vex_path.clone()); @@ -1563,7 +1561,7 @@ async fn scan_vendor_dry_run_with_vex_does_not_write_attestation_file() { let vex_path = tmp.path().join("vendor-dry.vex.json"); let mut args = default_args(tmp.path()); args.common.api_url = Some(server.uri()); - args.vendor = true; + args.mode = Some(socket_patch_cli::commands::scan::ScanMode::Vendored); args.common.dry_run = true; args.vex.vex = Some(vex_path.clone()); args.vex.vex_no_verify = true; @@ -1582,7 +1580,7 @@ async fn scan_vendor_dry_run_with_vex_does_not_write_attestation_file() { ); } -/// The INTERACTIVE arm's twin: `scan --vendor --dry-run --vex` without +/// The INTERACTIVE arm's twin: `scan --mode vendored --dry-run --vex` without /// `--json` returns through `embed_vex_human`, which generated (and wrote) /// the document for real — exit 1 on a not-yet-vendored project, an /// attestation file on disk otherwise. The dry-run guard lives in the embed @@ -1602,7 +1600,7 @@ async fn scan_vendor_dry_run_with_vex_interactive_does_not_fail_or_write() { let mut args = default_args(tmp.path()); args.common.api_url = Some(server.uri()); args.common.json = false; - args.vendor = true; + args.mode = Some(socket_patch_cli::commands::scan::ScanMode::Vendored); args.common.dry_run = true; args.vex.vex = Some(vex_path.clone()); @@ -1617,7 +1615,7 @@ async fn scan_vendor_dry_run_with_vex_interactive_does_not_fail_or_write() { ); } -/// `scan --apply --json --dry-run --vex`: the JSON apply arm synthesizes its +/// `scan --mode agent --json --dry-run --vex`: the JSON apply arm synthesizes its /// preview and falls through to `embed_vex_into_json` with apply_code 0, so /// without the guard the dry run generated and wrote the attestation. #[tokio::test] @@ -1655,7 +1653,7 @@ async fn scan_apply_json_dry_run_with_vex_does_not_write_attestation() { let vex_path = tmp.path().join("apply-dry.vex.json"); let mut args = default_args(tmp.path()); args.common.api_url = Some(server.uri()); - args.apply = true; + args.mode = Some(socket_patch_cli::commands::scan::ScanMode::Agent); args.common.dry_run = true; args.vex.vex = Some(vex_path.clone()); args.vex.vex_no_verify = true; diff --git a/crates/socket-patch-cli/tests/in_process_vendor.rs b/crates/socket-patch-cli/tests/in_process_vendor.rs index 759f0c20a..26f566a13 100644 --- a/crates/socket-patch-cli/tests/in_process_vendor.rs +++ b/crates/socket-patch-cli/tests/in_process_vendor.rs @@ -1,6 +1,6 @@ //! In-process + envelope contract tests for `socket-patch vendor` (npm //! backend, plus the golang apply-yields-to-vendor handshake, plus the gem -//! backend's `scan --vendor` arm — the one route into the vendor engine no +//! backend's `scan --mode vendored` arm — the one route into the vendor engine no //! gem project had ever been driven through). The vlt legs live in //! `in_process_vendor/vlt.rs`. //! @@ -716,7 +716,7 @@ async fn reconcile_leaves_detached_entries_alone() { // ───────────────────────────────────────────────────────────────────── /// Re-vendoring after the manifest moved to a newer patch uuid (the -/// `scan --vendor` auto-update path) must (a) rewire the lock at the new +/// `scan --mode vendored` auto-update path) must (a) rewire the lock at the new /// uuid, (b) remove the old uuid's now-orphaned artifact dir, and (c) carry /// the pre-vendor lock fragment forward so a later `--revert` still /// restores the registry spelling byte-for-byte. @@ -2887,7 +2887,7 @@ async fn offline_service_mode_refuses_instead_of_building() { } // ───────────────────────────────────────────────────────────────────── -// 13. gem through `scan --vendor` (mock-proxy API, hermetic bundler layout) +// 13. gem through `scan --mode vendored` (mock-proxy API, hermetic bundler layout) // ───────────────────────────────────────────────────────────────────── const GEM_UUID: &str = "35353535-3535-4335-8335-353535353535"; @@ -2898,7 +2898,7 @@ const GEM_PURL: &str = "pkg:gem/demo-gem@1.0.0"; const GEM_PURL_QUALIFIED: &str = "pkg:gem/demo-gem@1.0.0?platform=ruby"; const GEM_ORIG: &[u8] = b"module DemoGem\n STATUS = \"orig\"\nend\n"; const GEM_PATCHED: &[u8] = b"module DemoGem\n STATUS = \"patched\"\nend\n"; -const GEM_GEMSPEC: &str = "Gem::Specification.new do |s|\n s.name = \"demo-gem\"\n s.version = \"1.0.0\"\n s.summary = \"in-process scan --vendor fixture\"\n s.authors = [\"socket-patch e2e\"]\n s.require_paths = [\"lib\"]\nend\n"; +const GEM_GEMSPEC: &str = "Gem::Specification.new do |s|\n s.name = \"demo-gem\"\n s.version = \"1.0.0\"\n s.summary = \"in-process scan --mode vendored fixture\"\n s.authors = [\"socket-patch e2e\"]\n s.require_paths = [\"lib\"]\nend\n"; const GEM_GEMFILE: &str = "source \"https://rubygems.org\"\n\ngem \"demo-gem\", \"~> 1.0\"\n"; /// Hand-pinned bundler lock grammar (no CHECKSUMS — the 2.x/3.x default). const GEM_LOCK: &str = "GEM\n remote: https://rubygems.org/\n specs:\n demo-gem (1.0.0)\n\nPLATFORMS\n ruby\n\nDEPENDENCIES\n demo-gem (~> 1.0)\n\nBUNDLED WITH\n 2.6.2\n"; @@ -2954,7 +2954,7 @@ fn gem_fixture() -> GemFixture { } /// Mount discovery (batch), per-package search, and the full view (inline -/// `blobContent`, so `scan --vendor` runs against the mock alone) for the +/// `blobContent`, so `scan --mode vendored` runs against the mock alone) for the /// demo gem — the gem mirror of `scan_vendor_e2e::mount_patch_api`. /// /// `patch_purl` is the purl the SERVED patch records carry ([`GEM_PURL`] or @@ -3067,7 +3067,8 @@ fn run_scan_vendor(root: &Path, mock_uri: &str, extra: &[&str]) -> (i32, Value) let mut argv = vec![ "scan", "--json", - "--vendor", + "--mode", + "vendored", "--yes", "--api-url", mock_uri, @@ -3081,12 +3082,14 @@ fn run_scan_vendor(root: &Path, mock_uri: &str, extra: &[&str]) -> (i32, Value) argv.extend_from_slice(extra); let (code, stdout, stderr) = run_cli(root, &argv, &[]); let env: Value = serde_json::from_str(stdout.trim()).unwrap_or_else(|e| { - panic!("scan --vendor --json must emit JSON: {e}\nstdout:\n{stdout}\nstderr:\n{stderr}") + panic!( + "scan --mode vendored --json must emit JSON: {e}\nstdout:\n{stdout}\nstderr:\n{stderr}" + ) }); (code, env) } -/// `scan --vendor` end to end on a gem project: discover → download → +/// `scan --mode vendored` end to end on a gem project: discover → download → /// vendor lands the gem pair edit (Gemfile pin + `path:`, lock PATH section /// + `(= …)!` DEPENDENCIES pin) and the patched artifact dir, keyed in the /// ledger by the gem purl; a re-run is an `already_vendored` no-op; and @@ -3101,7 +3104,7 @@ async fn scan_vendor_gem_end_to_end_and_reverts() { let fx = gem_fixture(); let (code, env) = run_scan_vendor(fx.root(), &mock.uri(), &[]); - assert_eq!(code, 0, "scan --vendor must succeed: {env:#}"); + assert_eq!(code, 0, "scan --mode vendored must succeed: {env:#}"); assert_eq!(env["status"], "success", "envelope: {env:#}"); assert_eq!(env["download"]["downloaded"], 1, "envelope: {env:#}"); assert_eq!(env["vendor"]["summary"]["applied"], 1, "envelope: {env:#}"); @@ -3269,7 +3272,7 @@ async fn scan_vendor_gem_qualified_platform_ruby_purl_vendors() { let (code, env) = run_scan_vendor(fx.root(), &mock.uri(), &[]); assert_eq!( code, 0, - "scan --vendor must succeed on the qualified purl: {env:#}" + "scan --mode vendored must succeed on the qualified purl: {env:#}" ); assert_eq!(env["status"], "success", "envelope: {env:#}"); assert_eq!(env["download"]["downloaded"], 1, "envelope: {env:#}"); @@ -3343,7 +3346,7 @@ async fn scan_vendor_gem_qualified_platform_ruby_purl_vendors() { ); } -/// The QUALIFIED purl through `scan --vendor` + `vendor --revert`: a detached +/// The QUALIFIED purl through `scan --mode vendored` + `vendor --revert`: a detached /// ledger entry has NO manifest fallback, so the revert must find it via its /// own key/`basePurl` alone. The bare-purl detached shape is covered by /// [`scan_vendor_gem_detached_writes_no_manifest_and_reverts`]; this pins @@ -3357,7 +3360,7 @@ async fn scan_vendor_gem_detached_qualified_purl_reverts() { let (code, env) = run_scan_vendor(fx.root(), &mock.uri(), &[]); assert_eq!( code, 0, - "scan --vendor must succeed on the qualified purl: {env:#}" + "scan --mode vendored must succeed on the qualified purl: {env:#}" ); assert_eq!(env["vendor"]["summary"]["applied"], 1, "envelope: {env:#}"); @@ -3394,7 +3397,7 @@ async fn scan_vendor_gem_detached_qualified_purl_reverts() { assert!(!fx.root().join(".socket/vendor").exists()); } -/// `scan --vendor` on the gem project: no manifest is written, +/// `scan --mode vendored` on the gem project: no manifest is written, /// the ledger entry is detached with the patch record embedded, the pair /// edit still lands — and `vendor --revert` (the detached entry's only exit /// path) byte-restores both files. @@ -3405,7 +3408,7 @@ async fn scan_vendor_gem_detached_writes_no_manifest_and_reverts() { let fx = gem_fixture(); let (code, env) = run_scan_vendor(fx.root(), &mock.uri(), &[]); - assert_eq!(code, 0, "scan --vendor must succeed: {env:#}"); + assert_eq!(code, 0, "scan --mode vendored must succeed: {env:#}"); assert_eq!(env["vendor"]["summary"]["applied"], 1, "envelope: {env:#}"); assert!( @@ -3736,10 +3739,8 @@ snapshots: ..GlobalArgs::default() }, batch_size: Some(100), - apply: false, prune: false, sync: false, - vendor: false, mode: Some(ScanMode::Hosted), all_releases: false, vex: Default::default(), diff --git a/crates/socket-patch-cli/tests/in_process_vendor_bun_takeover.rs b/crates/socket-patch-cli/tests/in_process_vendor_bun_takeover.rs index 45fdb940f..67b6a9576 100644 --- a/crates/socket-patch-cli/tests/in_process_vendor_bun_takeover.rs +++ b/crates/socket-patch-cli/tests/in_process_vendor_bun_takeover.rs @@ -524,7 +524,7 @@ async fn bun_hosted_then_scan_vendored_takeover_round_trips_to_registry() { // The scan-side vendored preview is ledger-only by contract // (`would_vendor` / `already_vendored` / `would_revendor`, CLI_CONTRACT - // "scan --vendor"): it must at least not fail and not write anything. + // "scan --mode vendored"): it must at least not fail and not write anything. let (code, preview) = scan_mode(root, &server.uri(), "vendored", &["--dry-run"]); assert_eq!(code, 0, "vendored preview must succeed: {preview:#}"); assert_eq!( diff --git a/crates/socket-patch-cli/tests/remove/remove_invariants.rs b/crates/socket-patch-cli/tests/remove/remove_invariants.rs index abde1d328..cc774c433 100644 --- a/crates/socket-patch-cli/tests/remove/remove_invariants.rs +++ b/crates/socket-patch-cli/tests/remove/remove_invariants.rs @@ -475,7 +475,7 @@ const VENDORED_PURL: &str = "pkg:npm/__remove_vendored__@1.0.0"; const MANIFEST_UUID: &str = "55555555-5555-4555-8555-555555555555"; /// The generation that was actually vendored — one behind the manifest. /// This is the documented `vendor_uuid_mismatch` state: `get` / `scan -/// --apply` refreshed the manifest record while the re-vendor is still +/// --mode agent` refreshed the manifest record while the re-vendor is still /// pending (repair reports it and declines to cross patch generations). const LEDGER_UUID: &str = "66666666-6666-4666-8666-666666666666"; diff --git a/crates/socket-patch-cli/tests/repair/coverage_fix_repair_vendor_predelete.rs b/crates/socket-patch-cli/tests/repair/coverage_fix_repair_vendor_predelete.rs index 72362c0a2..af5116321 100644 --- a/crates/socket-patch-cli/tests/repair/coverage_fix_repair_vendor_predelete.rs +++ b/crates/socket-patch-cli/tests/repair/coverage_fix_repair_vendor_predelete.rs @@ -187,9 +187,9 @@ fn events_of(v: &serde_json::Value) -> Vec { v["events"].as_array().cloned().unwrap_or_default() } -/// `scan --vendor --yes` the gem fixture; returns the vendored copy dir. +/// `scan --mode vendored --yes` the gem fixture; returns the vendored copy dir. fn vendor_gem_project(root: &Path, mock_uri: &str) -> PathBuf { - let (code, stdout, stderr) = run_cli(root, mock_uri, &["scan", "--vendor", "--yes"]); + let (code, stdout, stderr) = run_cli(root, mock_uri, &["scan", "--mode", "vendored", "--yes"]); assert_eq!(code, 0, "gem vendor setup failed: {stdout} {stderr}"); let copy = root.join(gem_copy_rel()); assert_eq!( diff --git a/crates/socket-patch-cli/tests/repair/covgap_commands_repair.rs b/crates/socket-patch-cli/tests/repair/covgap_commands_repair.rs index c00520fd4..ae6a32db2 100644 --- a/crates/socket-patch-cli/tests/repair/covgap_commands_repair.rs +++ b/crates/socket-patch-cli/tests/repair/covgap_commands_repair.rs @@ -750,7 +750,7 @@ async fn repair_redownload_human_mode_prints_summary() { let (code, stdout, stderr) = run_cli( tmp.path(), &mock.uri(), - &["scan", "--vendor", "--yes"], + &["scan", "--mode", "vendored", "--yes"], true, ); assert_eq!( diff --git a/crates/socket-patch-cli/tests/repair/covgap_commands_repair_vendor.rs b/crates/socket-patch-cli/tests/repair/covgap_commands_repair_vendor.rs index c5ebab59f..7d4bbc49a 100644 --- a/crates/socket-patch-cli/tests/repair/covgap_commands_repair_vendor.rs +++ b/crates/socket-patch-cli/tests/repair/covgap_commands_repair_vendor.rs @@ -7,7 +7,7 @@ //! Fixtures and helpers mirror `repair_vendor_e2e.rs` (this suite owns its //! own copies). //! -//! A vendored run (`scan --vendor`) is manifest-free: every ledger entry is +//! A vendored run (`scan --mode vendored`) is manifest-free: every ledger entry is //! `detached` with its record embedded and `.socket/manifest.json` is never //! written. The manifest-backed repair arms (dropped / moved-on manifest //! records, the `(None, None)` uuid recovery) belong to LEGACY manifest-mode projects, which the tests @@ -406,19 +406,19 @@ fn run_cli_human(root: &Path, mock_uri: &str, argv: &[&str]) -> (i32, String, St run_cli_with(root, mock_uri, argv, false, &[]) } -/// `scan --vendor --yes` to establish a vendored npm project; returns the +/// `scan --mode vendored --yes` to establish a vendored npm project; returns the /// vendored tarball path. fn vendor_project(root: &Path, mock_uri: &str) -> PathBuf { - let (code, stdout, stderr) = run_cli(root, mock_uri, &["scan", "--vendor", "--yes"]); + let (code, stdout, stderr) = run_cli(root, mock_uri, &["scan", "--mode", "vendored", "--yes"]); assert_eq!(code, 0, "vendor setup failed: {stdout} {stderr}"); let tgz = root.join(format!(".socket/vendor/npm/{UUID}/left-pad-1.3.0.tgz")); assert!(tgz.is_file(), "setup must vendor the tarball"); tgz } -/// `scan --vendor --yes` the gem fixture; returns the vendored copy dir. +/// `scan --mode vendored --yes` the gem fixture; returns the vendored copy dir. fn vendor_gem_project(root: &Path, mock_uri: &str, expected_after: &[u8]) -> PathBuf { - let (code, stdout, stderr) = run_cli(root, mock_uri, &["scan", "--vendor", "--yes"]); + let (code, stdout, stderr) = run_cli(root, mock_uri, &["scan", "--mode", "vendored", "--yes"]); assert_eq!(code, 0, "gem vendor setup failed: {stdout} {stderr}"); let copy = root.join(gem_copy_rel()); assert_eq!( @@ -453,7 +453,7 @@ fn write_state(root: &Path, state: &serde_json::Value) { /// Hand-migrate the vendored fixture to the LEGACY manifest-mode shape: every /// ledger entry's embedded record moves into `.socket/manifest.json` (keyed /// by the ledger key) and the entry loses `detached` + `record` — exactly -/// what a pre-D2 `scan --vendor` (or a standalone `vendor` from a manifest) +/// what a pre-D2 `scan --mode vendored` (or a standalone `vendor` from a manifest) /// left behind. Returns the manifest path. fn to_legacy_manifest_mode(root: &Path) -> PathBuf { let mut state = read_state(root); @@ -1082,7 +1082,7 @@ async fn repair_rebuilds_qualified_ledger_key_from_installed_copy() { let copy = vendor_gem_project(tmp.path(), &mock.uri(), AFTER); // Re-key the ledger entry to the qualified spelling (`basePurl` stays - // bare — exactly what `scan --vendor` records for a served qualified + // bare — exactly what `scan --mode vendored` records for a served qualified // purl). let mut state = read_state(tmp.path()); let entry = state["entries"] @@ -1466,7 +1466,11 @@ async fn repair_recovers_multiple_records_by_uuid_sharing_one_client() { "sha512-orig==", ); write_gem_fixture(tmp.path(), false); - let (code, stdout, stderr) = run_cli(tmp.path(), &mock.uri(), &["scan", "--vendor", "--yes"]); + let (code, stdout, stderr) = run_cli( + tmp.path(), + &mock.uri(), + &["scan", "--mode", "vendored", "--yes"], + ); assert_eq!(code, 0, "combined vendor setup failed: {stdout} {stderr}"); let tgz = tmp .path() diff --git a/crates/socket-patch-cli/tests/repair/repair_invariants.rs b/crates/socket-patch-cli/tests/repair/repair_invariants.rs index 9467b068f..4f69c3f20 100644 --- a/crates/socket-patch-cli/tests/repair/repair_invariants.rs +++ b/crates/socket-patch-cli/tests/repair/repair_invariants.rs @@ -740,42 +740,6 @@ fn repair_deletes_lock_file_even_when_repair_fails() { ); } -// --------------------------------------------------------------------------- -// gc alias parity -// --------------------------------------------------------------------------- - -#[test] -fn gc_alias_behaves_identically_to_repair() { - let tmp = tempfile::tempdir().expect("tempdir"); - let socket = make_socket_dir(tmp.path()); - write_blob(&socket, REFERENCED_HASH, b"patched content"); - let orphan_hash = "abadcafe".repeat(8); - write_blob(&socket, &orphan_hash, b"orphaned content"); - - // Run via `gc` instead of `repair`. - let out = socket_cmd(tmp.path()) - .args(["gc", "--json", "--offline"]) - .output() - .expect("run socket-patch"); - assert_eq!(out.status.code(), Some(0)); - let v: serde_json::Value = serde_json::from_str(&String::from_utf8_lossy(&out.stdout)).unwrap(); - // The envelope's `command` field reports the canonical name, not the alias. - assert_eq!(v["command"], "repair"); - assert_eq!(v["status"], "success"); - // Full parity with `repair_offline_removes_orphan_blob`: the orphan is - // swept, the referenced blob survives, and nothing is downloaded offline. - assert_eq!(v["summary"]["removed"], 1); - assert_eq!(v["summary"]["downloaded"], 0); - assert!( - !socket.join("blobs").join(&orphan_hash).exists(), - "gc must remove the orphan just like repair" - ); - assert!( - socket.join("blobs").join(REFERENCED_HASH).exists(), - "gc must keep the referenced blob just like repair" - ); -} - // --------------------------------------------------------------------------- // Manifest-path override // --------------------------------------------------------------------------- diff --git a/crates/socket-patch-cli/tests/repair/repair_vendor_e2e.rs b/crates/socket-patch-cli/tests/repair/repair_vendor_e2e.rs index 21bb9c422..131aa524e 100644 --- a/crates/socket-patch-cli/tests/repair/repair_vendor_e2e.rs +++ b/crates/socket-patch-cli/tests/repair/repair_vendor_e2e.rs @@ -210,10 +210,10 @@ fn run_cli(root: &Path, mock_uri: &str, argv: &[&str]) -> (i32, String, String) ) } -/// `scan --vendor --yes` to establish a vendored project; returns the +/// `scan --mode vendored --yes` to establish a vendored project; returns the /// vendored tarball path. fn vendor_project(root: &Path, mock_uri: &str, extra: &[&str]) -> PathBuf { - let mut argv = vec!["scan", "--vendor", "--yes"]; + let mut argv = vec!["scan", "--mode", "vendored", "--yes"]; argv.extend_from_slice(extra); let (code, stdout, stderr) = run_cli(root, mock_uri, &argv); assert_eq!(code, 0, "vendor setup failed: {stdout} {stderr}"); @@ -937,9 +937,9 @@ fn add_healthy_cargo_dir_entry(root: &Path) -> PathBuf { dir } -/// `scan --vendor --yes` the gem fixture; returns the vendored copy dir. +/// `scan --mode vendored --yes` the gem fixture; returns the vendored copy dir. fn vendor_gem_project(root: &Path, mock_uri: &str) -> PathBuf { - let (code, stdout, stderr) = run_cli(root, mock_uri, &["scan", "--vendor", "--yes"]); + let (code, stdout, stderr) = run_cli(root, mock_uri, &["scan", "--mode", "vendored", "--yes"]); assert_eq!(code, 0, "gem vendor setup failed: {stdout} {stderr}"); let copy = root.join(gem_copy_rel()); assert_eq!( diff --git a/crates/socket-patch-cli/tests/repair/repair_vendor_flavors_e2e.rs b/crates/socket-patch-cli/tests/repair/repair_vendor_flavors_e2e.rs index aa0287f29..699b79d78 100644 --- a/crates/socket-patch-cli/tests/repair/repair_vendor_flavors_e2e.rs +++ b/crates/socket-patch-cli/tests/repair/repair_vendor_flavors_e2e.rs @@ -13,7 +13,7 @@ //! reference (`scan_vendor_references` tokenizes the pnpm/yarn/bun //! locks) is reported as `vendor_ledger_missing`, never reconstructed. //! -//! The fixtures run the ACTUAL `scan --vendor` flow in-test the way the +//! The fixtures run the ACTUAL `scan --mode vendored` flow in-test the way the //! capstones stage it — a hand-written flavor lock (the pre-vendor shape each //! backend's capstone asserts) plus an installed `node_modules/` copy, //! driven through the built binary against a mock API (no real package @@ -418,12 +418,12 @@ fn run_cli(root: &Path, mock_uri: &str, argv: &[&str]) -> (i32, String, String) common::run_with_env(root, &full, &[("SOCKET_TELEMETRY_DISABLED", "1")]) } -/// `scan --vendor --yes` to establish a vendored flavor project; returns the +/// `scan --mode vendored --yes` to establish a vendored flavor project; returns the /// vendored tarball path (identical layout for every npm flavor). A v0/v1 /// bun workspace shape gains its workspace member AFTER vendoring — the only /// way such a lock arises (a fresh vendor into it is refused by design). fn vendor_project(root: &Path, mock_uri: &str, flavor: Flavor) -> PathBuf { - let (code, stdout, stderr) = run_cli(root, mock_uri, &["scan", "--vendor", "--yes"]); + let (code, stdout, stderr) = run_cli(root, mock_uri, &["scan", "--mode", "vendored", "--yes"]); assert_eq!( code, 0, diff --git a/crates/socket-patch-cli/tests/repair_vendor_flavors_e2e/vlt.rs b/crates/socket-patch-cli/tests/repair_vendor_flavors_e2e/vlt.rs index 9c08880b2..7a3770e45 100644 --- a/crates/socket-patch-cli/tests/repair_vendor_flavors_e2e/vlt.rs +++ b/crates/socket-patch-cli/tests/repair_vendor_flavors_e2e/vlt.rs @@ -77,9 +77,9 @@ fn write_fixture(root: &Path, lock: VltLock, package_json: Option<&str>) { std::fs::write(pkg.join("index.js"), BEFORE).unwrap(); } -/// `scan --vendor --yes` over the fixture; returns the artifact dir. +/// `scan --mode vendored --yes` over the fixture; returns the artifact dir. fn vendor_project(root: &Path, uri: &str, lock: VltLock) -> PathBuf { - let (code, stdout, stderr) = run_cli(root, uri, &["scan", "--vendor", "--yes"]); + let (code, stdout, stderr) = run_cli(root, uri, &["scan", "--mode", "vendored", "--yes"]); assert_eq!(code, 0, "{lock:?}: vendor setup: {stdout}\n{stderr}"); let dir = root.join(rel()); assert!(dir.join("index.js").is_file(), "{lock:?}: {stdout}"); diff --git a/crates/socket-patch-cli/tests/scan/covgap_commands_scan_vendor_flow.rs b/crates/socket-patch-cli/tests/scan/covgap_commands_scan_vendor_flow.rs index 5190f1fd4..9770945e2 100644 --- a/crates/socket-patch-cli/tests/scan/covgap_commands_scan_vendor_flow.rs +++ b/crates/socket-patch-cli/tests/scan/covgap_commands_scan_vendor_flow.rs @@ -195,7 +195,8 @@ fn run_scan_vendor(root: &Path, mock_uri: &str, extra: &[&str]) -> (i32, String, let mut argv = vec![ "scan", "--json", - "--vendor", + "--mode", + "vendored", "--yes", "--api-url", mock_uri, @@ -355,8 +356,8 @@ async fn scan_vendor_dry_run_reports_already_vendored() { ); } -/// `scan --json --vendor --dry-run --prune` (a legal combination — -/// `--vendor` conflicts only with `--apply`/`--sync`): the vendor JSON +/// `scan --json --mode vendored --dry-run --prune` (a legal combination — +/// `--mode vendored` conflicts only with `--mode agent`/`--sync`): the vendor JSON /// path's dry-run arm must emit the GC PREVIEW (`prunable*`/`orphan*` /// field names, per `to_preview_json`) and mutate nothing on disk. #[tokio::test] diff --git a/crates/socket-patch-cli/tests/scan/scan_invariants.rs b/crates/socket-patch-cli/tests/scan/scan_invariants.rs index a7e321129..f2b3b6140 100644 --- a/crates/socket-patch-cli/tests/scan/scan_invariants.rs +++ b/crates/socket-patch-cli/tests/scan/scan_invariants.rs @@ -297,7 +297,7 @@ async fn scan_emits_updates_entry_when_newer_uuid_available() { #[tokio::test] async fn scan_update_candidate_is_the_highest_ranked_patch() { - // `updates[].newUuid` must name the patch `--apply` would install — + // `updates[].newUuid` must name the patch `--mode agent` would install — // the highest-ranked one (severity → advisory count → recency), NOT whatever // the server listed first. The two are computed by different code over // different API shapes (`detect_updates` over the batch response, @@ -427,7 +427,7 @@ async fn scan_emits_updates_entry_for_scoped_purl_despite_manifest_percent_encod let tmp = tempfile::tempdir().expect("tempdir"); write_root_package_json(tmp.path()); write_npm_package(tmp.path(), "@scope/left-pad", "1.3.0"); - // Manifest keyed by the ENCODED purl — exactly what `get`/`scan --apply` + // Manifest keyed by the ENCODED purl — exactly what `get`/`scan --mode agent` // write for a scoped package. let socket = tmp.path().join(".socket"); std::fs::create_dir_all(&socket).unwrap(); @@ -555,7 +555,7 @@ async fn scan_without_prune_omits_gc_field() { // --------------------------------------------------------------------------- // --------------------------------------------------------------------------- -// --apply --dry-run — synthesizes per-patch actions without writing +// --mode agent --dry-run — synthesizes per-patch actions without writing // --------------------------------------------------------------------------- #[tokio::test] @@ -584,7 +584,7 @@ async fn scan_apply_dry_run_with_empty_manifest_emits_added_action() { }))) .mount(&mock) .await; - // by-package search (used by --apply mode for full PatchSearchResult) + // by-package search (used by --mode agent mode for full PatchSearchResult) Mock::given(method("GET")) .and(path(format!( "/v0/orgs/{ORG_SLUG}/patches/by-package/pkg%3Anpm%2Fminimist%401.2.2" @@ -608,17 +608,20 @@ async fn scan_apply_dry_run_with_empty_manifest_emits_added_action() { write_root_package_json(tmp.path()); write_npm_package(tmp.path(), "minimist", "1.2.2"); - let (code, stdout, stderr) = - run_scan(tmp.path(), &mock.uri(), &["--apply", "--dry-run", "--yes"]); + let (code, stdout, stderr) = run_scan( + tmp.path(), + &mock.uri(), + &["--mode", "agent", "--dry-run", "--yes"], + ); assert_eq!( code, 0, - "scan --apply --dry-run must succeed; stdout={stdout}; stderr={stderr}" + "scan --mode agent --dry-run must succeed; stdout={stdout}; stderr={stderr}" ); let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("valid JSON"); assert_eq!(v["status"], "success"); let apply = v["apply"] .as_object() - .expect("apply object present in --apply mode"); + .expect("apply object present in --mode agent mode"); assert_eq!(apply["dryRun"], true); assert_eq!(apply["found"], 1); assert_eq!(apply["added"], 1); @@ -633,10 +636,10 @@ async fn scan_apply_dry_run_with_empty_manifest_emits_added_action() { // CRITICAL: dry-run must not write the manifest. assert!( !tmp.path().join(".socket/manifest.json").exists(), - "scan --apply --dry-run must not write .socket/manifest.json" + "scan --mode agent --dry-run must not write .socket/manifest.json" ); - // --apply mode must query BOTH endpoints: the batch search (carrying + // --mode agent mode must query BOTH endpoints: the batch search (carrying // the crawled PURL) and the per-package detail fetch. The "added" // action above is only trustworthy if it was synthesized from a real // detail fetch, not fabricated. @@ -644,7 +647,7 @@ async fn scan_apply_dry_run_with_empty_manifest_emits_added_action() { assert_single_batch_carries_purl(&reqs, purl); assert!( by_package_gets(&reqs) >= 1, - "scan --apply must fetch per-package patch details; saw {} by-package GET(s)", + "scan --mode agent must fetch per-package patch details; saw {} by-package GET(s)", by_package_gets(&reqs) ); } @@ -696,7 +699,7 @@ async fn scan_apply_dry_run_with_existing_uuid_emits_skipped_action() { let tmp = tempfile::tempdir().expect("tempdir"); write_root_package_json(tmp.path()); write_npm_package(tmp.path(), "minimist", "1.2.2"); - // Manifest already has the SAME UUID — scan --apply must skip it. + // Manifest already has the SAME UUID — scan --mode agent must skip it. let socket = tmp.path().join(".socket"); std::fs::create_dir_all(&socket).unwrap(); std::fs::write( @@ -719,7 +722,11 @@ async fn scan_apply_dry_run_with_existing_uuid_emits_skipped_action() { ) .unwrap(); - let (code, stdout, _) = run_scan(tmp.path(), &mock.uri(), &["--apply", "--dry-run", "--yes"]); + let (code, stdout, _) = run_scan( + tmp.path(), + &mock.uri(), + &["--mode", "agent", "--dry-run", "--yes"], + ); assert_eq!(code, 0); let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("valid JSON"); let apply = &v["apply"]; @@ -733,7 +740,7 @@ async fn scan_apply_dry_run_with_existing_uuid_emits_skipped_action() { assert_single_batch_carries_purl(&reqs, purl); assert!( by_package_gets(&reqs) >= 1, - "scan --apply must fetch per-package patch details; saw {} by-package GET(s)", + "scan --mode agent must fetch per-package patch details; saw {} by-package GET(s)", by_package_gets(&reqs) ); } @@ -808,7 +815,11 @@ async fn scan_apply_dry_run_with_different_uuid_emits_updated_action() { ) .unwrap(); - let (code, stdout, _) = run_scan(tmp.path(), &mock.uri(), &["--apply", "--dry-run", "--yes"]); + let (code, stdout, _) = run_scan( + tmp.path(), + &mock.uri(), + &["--mode", "agent", "--dry-run", "--yes"], + ); assert_eq!(code, 0); let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("valid JSON"); let apply = &v["apply"]; @@ -824,7 +835,7 @@ async fn scan_apply_dry_run_with_different_uuid_emits_updated_action() { assert_single_batch_carries_purl(&reqs, purl); assert!( by_package_gets(&reqs) >= 1, - "scan --apply must fetch per-package patch details; saw {} by-package GET(s)", + "scan --mode agent must fetch per-package patch details; saw {} by-package GET(s)", by_package_gets(&reqs) ); } @@ -1049,7 +1060,7 @@ async fn mount_batch_ok_details_500(mock: &MockServer, purl: &str, uuid: &str) { /// failures honor that — `--offline` and the all-batches-failed bail both /// print `{"status": "error", "error": ...}`. The all-detail-queries-failed /// bail did not: it returned exit 1 straight out of `discover_selected` -/// with EMPTY stdout, so a bot parsing `scan --json --apply` got a JSON +/// with EMPTY stdout, so a bot parsing `scan --json --mode agent` got a JSON /// parse error instead of a diagnosable failure envelope. #[tokio::test] async fn scan_apply_all_detail_queries_failed_emits_json_error_envelope() { @@ -1061,11 +1072,11 @@ async fn scan_apply_all_detail_queries_failed_emits_json_error_envelope() { write_root_package_json(tmp.path()); write_npm_package(tmp.path(), "minimist", "1.2.2"); - let (code, stdout, stderr) = run_scan(tmp.path(), &mock.uri(), &["--apply", "--yes"]); + let (code, stdout, stderr) = run_scan(tmp.path(), &mock.uri(), &["--mode", "agent", "--yes"]); let v: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap_or_else(|e| { panic!( - "scan --json --apply must emit a JSON envelope even when every \ + "scan --json --mode agent must emit a JSON envelope even when every \ patch-detail query fails; err={e}; stdout={stdout:?}; stderr={stderr}" ) }); @@ -1255,9 +1266,9 @@ async fn scan_prune_removes_withdrawn_patch_entry() { /// Update detection: when the API returns a different UUID for the /// same PURL that's in the manifest, `scan` surfaces that in the -/// `updates` array even without `--apply`. Sibling to +/// `updates` array even without `--mode agent`. Sibling to /// `scan_emits_updates_entry_when_newer_uuid_available` but exercised -/// with a stub blob on disk so we pin that scan without `--apply` never +/// with a stub blob on disk so we pin that scan without `--mode agent` never /// rewrites the manifest or existing blobs. #[tokio::test] async fn scan_detects_update_without_touching_existing_blobs() { @@ -1310,7 +1321,7 @@ async fn scan_detects_update_without_touching_existing_blobs() { ), ) .unwrap(); - // Marker blob: scan without --apply must leave it untouched. + // Marker blob: scan without --mode agent must leave it untouched. let marker = socket.join("blobs").join("untouched-by-scan"); std::fs::write(&marker, b"original contents").unwrap(); @@ -1324,19 +1335,19 @@ async fn scan_detects_update_without_touching_existing_blobs() { assert_eq!(updates[0]["oldUuid"], OLD_UUID); assert_eq!(updates[0]["newUuid"], NEW_UUID); - // Scan without --apply never rewrites the manifest or blobs. The manifest still records the OLD - // UUID and the marker blob is byte-for-byte unchanged. + // Scan without --mode agent never rewrites the manifest or blobs. The + // manifest still records the OLD UUID and the marker blob is byte-for-byte unchanged. let manifest: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(socket.join("manifest.json")).unwrap()) .unwrap(); assert_eq!( manifest["patches"]["pkg:npm/lodash@4.17.20"]["uuid"], OLD_UUID, - "scan without --apply must not rewrite the manifest" + "scan without --mode agent must not rewrite the manifest" ); assert_eq!( std::fs::read(&marker).unwrap(), b"original contents", - "scan without --apply must not touch existing blobs" + "scan without --mode agent must not touch existing blobs" ); let reqs = recorded(&mock).await; diff --git a/crates/socket-patch-cli/tests/scan/scan_sync_e2e.rs b/crates/socket-patch-cli/tests/scan/scan_sync_e2e.rs index 697637386..b46db1a25 100644 --- a/crates/socket-patch-cli/tests/scan/scan_sync_e2e.rs +++ b/crates/socket-patch-cli/tests/scan/scan_sync_e2e.rs @@ -1,4 +1,4 @@ -//! End-to-end tests for `scan --sync` (and `scan --apply` non-dry-run) +//! End-to-end tests for `scan --sync` (and `scan --mode agent` non-dry-run) //! — the canonical bot workflow that combines discovery, download, //! manifest write, file patch, and optional pruning. Exercises the //! full `scan -> get -> apply` pipeline against a mock API + a real @@ -77,7 +77,7 @@ async fn scan_sync_against_clean_project_adds_and_applies_patch() { }))) .mount(&mock) .await; - // Per-package search (scan --apply uses it) + // Per-package search (scan --mode agent uses it) Mock::given(method("GET")) .and(path(format!( "/v0/orgs/{ORG_SLUG}/patches/by-package/{encoded}" @@ -257,7 +257,7 @@ async fn scan_sync_against_clean_project_adds_and_applies_patch() { #[tokio::test] async fn scan_apply_with_existing_blob_uses_local_cache() { - // When the after-hash blob is already in .socket/blobs, scan --apply + // When the after-hash blob is already in .socket/blobs, scan --mode agent // should skip the blob download and use the cached one. let before = b"before\n"; let after = b"after\n"; @@ -327,7 +327,7 @@ async fn scan_apply_with_existing_blob_uses_local_cache() { write_root(tmp.path()); write_npm_package(tmp.path(), "cached-sync", "1.0.0", before); - // Pre-stage the manifest WITH the same UUID — scan --apply should + // Pre-stage the manifest WITH the same UUID — scan --mode agent should // emit `action: skipped` because UUID matches the manifest entry. let socket = tmp.path().join(".socket"); std::fs::create_dir_all(&socket).unwrap(); @@ -361,7 +361,8 @@ async fn scan_apply_with_existing_blob_uses_local_cache() { .args([ "scan", "--json", - "--apply", + "--mode", + "agent", "--yes", "--api-url", &mock.uri(), @@ -378,7 +379,7 @@ async fn scan_apply_with_existing_blob_uses_local_cache() { let stderr = String::from_utf8_lossy(&out.stderr).to_string(); assert_eq!( code, 0, - "scan --apply with cached UUID must succeed; stdout={stdout}; stderr={stderr}" + "scan --mode agent with cached UUID must succeed; stdout={stdout}; stderr={stderr}" ); let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("valid JSON"); @@ -391,7 +392,7 @@ async fn scan_apply_with_existing_blob_uses_local_cache() { // left unpatched with exit 0). let apply = v["apply"] .as_object() - .unwrap_or_else(|| panic!("scan --apply must emit an apply sub-object; envelope={v}")); + .unwrap_or_else(|| panic!("scan --mode agent must emit an apply sub-object; envelope={v}")); assert_eq!(apply["found"], 1, "apply.found; apply={apply:?}"); assert_eq!( apply["skipped"], 1, @@ -464,7 +465,7 @@ async fn scan_apply_with_existing_blob_uses_local_cache() { #[tokio::test] async fn scan_apply_with_no_patches_emits_empty_apply_object() { - // Discovery returns zero patches — scan --apply still emits the + // Discovery returns zero patches — scan --mode agent still emits the // apply sub-object so downstream consumers always see it. let mock = MockServer::start().await; Mock::given(method("POST")) @@ -484,7 +485,8 @@ async fn scan_apply_with_no_patches_emits_empty_apply_object() { .args([ "scan", "--json", - "--apply", + "--mode", + "agent", "--yes", "--api-url", &mock.uri(), @@ -530,7 +532,7 @@ async fn scan_apply_skips_vendored_purl_without_downloading() { // the vendored uuid (moving past it would break VEX verification with // `vendor_uuid_mismatch`), the patch view must never be fetched, and // the newer uuid still surfaces in `updates[]` as the operator's - // signal to run `scan --vendor` / `vendor`. + // signal to run `scan --mode vendored` / `vendor`. const NEW_UUID: &str = "22222222-2222-4222-8222-222222222222"; let before = b"before\n"; let before_hash = git_sha256(before); @@ -628,7 +630,8 @@ async fn scan_apply_skips_vendored_purl_without_downloading() { .args([ "scan", "--json", - "--apply", + "--mode", + "agent", "--yes", "--api-url", &mock.uri(), diff --git a/crates/socket-patch-cli/tests/scan/scan_vendor_step_error_e2e.rs b/crates/socket-patch-cli/tests/scan/scan_vendor_step_error_e2e.rs index 225d449d5..19031585e 100644 --- a/crates/socket-patch-cli/tests/scan/scan_vendor_step_error_e2e.rs +++ b/crates/socket-patch-cli/tests/scan/scan_vendor_step_error_e2e.rs @@ -73,7 +73,7 @@ fn write_fixture(root: &Path) { } /// Discovery (batch) plus the per-package search that selects `UUID` for -/// `PURL` — the endpoints `scan --vendor` hits before the download phase. +/// `PURL` — the endpoints `scan --mode vendored` hits before the download phase. async fn mount_discovery(mock: &MockServer) { Mock::given(method("POST")) .and(path(format!("/v0/orgs/{ORG_SLUG}/patches/batch"))) @@ -203,7 +203,8 @@ async fn scan_vendor_download_error_preserves_the_vendor_envelope() { &[ "scan", "--json", - "--vendor", + "--mode", + "vendored", "--yes", "--api-url", &mock.uri(), diff --git a/crates/socket-patch-cli/tests/scan_api_retry_e2e.rs b/crates/socket-patch-cli/tests/scan_api_retry_e2e.rs index 0b36f1f41..2ef7df2eb 100644 --- a/crates/socket-patch-cli/tests/scan_api_retry_e2e.rs +++ b/crates/socket-patch-cli/tests/scan_api_retry_e2e.rs @@ -428,7 +428,7 @@ async fn exhausted_detail_fetch_is_a_json_warning() { let (code, stdout, stderr) = run_scan( tmp.path(), &server.uri(), - &["--json", "--apply", "--dry-run"], + &["--json", "--mode", "agent", "--dry-run"], &[], ); let v = json(&stdout); diff --git a/crates/socket-patch-cli/tests/scan_requirements_lock_only.rs b/crates/socket-patch-cli/tests/scan_requirements_lock_only.rs index 61b86143b..da2433b07 100644 --- a/crates/socket-patch-cli/tests/scan_requirements_lock_only.rs +++ b/crates/socket-patch-cli/tests/scan_requirements_lock_only.rs @@ -121,7 +121,7 @@ async fn assert_lock_only_discovers_bytes_with_env( envs: &[(&str, &str)], expected: &[&str], ) { - for mode in [&[][..], &["--vendor"][..]] { + for mode in [&[][..], &["--mode", "vendored"][..]] { let mock = MockServer::start().await; mount_empty_batch(&mock).await; let tmp = tempfile::tempdir().unwrap(); diff --git a/crates/socket-patch-cli/tests/scan_vendor_e2e.rs b/crates/socket-patch-cli/tests/scan_vendor_e2e.rs index 2f0208055..d6d7af5f1 100644 --- a/crates/socket-patch-cli/tests/scan_vendor_e2e.rs +++ b/crates/socket-patch-cli/tests/scan_vendor_e2e.rs @@ -1,4 +1,4 @@ -//! End-to-end tests for `scan --vendor` — the bot workflow that discovers +//! End-to-end tests for `scan --mode vendored` — the bot workflow that discovers //! patches, fetches their records in memory, and vendors each patched //! package into the committable `.socket/vendor/` tree instead of //! applying in place. Vendored mode is manifest-free: the ledger's @@ -203,7 +203,8 @@ fn run_scan_vendor(root: &Path, mock_uri: &str, extra: &[&str]) -> (i32, String, let mut argv = vec![ "scan", "--json", - "--vendor", + "--mode", + "vendored", "--yes", "--api-url", mock_uri, @@ -233,7 +234,7 @@ fn assert_socket_dir_lean(root: &Path) { #[tokio::test] async fn scan_vendor_end_to_end_is_manifest_free() { - // scan --vendor: discover → fetch records in memory → vendor. The + // scan --mode vendored: discover → fetch records in memory → vendor. The // ledger (with embedded records) is the only state written. let mock = MockServer::start().await; mount_patch_api(&mock, UUID).await; @@ -342,7 +343,7 @@ async fn mount_empty_discovery(mock: &MockServer) { } /// Seed a committed `.socket/manifest.json` plus its afterHash blob — the -/// state a repo has after `scan --vendor` was run and `.socket/vendor/` +/// state a repo has after `scan --mode vendored` was run and `.socket/vendor/` /// was later wiped (or never committed). The blob lets the vendor engine /// stage sources with no download phase and no network. fn seed_committed_manifest(root: &Path) { @@ -376,7 +377,7 @@ fn seed_committed_manifest(root: &Path) { /// Vendored mode takes its work from DISCOVERY, never from a committed /// manifest: with nothing discovered there is nothing to vendor, so -/// `scan --vendor` is a clean no-op that creates nothing — no +/// `scan --mode vendored` is a clean no-op that creates nothing — no /// `.socket/vendor/`, no `apply.lock` — and a legacy manifest is left /// byte-identical. Both arms agree (the interactive arm exits before the /// vendor dispatch; the JSON arm's vendor step skips itself before taking @@ -396,7 +397,8 @@ async fn scan_vendor_with_empty_discovery_is_a_no_op() { let manifest_before = std::fs::read(tmp.path().join(".socket/manifest.json")).unwrap(); let mut argv = vec![ "scan", - "--vendor", + "--mode", + "vendored", "--yes", "--api-url", &uri, @@ -523,7 +525,7 @@ async fn scan_vendor_migrates_legacy_manifest_mode_project() { #[tokio::test] async fn scan_vendor_writes_no_manifest() { - // scan --vendor: the manifest-free flow, embedded-record ledger and all. + // scan --mode vendored: the manifest-free flow, embedded-record ledger and all. let mock = MockServer::start().await; mount_patch_api(&mock, UUID).await; let tmp = tempfile::tempdir().unwrap(); @@ -661,7 +663,7 @@ async fn scan_vendor_dry_run_previews_without_touching_disk() { ); } -/// Interactive (non-JSON) `scan --vendor` with a failing patch +/// Interactive (non-JSON) `scan --mode vendored` with a failing patch /// view fetch must SAY what failed: exit 1 with a `[fail]` line naming the /// purl on stderr. Regression guard: `download_patch_records`' failure arms /// recorded the error only in their JSON report, so the human path exited @@ -721,7 +723,8 @@ async fn scan_vendor_fetch_failure_reports_error() { let out = Command::new(binary()) .args([ "scan", - "--vendor", + "--mode", + "vendored", "--yes", "--api-url", &mock.uri(), @@ -762,11 +765,11 @@ async fn scan_vendor_fetch_failure_reports_error() { } #[tokio::test] -async fn scan_vendor_flag_conflicts_are_clap_errors() { - // --vendor conflicts with --apply/--sync. +async fn scan_mode_sync_conflicts_are_usage_errors() { + // --sync means --mode agent --prune, so it conflicts with any other mode. for argv in [ - &["scan", "--vendor", "--apply"][..], - &["scan", "--vendor", "--sync"][..], + &["scan", "--mode", "vendored", "--sync"][..], + &["scan", "--mode", "hosted", "--sync"][..], ] { let out = Command::new(binary()) .args(argv) @@ -775,10 +778,7 @@ async fn scan_vendor_flag_conflicts_are_clap_errors() { .expect("run"); let code = out.status.code().unwrap_or(-1); let stderr = String::from_utf8_lossy(&out.stderr); - assert_eq!( - code, 2, - "argv={argv:?} must be a clap usage error: {stderr}" - ); + assert_eq!(code, 2, "argv={argv:?} must be a usage error: {stderr}"); assert!( stderr.contains("cannot be used with"), "argv={argv:?}: {stderr}" @@ -819,7 +819,8 @@ async fn scan_vendor_emits_no_telemetry_even_with_endpoint_env() { &[ "scan", "--json", - "--vendor", + "--mode", + "vendored", "--yes", "--api-url", &mock_uri, @@ -1024,7 +1025,7 @@ async fn scan_vendor_resolves_percent_encoded_scoped_purl() { // ───────────────────── prune reconciles vendored state ───────────────────── /// After a dependency is removed and re-locked, `scan --prune` (without -/// `--vendor`) reclaims its vendored entry in one run (#665): +/// `--mode vendored`) reclaims its vendored entry in one run (#665): /// /// 1. The wired lock entry VANISHED (`npm uninstall`). That is not drift: /// nothing in the lock resolves through the artifact any more, so the @@ -1078,7 +1079,7 @@ async fn scan_prune_reverts_unused_vendored_entry() { std::fs::write(tmp.path().join("package-lock.json"), &lock_bytes).unwrap(); std::fs::remove_dir_all(tmp.path().join("node_modules/left-pad")).unwrap(); - // Plain prune scan (read-only discovery + GC; no --vendor, no --apply). + // Plain prune scan (read-only discovery + GC; no --mode vendored, no --mode agent). let run_prune = || { let out = Command::new(binary()) .args([ @@ -1306,7 +1307,7 @@ async fn scan_vendor_prune_reconciles_unwired_entry_on_an_empty_crawl() { assert_eq!(unwired(&v), 0, "envelope={v}"); } -/// Interactive (non-JSON) `scan --vendor` pre-verifies patch baselines: +/// Interactive (non-JSON) `scan --mode vendored` pre-verifies patch baselines: /// installed content matching NEITHER hash is annotated before vendoring /// starts, and the run still vendors (auto-force) with the /// `vendor_content_mismatch_overwritten` warning on stderr. @@ -1326,7 +1327,8 @@ async fn scan_vendor_annotates_mismatched_baseline_and_vendors_anyway() { let out = Command::new(binary()) .args([ "scan", - "--vendor", + "--mode", + "vendored", "--yes", "--api-url", &mock.uri(), @@ -1755,7 +1757,7 @@ async fn vendor_verifies_server_artifact_without_old_lock_integrity() { } /// The headline flow: a COMPLETELY fresh clone (lockfile, no node_modules, -/// no .socket) discovers from the lockfile and `scan --vendor` vendors +/// no .socket) discovers from the lockfile and `scan --mode vendored` vendors /// end-to-end via the registry fetch. #[tokio::test] async fn scan_vendor_works_on_a_completely_fresh_clone() { @@ -1996,7 +1998,7 @@ async fn scan_flags_scoped_lockfile_only_package_despite_api_purl_encoding() { ); } -/// `scan --apply` skips lockfile-only patches calmly: exit 0, a skipped +/// `scan --mode agent` skips lockfile-only patches calmly: exit 0, a skipped /// record with package_not_installed, and NO manifest entry written. #[tokio::test] async fn scan_apply_skips_lockfile_only_without_error() { @@ -2013,7 +2015,8 @@ async fn scan_apply_skips_lockfile_only_without_error() { .args([ "scan", "--json", - "--apply", + "--mode", + "agent", "--yes", "--api-url", &mock.uri(), @@ -2090,7 +2093,7 @@ async fn scan_vendored_bun_v1_workspace_refuses_in_download_phase() { write_bun_v1_workspace_fixture(tmp.path()); let lock_before = std::fs::read(tmp.path().join("bun.lock")).unwrap(); - let (code, stdout, stderr) = run_scan_vendor(tmp.path(), &mock.uri(), &["--mode", "vendored"]); + let (code, stdout, stderr) = run_scan_vendor(tmp.path(), &mock.uri(), &[]); assert_eq!(code, 1, "stdout={stdout}; stderr={stderr}"); let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("valid JSON"); assert_eq!(v["status"], "partial_failure", "envelope={v}"); @@ -2224,7 +2227,7 @@ async fn scan_vendored_vlt_transitive_refuses_in_download_phase() { let tmp = tempfile::tempdir().unwrap(); write_vlt_fixture(tmp.path(), true); let lock_before = std::fs::read(tmp.path().join("vlt-lock.json")).unwrap(); - let (code, stdout, stderr) = run_scan_vendor(tmp.path(), &mock.uri(), &["--mode", "vendored"]); + let (code, stdout, stderr) = run_scan_vendor(tmp.path(), &mock.uri(), &[]); assert_eq!(code, 1, "stdout={stdout}; stderr={stderr}"); let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("valid JSON"); let dl = &v["download"]; @@ -2244,11 +2247,7 @@ async fn scan_vendored_vlt_transitive_refuses_in_download_phase() { ); assert!(!tmp.path().join(".socket").exists()); - let (code, stdout, stderr) = run_scan_vendor( - tmp.path(), - &mock.uri(), - &["--mode", "vendored", "--dry-run"], - ); + let (code, stdout, stderr) = run_scan_vendor(tmp.path(), &mock.uri(), &["--dry-run"]); assert_eq!(code, 0, "stdout={stdout}; stderr={stderr}"); let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("valid JSON"); let text = v.to_string(); @@ -2265,7 +2264,7 @@ async fn scan_vendored_vlt_direct_dependency_vendors() { mount_patch_api(&mock, UUID).await; let tmp = tempfile::tempdir().unwrap(); write_vlt_fixture(tmp.path(), false); - let (code, stdout, stderr) = run_scan_vendor(tmp.path(), &mock.uri(), &["--mode", "vendored"]); + let (code, stdout, stderr) = run_scan_vendor(tmp.path(), &mock.uri(), &[]); assert_eq!(code, 0, "stdout={stdout}; stderr={stderr}"); let rel = format!(".socket/vendor/npm/{UUID}/left-pad-1.3.0/node_modules/left-pad"); assert_eq!( @@ -2282,13 +2281,13 @@ async fn scan_vendored_vlt_direct_dependency_vendors() { assert_socket_dir_lean(tmp.path()); } -/// Manifest-less VEX over the committed state `scan --vendor` leaves +/// Manifest-less VEX over the committed state `scan --mode vendored` leaves /// (manifest-free since 5.0 — the ledger's `detached` entries embed the /// records, so there is one shape to cover): the checkout attests `(vendored)` from the ledger's /// embedded record, then from lockfile discovery + the patch API once the /// ledgers are gone too, never `--offline` (`record_unavailable`, zero /// requests), and not once the lock is reverted (`vendor_unwired`, -/// `--no-verify` too). The embedded `scan --vendor --vex` of the producing +/// `--no-verify` too). The embedded `scan --mode vendored --vex` of the producing /// run attests as well. The manifest-driven standalone `vendor` shape (a /// NON-detached entry with a fallback record) is /// `standalone_vendor_state_attests_from_the_embedded_record`. @@ -2305,7 +2304,7 @@ async fn scan_vendor_state_attests_manifest_less() { let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("valid JSON"); assert_eq!( v["vex"]["statements"], 1, - "embedded scan --vendor --vex: {v}" + "embedded scan --mode vendored --vex: {v}" ); assert!( !tmp.path().join(".socket/manifest.json").exists(), @@ -2314,7 +2313,7 @@ async fn scan_vendor_state_attests_manifest_less() { let checkout = tmp.path().join("checkout"); npm_e2e_common::fresh_checkout(tmp.path(), &checkout, &["package-lock.json"]); - run_manifestless_tail("scan --vendor", &checkout, pristine); + run_manifestless_tail("scan --mode vendored", &checkout, pristine); } /// The committed state of the manifest-driven standalone `vendor` — the diff --git a/crates/socket-patch-cli/tests/vex_pdm_hatch_common/mod.rs b/crates/socket-patch-cli/tests/vex_pdm_hatch_common/mod.rs index 35ebebdba..8f1c0e2b2 100644 --- a/crates/socket-patch-cli/tests/vex_pdm_hatch_common/mod.rs +++ b/crates/socket-patch-cli/tests/vex_pdm_hatch_common/mod.rs @@ -17,7 +17,7 @@ //! ``` //! //! Every project is wired by the REAL CLI, not by hand: `scan --mode hosted` -//! (hosted) or `scan --vendor --vendor-source build` (vendored) runs against +//! (hosted) or `scan --mode vendored --vendor-source build` (vendored) runs against //! a wiremock stand-in for the Socket API, with the package's pristine //! install in a fabricated `.venv` for the vendored build. The wired tree is //! snapshotted once per (flavor, mode) and every cell restores the snapshot @@ -426,7 +426,7 @@ pub fn path_with_fake_hatch(scratch: &Path) -> std::ffi::OsString { std::env::join_paths(paths).unwrap() } -/// `scan --json` (hosted: `--mode hosted`; vendored: `--vendor --vendor-source +/// `scan --json` (hosted: `--mode hosted`; vendored: `--mode vendored --vendor-source /// build`) in `cwd` against `api`. pub fn run_scan( cwd: &Path, @@ -452,7 +452,9 @@ pub fn run_scan( .collect(); match mode { Mode::Hosted => args.push("--mode=hosted".into()), - Mode::Vendored => args.extend(["--vendor", "--vendor-source", "service"].map(String::from)), + Mode::Vendored => { + args.extend(["--mode", "vendored", "--vendor-source", "service"].map(String::from)) + } } args.extend(extra.iter().map(|s| s.to_string())); let out = cli() @@ -1175,7 +1177,7 @@ pub fn g_vendored_attests_over_a_pristine_venv_with_a_warning(flavors: &[Flavor] } } -/// `scan --vendor --vex` never writes a manifest; its own VEX +/// `scan --mode vendored --vex` never writes a manifest; its own VEX /// and a later standalone `vex` both attest (ledger present, then gone). pub fn embedded_detached_vendor_scan_attests_without_a_manifest(flavors: &[Flavor]) { for flavor in flavors.iter().filter(|f| f.vendored) { @@ -1201,7 +1203,7 @@ pub fn embedded_detached_vendor_scan_attests_without_a_manifest(flavors: &[Flavo } } -/// A CI re-run of `scan --mode hosted --vex` / `scan --vendor --vex` on a +/// A CI re-run of `scan --mode hosted --vex` / `scan --mode vendored --vex` on a /// checkout whose `.socket/` was never committed (the wiring is already /// there): the embedded document still attests. `expect_refusal` names the /// flavors whose vendored backend documents a refusal for a ledgerless diff --git a/crates/socket-patch-cli/tests/vex_pipenv_pip_common/mod.rs b/crates/socket-patch-cli/tests/vex_pipenv_pip_common/mod.rs index f871cf1f6..fc8029810 100644 --- a/crates/socket-patch-cli/tests/vex_pipenv_pip_common/mod.rs +++ b/crates/socket-patch-cli/tests/vex_pipenv_pip_common/mod.rs @@ -17,7 +17,7 @@ //! ``` //! //! Every project is wired by the REAL CLI, not by hand: `scan --mode hosted` -//! (hosted) or `scan --vendor --vendor-source build` (vendored) runs against +//! (hosted) or `scan --mode vendored --vendor-source build` (vendored) runs against //! a wiremock stand-in for the Socket API, with the package's pristine //! install in a fabricated `.venv` for the vendored build. The wired tree is //! snapshotted once per (flavor, mode) and every cell restores the snapshot @@ -467,7 +467,7 @@ pub fn run_scan( ]; match mode { Mode::Hosted => args.push("--mode=hosted"), - Mode::Vendored => args.extend(["--vendor", "--vendor-source", "service"]), + Mode::Vendored => args.extend(["--mode", "vendored", "--vendor-source", "service"]), } args.extend_from_slice(extra); let out = cli(cwd.parent().unwrap()) @@ -1237,7 +1237,7 @@ pub fn g_vendored_attests_over_a_pristine_venv_with_a_warning(flavors: &[Flavor] } } -/// `scan --vendor --vex` never writes a manifest; its own VEX +/// `scan --mode vendored --vex` never writes a manifest; its own VEX /// and a later standalone `vex` both attest (ledger present, then gone). pub fn embedded_detached_vendor_scan_attests_without_a_manifest(flavors: &[Flavor]) { for flavor in flavors.iter().filter(|f| f.vendored) { @@ -1263,7 +1263,7 @@ pub fn embedded_detached_vendor_scan_attests_without_a_manifest(flavors: &[Flavo } } -/// A CI re-run of `scan --mode hosted --vex` / `scan --vendor --vex` on a +/// A CI re-run of `scan --mode hosted --vex` / `scan --mode vendored --vex` on a /// checkout whose `.socket/` was never committed (the wiring is already /// there): the embedded document still attests, and the re-scan leaves the /// wired lockfile byte-identical. diff --git a/crates/socket-patch-cli/tests/vex_pipenv_pip_real/mod.rs b/crates/socket-patch-cli/tests/vex_pipenv_pip_real/mod.rs index 0d43245bc..6c8482cce 100644 --- a/crates/socket-patch-cli/tests/vex_pipenv_pip_real/mod.rs +++ b/crates/socket-patch-cli/tests/vex_pipenv_pip_real/mod.rs @@ -95,7 +95,7 @@ impl Mode { pub fn scan_flags(self) -> &'static [&'static str] { match self { Mode::Hosted => &["--mode=hosted"], - Mode::Vendored => &["--vendor", "--vendor-source", "service"], + Mode::Vendored => &["--mode", "vendored", "--vendor-source", "service"], } } } diff --git a/docs/migrating-to-v5.md b/docs/migrating-to-v5.md index 028ce8d65..27ce1d90a 100644 --- a/docs/migrating-to-v5.md +++ b/docs/migrating-to-v5.md @@ -99,7 +99,15 @@ run `socket-patch apply` once after migration to confirm the manifest still appl | `SOCKET_PATCH_PROXY_URL` | `SOCKET_PROXY_URL` | | `SOCKET_PATCH_DEBUG` | `SOCKET_DEBUG` | | `SOCKET_PATCH_TELEMETRY_DISABLED` | `SOCKET_TELEMETRY_DISABLED` | +| `scan --apply` | `scan --mode agent` | +| `scan --vendor` | `scan --mode vendored` | +| `get --no-apply` | `get --save-only` (`SOCKET_SAVE_ONLY` is unchanged) | +| `socket-patch download` | `socket-patch get` | +| `socket-patch gc` | `socket-patch repair` | | `SOCKET_FORCE` | Pass `--force` to the one command that needs it (`apply`, `vendor`, `--update`); the variable is now ignored | +A removed spelling is a usage error (exit 2). `scan --sync` stays as the +shorthand for `scan --mode agent --prune`. + Legacy `.socket/packages/` archives are no longer read. Patch data uses diff archives or blobs; cleanup commands remove obsolete package archives.