diff --git a/crates/socket-patch-cli/CLI_CONTRACT.md b/crates/socket-patch-cli/CLI_CONTRACT.md index 68f19d7af..a9a861dd6 100644 --- a/crates/socket-patch-cli/CLI_CONTRACT.md +++ b/crates/socket-patch-cli/CLI_CONTRACT.md @@ -36,7 +36,49 @@ Rows are in `--help` order (v5.0): the hosted/vendored workflow (`scan` → `vex **Bare-UUID fallback.** `socket-patch ` is rewritten to `socket-patch get `, also when root-position flags come first (`socket-patch --json `); a UUID after a subcommand name is that subcommand's operand. The UUID shape checked is the standard 8-4-4-4-12 hex pattern (case-insensitive), the target grammar's. -**Target grammar (v5.0).** `get`, `remove`, `rollback` and the bare-UUID fallback classify their package/patch token with one parser (core `utils::target`): a UUID; `CVE-…` / `GHSA-…` (case-insensitive); a `pkg:` purl (with a version: that release, a base purl covering every release variant and a `?qualified` one exactly one; without a version: every version of that package, compared by purl identity — case-sensitive for npm, Go, Maven, cargo and gem, folded for PyPI (PEP 503), NuGet and Composer, so `pkg:npm/jsonstream` never selects `JSONStream`); otherwise an **exact** package name — full name or last segment, case-insensitive, PEP 503 for PyPI — the same matcher as `scan --package` and `socket.yml`. A name never matches by prefix or substring, and a Go major-version suffix (`v2`) is never a name. **Ambiguous names**: `get`, `remove` and `rollback` act on one package per name, so a name whose last-segment rule reaches several packages (`core` → `@angular/core` and `@babel/core`; the same name in two ecosystems), or whose case-insensitive rule reaches case-distinct packages (`jsonstream` → npm's `JSONStream` and `jsonstream`, Go's `Sirupsen` and `sirupsen`), is refused with exit 1 before anything is searched or changed — `"core" is ambiguous: it names pkg:npm/@angular/core, pkg:npm/@babel/core; use the full name or a purl` (`remove --json`: `errorCode: "ambiguous_target"`; `get` / `rollback --json`: `{status: "error", error}`). Several versions of one package are not ambiguous, and a name typed with an uppercase letter settles on its exact-case package among case-distinct ones (`JSONStream`). `scan --package` and `socket.yml` keep selecting every package the name reaches. `get ` searches every installed version of the matched name (within `--ecosystems`) and prints `Matched: …` on stderr; with no exact match it is `no_match` (exit 0, no API call) and, in human mode, suggests up to five near names (`Did you mean: …?`) without acting on them. `remove` / `rollback` accept the same names and versionless purls against manifest records, vendor-ledger entries and hosted pins; CVE/GHSA ids match no record there. A name containing `/` (composer `vendor/pkg`, a go module path) is path-shaped, so `rollback` first tries it as a name: when it selects a recorded or hosted patch it is a target, otherwise a path glob. +**Target grammar (v5.0).** `get`, `remove`, `rollback` and the bare-UUID +fallback classify their package/patch token with one parser (core `utils::target`): +a UUID; `CVE-…` / `GHSA-…` (case-insensitive); a `pkg:` purl (with a version: +that release, a base purl covering every release variant and a `?qualified` one +exactly one; without a version: every version of that package, compared by purl identity — +case-sensitive for npm, Go, Maven, cargo and gem, folded for PyPI (PEP 503), +NuGet and Composer, so `pkg:npm/jsonstream` never selects `JSONStream`); otherwise an **exact** package +name — full name or last segment, case-insensitive, PEP 503 for PyPI — the same +matcher as `scan --package` and `socket.yml`. A name never matches by prefix or +substring, and a Go major-version suffix (`v2`) alone never selects a module. + +A full-name match takes precedence over last-segment matches: `lodash` selects +only `lodash` when `@types/lodash` is also present. **Ambiguous names**: `get`, +`remove` and `rollback` act on one package per name, so a name that still reaches +several packages (`core` → `@angular/core` and `@babel/core`; the same name in two +ecosystems), or whose case-insensitive rule reaches case-distinct packages +(`jsonstream` → npm's `JSONStream` and `jsonstream`, Go's `Sirupsen` and +`sirupsen`), is refused with exit 1 before any patch search or mutation. The +message lists versionless purls and a remedy: `"core" is ambiguous: it names +pkg:npm/@angular/core, pkg:npm/@babel/core; use the full name or a purl`. All three +commands report `status: "error"` and `error.code: "ambiguous_target"` under +`--json`. Several versions of one package are not ambiguous, and a name typed with an +uppercase letter settles on its exact-case package among case-distinct ones +(`JSONStream`). `scan --package` and +`socket.yml` keep selecting every package the name reaches. + +`get ` searches every installed version of the matched name (within +`--ecosystems`) and prints `Matched: …` on stderr. With no exact match in a +nonempty inventory it is `no_match` (exit 0, no patch-search API call) and, in +human mode, suggests up to five near names (`Did you mean: …?`) without acting on +them. An empty inventory is `no_packages` (exit 0). `--ecosystems` also filters +advisory/purl search results and UUID selection before any patch is written; a +UUID outside the selected ecosystems is `not_found` (exit 0). + +`remove` / `rollback` accept the same names and versionless purls against manifest +records, vendor-ledger entries and hosted pins; CVE/GHSA ids match no record +there. An npm scoped name (`@babel/core`) is always a package target. A relative +name containing `/` (composer `vendor/pkg`, a Go module path), without glob +metacharacters or `.` / `..` path segments, is path-shaped, so `rollback` first +tries it as a name: when it selects a recorded or hosted patch it is a target, +otherwise a path glob. Use `./vendor/pkg` or `'vendor/pkg/**'` to explicitly select +installed copies by path. See the [migration guide](../../docs/migrating-to-v5.md#package-targeting) +for examples and script migration guidance. **Root `--update` flag.** `socket-patch --update [VERSION]` updates the binary itself from GitHub Releases. It is a root flag, not a subcommand: argv is rewritten (the same mechanism as the bare-UUID fallback) onto an internal hidden subcommand whose name carries no stability guarantee — script the flag, never the internal name. Combining the flag with a subcommand (`socket-patch --update scan`) is a usage error (exit 2). Full contract: [Self-update contract](#self-update-contract-socket-patch---update). diff --git a/docs/migrating-to-v5.md b/docs/migrating-to-v5.md index 19898097c..9265748ba 100644 --- a/docs/migrating-to-v5.md +++ b/docs/migrating-to-v5.md @@ -49,6 +49,57 @@ existing scripts against the new CLI; the [changelog](../CHANGELOG.md) and `SOCKET_ORG_SLUG` to get org patches. The org is resolved once per run, so an embedded `--vex` no longer resolves it again. +## Package targeting + +`get ` now selects an exact package name instead of automatically choosing +the nearest fuzzy match. For example, `get yaml` never selects `yaml-ast-parser`; +near names are suggestions only. It queries every installed version of the +selected package, including older nested copies. If a script should select only +one release, use a versioned PURL such as `pkg:npm/lodash@4.17.20`. + +`get`, `remove`, and `rollback` share these package/patch target forms: + +| Target | Meaning | +| --- | --- | +| Package name, such as `lodash` or `@babel/core` | Every installed version for `get`; every recorded version for `remove` / `rollback`, including vendor records and hosted pins | +| Versionless PURL, such as `pkg:npm/lodash` | Select the package across versions, with an explicit ecosystem | +| Versioned PURL, such as `pkg:npm/lodash@4.17.20` | Select that release; an unqualified PURL covers its release variants, while a qualified PURL selects one variant | +| Patch UUID | Select one patch; `socket-patch ` is shorthand for `get `, including after root flags such as `--json` | +| CVE or GHSA ID | Search by advisory with `get`; these IDs do not select records for `remove` / `rollback`, so use the package PURL or patch UUID there | + +Name matching uses the same case-insensitive rules as `scan --package`, including +PyPI normalization of `.`, `_`, and `-`. A full-name match takes precedence: +`get lodash` selects `lodash` when both it and `@types/lodash` are installed. A +last-segment name such as `core` is accepted only when it identifies one package. +If it reaches both `@angular/core` and `@babel/core`, `get`, `remove`, and `rollback` +refuse it with exit 1 and `error.code: "ambiguous_target"` under `--json`, before +changing any patches. The message lists exact versionless PURLs to use instead. +The same name in two ecosystems also needs a PURL to disambiguate it. Multiple +versions of one package are not ambiguous. `scan --package` and `socket.yml` +continue to select every package a name matches. Go major-version suffixes such +as `v2` alone do not select modules; use the full module path or PURL. + +`get --ecosystems` now scopes package-name discovery and filters every search +result and UUID selection before any patch is written, in every mode. A UUID +outside the selected ecosystems returns `status: "not_found"` with exit 0. A +name absent from a nonempty package inventory returns `status: "no_match"` with +exit 0, without searching for or applying a suggested package. An empty inventory +returns `status: "no_packages"`. Scripts should inspect these statuses instead of +assuming exit 0 means a patch was selected. + +`rollback` also accepts path globs. An npm scoped name such as `@babel/core` is +always a package target. A slash-containing name such as `monolog/monolog` is a +package target when it selects a recorded patch or hosted pin; otherwise it is +treated as a path glob. Use an explicit path such as `./monolog/monolog` or a +quoted glob such as `'node_modules/**'` to select installed copies by location. + +```sh +socket-patch get yaml --ecosystems npm --dry-run +socket-patch get pkg:npm/lodash@4.17.20 --dry-run +socket-patch remove pkg:npm/@babel/core --dry-run +socket-patch rollback pkg:composer/monolog/monolog --dry-run +``` + ## Service-only vendoring v4 could build patched artifacts locally, with `auto` as the default acquisition diff --git a/docs/usage.md b/docs/usage.md index 23f0cf4da..c9ab487dc 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -43,9 +43,12 @@ Replace the example identifiers with the advisory or package you need. `get` als accepts a patch UUID or an exact package name, which covers every installed version of that package. `remove` and `rollback` take the same names. A short name that names several packages (`core` for `@angular/core` and `@babel/core`) is refused: -use the full name or a purl. `get` defaults to hosted mode; `--save-only` and -global targeting default to agent mode instead. Hosted and vendored `get` do not -prompt. Agent-mode searches can offer an interactive choice. +use the full name or a purl. Near names are only suggestions, and `--ecosystems` +scopes both `get` searches and UUID selection. See the +[v5 targeting changes](migrating-to-v5.md#package-targeting) for target forms, +ambiguity rules, and no-match statuses. `get` defaults to hosted mode; `--save-only` +and global targeting default to agent mode instead. Hosted and vendored `get` do +not prompt. Agent-mode searches can offer an interactive choice. For each package version, automatic selection prefers the highest severity among downloadable patches, then the most distinct advisories fixed, then the newest @@ -228,8 +231,8 @@ The removed `setup` command is covered in the [migration guide](migrating-to-v5. | Command | Effect | | --- | --- | | `list` | Show agent records, vendor records, and hosted pins; an empty project succeeds | -| `rollback [PURL\|UUID\|PATH]...` | Restore selected patches, or all patches when no target is given, and remove their local state | -| `remove ` | Restore and remove one patch | +| `rollback [NAME\|PURL\|UUID\|PATH]...` | Restore selected patches, or all patches when no target is given, and remove their local state | +| `remove ` | Restore and remove matching patches; a name or versionless PURL selects every recorded version | | `vendor --revert` | Undo vendoring from its recorded edits and remove the vendored artifacts | | `repair` | Restore missing agent patch data or missing/corrupt vendored artifacts using existing records, and clean unused data | | `scan --mode agent --prune` | Patch discovered packages and remove records for dependencies that left the project |