Consolidated terminal UI and CLI-wide output polish - #248
Merged
Merged
Conversation
Scan's progress lines garbled on a real terminal, e.g. "Found 7 patches for 1 packagesatch 7/7)". Hand-rolled `\r` rewrites never cleared to end of line, and warnings printed mid-line. An end-to-end audit of every command (243 verified findings) found the same class of glitch across the CLI. This commit fixes them and moves all terminal-UI logic into one small, unit-tested module. New `crates/socket-patch-cli/src/ui/` (replaces `output.rs`; drops the unused `indicatif`): - `StatusLine<W: Write>`: deterministic status line. Every update writes `\r\x1b[2K` and a width-truncated message. `println` puts a line above an active status. The line clears on finish and on Drop. It is never live off a TTY, under TERM=dumb, in debug mode, or under --json/--silent. - Prompts: `confirm` / `confirm_or_proceed` / `select_one`, built on the testable `confirm_with` core. EOF (Ctrl-D) declines instead of accepting. Typeahead is flushed before prompting. The non-TTY defaults are kept, but their note respects --silent. The dialoguer cursor is restored on Ctrl-C. - Color: one `color_enabled` policy honoring NO_COLOR / CLICOLOR / CLICOLOR_FORCE / TERM=dumb, shared with dialoguer. Table cells are padded before painting, so colored rows align. - Text: `plural`, and a char-safe word-boundary `truncate`. - Core: `utils::notice` quiet switch. Info advisories are muted under --silent/--json and print once per process. Warnings are muted only under --silent. Per-command polish (scan agent/hosted/vendored, get, apply, rollback, remove, vendor, repair, list, lock waits, --update, notifier, vex, setup): - counted nouns instead of "(s)"; - consistent "Error:"/"Warning:" prefixes, with progress and warnings on stderr; - --silent is errors-only, but a failing run still says why; - deterministic ordering; - no developer notes leaking into --help; - dry-run fixes for `get` and `vex`, plus `vex -O -`. Rebased onto #247; its behavior and contract are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Mechanical fixes so clippy --all-targets -D warnings passes on both crates: assert! for a literal-bool assert_eq!, slice::from_ref over a one-element clone, a Tamper alias for a complex test type, non-test items moved above bun_binary's test modules, an eta-reduced closure, an elided lifetime, and an initialized-in-place let. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…cked The vendor summary lost its closing period to clap's first-paragraph trim; it is now a one-line summary with the rest in the long help. The repair about now names what it restores (blobs, diff/package archives, vendored artifacts). help_text_hygiene scans every subcommand's page, self-update included, instead of three owned pages. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A non-2xx response now reports the body's error.message / message / error string when it is JSON, the trimmed text otherwise, and no dangling colon when the body is empty. A failed binary body read keeps its cause chain like the send error does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The hosted arm dropped the VEX summary's warnings, and under --json they are muted on stderr, so e.g. product_not_iri or an unreadable vendor ledger reached no channel. They now ride vex.warnings (skip-if-empty), as in the agent arm. apply --vex and scan --vex print their summary via the shared vex::format_vex_written. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…complete." repair exits 1 when a vendored artifact cannot be rebuilt, but the human run still closed with "Repair complete." on stdout. It now closes with "Repair finished with errors." on stderr, like a failed download. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
get --mode vendored printed a raw "Error (<code>): <message>" for a failed vendor step or bun refusal, without the capital, the period or the --lock-timeout hint that scan's vendored arm prints. Both now go through scan::vendor_flow::format_vendor_step_error. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
get kept its own copy of "No packages found. Run your package manager's install first."; it now calls scan::render's no_packages_message, so the two cannot drift. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The vendored arm hand-rolled "GC: pruned N manifest entries.", which dropped the swept orphan files and said nothing when only files were removed. It now prints gc::format_gc_line, like the agent arm. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
scan hands select_patches its args with json off (scan has no selection_required path), which also meant a free user's scan --json on a TTY with several patches per package got the interactive menu. --json now also counts as --yes there, taking the menu default (the top-ranked patch), as a non-TTY run already did. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The detached-ledger reuse printed the raw purl (pkg:npm/%40scope/...), unlike the "already in manifest" line beside it. Both now share format_record_skip, which displays the decoded purl. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…d scan vendor_flow and hosted each spelled out the same --lock-timeout hint; both now use lock_cli::HELD_RETRY_HINT. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
"Cannot rollback: <file> - <why>" and "Cannot safely rollback." now read "Cannot roll back" / "Cannot safely roll back.", matching the CLI's own wording. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
setup writes both scripts.postinstall and scripts.dependencies, but the preview listed only the postinstall line (the remove preview already showed both). Core UpdateResult now carries the old/new dependencies script and the preview prints "-> dependencies" when it changes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… --vex scan's agent arm said "[dry-run] VEX generation skipped. No attestation written." while apply, vendor and hosted scan said "Skipping VEX generation (--dry-run: nothing was <done>).". All four now print vex::format_vex_dry_run_skip. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Output fixes found by driving the binary (pty + pipes): - get/scan --json: the nested apply no longer prints per-package "Error: Failed to patch" lines on stderr (regression from eb131ca); ApplyArgs gains a non-CLI `nested` marker carrying the caller's JSON-ness. The closing "Some patches could not be applied" line drops its stale "re-run without --silent" hint (the per-package lines print under --silent now). - Deterministic order: apply walks packages in PURL order; core apply and rollback walk a patch's files in name order, so the file named in "Cannot apply patch: <file>" is stable run to run. - The nested apply's "Patched packages:" separator goes to stderr: piped stdout of `get`/`scan --yes` no longer carries a double blank line. - select_one fits the prompt and options to the terminal width, so dialoguer's line-count erase leaves no menu rows behind at 40-80 cols. - scan: one blank line opens the post-Summary skip/recorded block and the select menu; a report-only piped scan picks without the non-interactive note; the zero-patch result is said once (stdout); a failed API batch is a "Warning: API batch N of M failed" (none for a one-batch scan) and the fatal line reads "Error: The API query failed: ...". - Progress through StatusLine only: fetch_stage's artifact download and in-memory content fetch (no "(mode: diff)" tag), a per-package vendor progress line, the `--update` version check, setup's search/configure/ apply/remove phases, and the contended-lock wait for every direct apply-lock site (new lock_cli::acquire_with_status). get and hosted scan print the shared "Error: Another socket-patch process ..." line plus the lock-file hint (lock_cli::format_lock_error). - Wording: npm drift warning names the live resolution instead of a Debug `Some("...")`; missing-lockfile refusal lists lockfiles once and says "in the project root" for `.`; "All N packages are already vendored; nothing to do."; repair says "No manifest; no patch artifacts to download." on vendored-only projects, "Would rebuild/download ...:" instead of "Dry run - would ...", and ends the hosted-only sentence with a period; vendor warns when the ledger is unreadable instead of "No manifest found, nothing to vendor."; hosted "No patches could be redirected:" items drop their redundant lead; remove's dry-run footer ("Dry run: no changes made.") closes the whole preview; multi-advisory summaries read "(highest: HIGH)", colored; one non-interactive note wording; "Setup cancelled." / "Hook removal cancelled.". - Errors reach stderr: rollback and setup human runs that exit 1 end with a closing Error: line (the per-item detail stays in the stdout report). - setup --remove preview lists only the scripts the file has, values aligned; no "Proposed changes:" header over an empty preview. - Help: no protocol notes in --vendor-url/--patch-server-url/--vendor-source, --lock-timeout names get/scan, setup --remove names Composer, `--update --version` prints "socket-patch <ver>". Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Wenxin Jiang (Wenxin-Jiang)
approved these changes
Sep 23, 2026
scan now prints counted nouns ("Found 1 package (1 nuget)"), so the
legacy-layout leg's "packages" substring match found no summary line.
Match either number and build the breakdown needle with the right noun.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko)
merged commit Sep 23, 2026
15a6ba6
into
main
281 of 287 checks passed
Mikola Lysenko (mikolalysenko)
added a commit
that referenced
this pull request
Sep 24, 2026
…m lockfiles `socket-patch vex` and the embedded `apply` / `scan` / `vendor --vex` now attest hosted and vendored patches with no `.socket/manifest.json` — and with no `.socket/vendor` ledgers either — by reading the wiring out of the project's lockfiles (`vex::discover`). This covers a depscan-opened PR, a clone that never committed its ledgers, and a lock-only CI checkout. Record view (`commands::vex_sources::plan`): four sources — the manifest, the redirect ledger's records, the vendor ledger's embedded records and lockfile discovery — merge into one view. A candidate's record must carry the uuid the lockfile actually WIRES; it comes from the first source that has one, else (online only) from the patch API by uuid, 10 fetches at a time with `get`'s one-shot 401/403 → public-proxy fallback (#247's uuid-only `ApiClient::fetch_patch`). Nothing is written: vex never takes the apply lock, never creates `.socket/`, never writes the manifest, and the `socket-patch.vendor.json` marker is never a record. Gates, applied before hashing and kept under `--no-verify` (which now skips only the hashing): - a vendor-ledger entry attests only while a lockfile still wires its artifact (`vendor_unwired`); a redirect-ledger record only while a lockfile wires its hosted patch (`redirect_unwired`); discovery is authoritative for every uuid a read file mentions, so a rejected mention keeps nothing alive; - `record_unavailable` (offline, 404, refused, transport error — the run continues), `record_mismatch`, `wiring_conflict` (lockfiles wire one package to several patches); - a malformed / unreadable `.socket/vendor/state.json` is the hard error `vendor_ledger_corrupt` (exit 2), mirroring `redirect_ledger_corrupt` — this supersedes #247's degrade-and-disclose posture for `vex` only; `setup --check` keeps #247's `vendor_context_from` / `warn_unreadable_vendor_state` path. Evidence: vendored refs hash the committed artifact (the ledger entry when it names the wired artifact, else one synthesized from the ref); hosted refs hash the installed copies the build CONSUMES (`vex_consumed::hosted_consumed_copies` → core `VendorContext::hosted` / `HostedCopies`: the Go replacement module, the Socket-registry cargo src dir, maven's suffixed version — never a pristine sibling), and with nothing installed a discovered pinned reference attests from its lockfile pin, like in-run `scan --mode hosted --vex`. Discovered refs bypass the Property 7 ecosystem filter. Commands: - `apply --vex` / `vendor --vex` with no manifest attest what the lockfiles and ledgers wire (nothing anywhere keeps the calm exit 0 and removes a stale document; `apply --check` and `--dry-run` never generate); #247's no-manifest lines ("No patch manifest found; nothing to apply.", "No manifest found, nothing to vendor.") are kept; - failed VEX runs carry the discovery diagnostics into `warnings[]` (standalone envelope, embedded envelopes, scan JSON — hosted included); - `manifest_not_found` now means no manifest AND nothing wired; - human output: `Note:` lines for superseded records / fetch failures, phrased omission reasons. Writer hardening: the manifest-driven standalone `vendor` now embeds the patch `record` in its ledger entries too (vendored mode already does, as `detached` entries). `detached` stays the "no manifest owner" flag, so the manifest reconcile, legacy-manifest migration and get/scan idempotency from #247 are unaffected. Every reader of embedded records shares one ownership rule (`commands::vendor_record_is_unowned`): a detached entry's record always stands alone, a standalone `vendor` entry's fallback copy only when no manifest entry covers it (by ledger key or base purl). `vex`, `list` and `setup --check` (`fold_vendor_records`, formerly `fold_detached_records`) all apply it, so one tree never lists "no patches" while its VEX document attests one. `repair` stays narrower: it keeps preferring a manifest that moved on to a newer uuid and falls back to the embedded copy only with no manifest at all. One liveness rule for vex and scan: `scan`'s cross-mode takeover classification (`classify_overlap_takeover`), its `hosted_wiring_retained` warning and `redirectState.wiringLive` ask the same core discovery (`commands::discover_wiring`) and liveness rule (`Discovery::redirect_record_live` / `vendor_entry_live` / `wires_package`, through one `LedgerLiveness` holder per call site) that gate attestation, replacing scan's private cargo / hosted / vendored checks and its looser text scan. The CLI also shares one purl splitter (`utils::purl::purl_parts`), one vendor-ledger lookup (`vendor::state::lookup_entry_kv`) and one npm alias-aware identity crawl (`ecosystem_dispatch::npm_paths_by_identity`) across vex, scan and vendor, and pairs PEP 723 script locks through `utils::python_lock::script_of_lock` like the rewriters do. Output follows the conventions from #248: `ui::plural` counts, a `ui::StatusLine` progress line for record fetches, and the shared `format_vex_written` / `format_vex_dry_run_skip` lines on the manifest-less `apply --vex` / `vendor --vex` paths. Manifest-less `vex` honours `--dry-run` and `-O -` like the manifest path. Product auto-detection adds go.mod, composer.json, pom.xml, a single `*.csproj` and a single `*.gemspec`, after the existing probes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko)
added a commit
that referenced
this pull request
Sep 24, 2026
…allow-remote auto-config (v5) (#251) * fix(core): hosted/vendored rewriter fixes found by the real-PM matrices First of seven commits that land the manifest-less VEX branch (feat/vex-lockfile-inventory) on top of #247. The branch's ~165-commit history, built on the pre-#247 main 9489b18 with per-PM merge commits, was squashed onto 09956d9 and re-split by concern; the full pre-rebase history is preserved on branch backup/vex-lockfile-pre-rebase. These are the product bugs the per-PM real-toolchain matrices turned up while the manifest-less VEX suites were written. Each is independent of VEX and pinned by a core regression test: - composer (hosted): the rewriter drops the entry's `source` block when it immediately precedes `dist` (one `redirect_composer_dist` fragment edit spanning both, reverted byte-for-byte). Composer 1 and 2.2 LTS silently installed the pristine upstream commit from git whenever the hosted download failed; a hand-ordered source that cannot be dropped warns `redirect_composer_source_kept`. Golden fixture: `source-and-dist`. - gem (hosted): the patch-registry `GEM` section is inserted in bundler's source order (sorted by remote), so a frozen `bundle install` on bundler >= 4.0.19 accepts the converged lock. The `basic` golden and the exact lock expectations move the Socket section first. - cargo (hosted + vendored): a v1 `Cargo.lock` (checksums in `[metadata]`, dependents naming the crate by its full package id) is redirected and vendored correctly: the `[metadata]` line and every dependent's full-id reference follow the source, each fragment its own ledger edit, and `cargo --locked` accepts the result; the vendored detach/restore of a v1 entry is byte-identical. `plan_cargo_lock` keeps #247's multi-source twin disambiguation (`Ambiguous`) and hoisted regexes; the block end now also stops at a trailing `[metadata]` / `[[patch.unused]]`, and the checksum is inserted after a block-final `source` line too. - yarn berry (hosted + vendored): written checksums follow the lock's own spelling — yarn 4.0.0–4.0.2 spell `10c0` checksums as bare hex, so the prefixed form failed `yarn install --immutable` with YN0028. - npm (vendored): npm 12 reifies from the `package-lock.json` it creates beside a committed shrinkwrap, so `vendor` now rewires every present npm lock (siblings first, primary last; an unrewirable sibling warns `vendor_npm_sibling_lock_unwired`), and the in-use/revert probes read every npm lock before deleting an artifact. `select_lockfile` reads through #247's guarded `read_regular_to_bytes`. - npm (hosted): a lockfileVersion 1 redirect warns `redirect_npm_legacy_client` — npm 6 ignores a v1 lock's `resolved` and fails EINTEGRITY against the patched pin. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(hosted): auto-configure npm 12 allow-remote in the project .npmrc npm >= 12 defaults to `allow-remote=none` and refuses (EALLOWREMOTE) every lockfile entry whose tarball is not served by the configured registry — exactly what a hosted redirect writes. When `scan --mode hosted` / `get --mode hosted` leaves a root package-lock.json / npm-shrinkwrap.json carrying a granted hosted artifact URL, the run now ensures `allow-remote=all` in the project `.npmrc` (creating the file, or appending one line with the BOM, CRLF and every other byte preserved) and records it in the redirect ledger as `redirect_npmrc_allow_remote` (`created` / `added`). - core `patch::redirect::npmrc`: npm's `.npmrc` grammar as measured against npm 12.1.0 (exact `allow-remote` key, last top-level assignment wins, `[section]` bodies are not top-level, bare-CR line splits, case- sensitive value), the plan (create / append / already-all / respected user / env / outer-layer value / unsupported), and the unwinds. - Every reversal removes exactly what was added once no package-lock entry needs it: the whole-ledger replay (a new `NpmrcAllowRemote` inverse, grouped with the npm lock kinds), the per-purl npm revert behind scoped rollback / remove / the vendored takeover ("last one out", same transaction, flushed after the lock through #247's shared `staged::flush_staged`), and the vendored-supersedes-hosted reconcile. A modified created file keeps the user's lines (`redirect_npmrc_allow_remote_modified`, surfaced by rollback, remove, vendor and the reconcile). A symlinked `.npmrc` refuses an unwind at plan time, before anything is written. - Hosted run: the `.npmrc` edit rides `rewrite.files` / `rewrite.edits`, so it is written under #247's apply-lock window, after its whole-run SYMLINK GUARD, and only after the ledger persisted — never on `--dry-run` (which previews the write, also for a vendored → hosted takeover; the root locks are now read for such a preview so the pnpm `trustLockfile` preview sees the lock the wet run splices). An explicit user value (project `.npmrc`, user/global/builtin config, or an `npm_config_allow_remote` env var) is respected and named; a symlinked, unreadable or bare-CR `.npmrc` is left alone; `--no-npm-allow-remote- config` / `SOCKET_NO_NPM_ALLOW_REMOTE_CONFIG` opts out. Every variant warns `redirect_npm_allow_remote` with the whole-tree tradeoff. - `atomic_write_bytes_preserving_mode` creates its stage with the destination's permission bits (kept inside #247's `commit_stage` structure), so a 0600 token-bearing `.npmrc` is never staged world- readable. - remove: the hosted leg's advisories are printed inside #247's `unwind_hosted` (so a run that then fails still reports them) and carried into the success envelope's `warnings[]`. Tests: npmrc/replay/takeover/scan unit tests, the flag's parse coverage, `redirect_npm_allow_remote` (plus #247's invariants: the lock never outlives the run, dry runs create no `.socket/`, a full rollback leaves no `.socket/` and never the user's `.npmrc`), and the dry-run takeover previews in `coverage_fix_scan_hosted_dryrun_vendored`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * refactor(core/vendor): split lock_inventory into per-format submodules A verbatim move with no behavior change, so the next commit can give each lock format its shared entry model beside its registry view: `vendor/lock_inventory.rs` becomes a directory module with one file per format (`npm`, `npm_family`, `pnpm`, `yarn`, `bun`, `cargo`, `golang`, `composer`, `gem`, `pypi`), ledger recovery (`recover`) and the rewired-lock trust anchor (`wired`). `mod.rs` keeps the public API (`LockIntegrity`, `LockfileEntry`, `UnsupportedNpmLayout`, `lookup`, `inventory_project(_diagnosed)`, `recover_lock_entry`, `wired_vendor_integrity`); the three test modules move to `tests.rs`, `recover_tests.rs` and `python_lock_union_tests.rs`. Only imports, visibility (helpers another file calls become `pub(super)`), sibling-module paths (`super::state` -> `crate::vendor::state`), module docs and the single-file section banners changed; the test modules lost one indentation level and were re-wrapped by rustfmt. `git show --color-moved` shows everything else as moved. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(core/vex): discover hosted and vendored patch references from lockfiles New read-only module `vex::discover`: `discover_patched_refs(_with)` reads every supported ROOT lockfile / package-manager config and returns the Socket patch wiring it finds — `Discovery { refs, diagnostics, recognized, unlocked_pins, elsewhere }`, one `PatchedRef { purl, uuid, mode (Hosted | Vendored), source_file, artifact_rel, locked_integrity, integrity_required, url }` per live reference. It never touches the network, never writes, and never fails the run: a malformed file is a diagnostic (`lockfile_unreadable`, `lockfile_unparseable`, `patched_ref_invalid`, `patched_ref_unattributable`). One extractor per package-manager family: npm (package-lock / shrinkwrap, pnpm every lock generation + Rush locks), yarn classic and berry, bun (`bun.lock` / `bun.lockb`), cargo, golang (go.mod / go.work + sums), pypi locks (uv, PEP 723 script locks, pylock, poetry, pdm), pypi other (Pipfile.lock, requirements + `-r`, Hatch / PEP 621 direct refs), gem, composer, maven, nuget; deno is explicitly empty. Every file present is read — no precedence chain — because the hosted rewriter edits every candidate it finds. Every value is committed, tamperable data and is validated fail-closed: - hosted: `hosted_patch_uuid` accepts only `https://patch.socket.dev` or a configured `--patch-server-url` origin, no userinfo, and takes the LAST canonical-uuid path segment (grant tokens may be uuid-shaped); percent-encoding, `\/` escapes and fragments are handled; - vendored: root-anchored `.socket/vendor/<eco>/<uuid>/…` paths whose leaf names the entry's own artifact (`vendor_ref` takes the path literally; only yarn / URL-form pip strip their `#…` / `::…` decorations); - pins, not definitions: a registry / index / source definition alone (cargo `[registries]`, nuget `<add>`, pom `<repository>`, uv index tables, `.npmrc`) never makes a reference; - contested locks: a lock that resolves the same name@version from a non-Socket source drops the ref (`patched_ref_unattributable`); - lockless cargo pins / exclusive nuget mappings are recorded as `UnlockedPin`s that can only keep a ledger record live, never create a ref; `recognized` lists every uuid a read file mentions, so a rejected mention keeps nothing alive downstream. Supporting core changes: `patch::redirect::{SOCKET_PATCH_SERVER_HOST, hosted_patch_uuid, hosted_patch_url_uuids}` (the pipenv owner check uses the shared host constant); the pnpm lock grammar and `hosted_url_version` exported crate-wide so readers parse exactly what the writers write; `utils::digest` (the SRI pin rule and the hex digest shapes — one copy for the inventory, discovery, ledger recovery and the rewriters, each call site keeping its case policy); and `utils::purl`'s validating purl builders, which discovery and the inventory's registry views share. A few writer helpers become `pub(crate)` so the extractors' tests derive their fixtures from the writers themselves. One lockfile traversal layer: discovery and `vendor::lock_inventory` (the scan / get / vendor / repair inventory) read each format through ONE reader that yields every entry, Socket-owned ones included — the inventory's registry views drop those, the extractors classify them: - `lock_inventory` becomes a directory module, one file per format, each laid out as a pure entry model, a stat-only file-selection section and the registry view (an architecture test enforces the layering). Entry models: `npm_lock_nodes`; `pnpm_packages` over the hosted rewriter's pnpm grammar (every key generation, CRLF included); the yarn `classic_entries` / `berry_entries` models with one berry locator, cache-key and checksum rule; `BunLockb::parse_packages`; and `composer_lock_packages`, whose array index the composer writer's lock walks use too. - Every other format reads through the reader its writer owns: `cargo_lock::locked_packages` and `cargo_config`'s `[patch]` / `[registries]` walks; `go_mod_edit` / `go_sum_edit` read helpers; a new `vendor::gemfile_lock` model; the `utils::python_lock` / `poetry_lock` / `hatch` readers (uv source fields, script-lock pairing, the pyproject / Hatch declaration walk); a new `utils::requirements` lexer lifted out of the vendored requirements planner; and two new XML readers, `vendor::maven_pom` and `vendor::nuget_config`, with `nuget_feed::nuget_lock_entries` shared by discovery and the feed writer. - `DiscoverCtx::locate` classifies a lock location once (vendored path, hosted uuid, decorated leaf) for every extractor. The npm-family extractors iterate the entry models only, never the grammar primitives, and every extractor reads content only through the recognizing ctx (rule 11) — both enforced by architecture tests. Ledger liveness is one rule too: `Discovery::{wires_package, vendor_entry_live, redirect_record_live}`, held per call site by `LedgerLiveness` (the sorted redirect-ledger files, the lock inventory loaded lazily at most once), which the CLI's vex and scan both use. Cargo crates.io provenance is explicit (`LockfileEntry::source_kind`), not inferred from the checksum variant. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(vex): manifest-less VEX — attest hosted and vendored patches from lockfiles `socket-patch vex` and the embedded `apply` / `scan` / `vendor --vex` now attest hosted and vendored patches with no `.socket/manifest.json` — and with no `.socket/vendor` ledgers either — by reading the wiring out of the project's lockfiles (`vex::discover`). This covers a depscan-opened PR, a clone that never committed its ledgers, and a lock-only CI checkout. Record view (`commands::vex_sources::plan`): four sources — the manifest, the redirect ledger's records, the vendor ledger's embedded records and lockfile discovery — merge into one view. A candidate's record must carry the uuid the lockfile actually WIRES; it comes from the first source that has one, else (online only) from the patch API by uuid, 10 fetches at a time with `get`'s one-shot 401/403 → public-proxy fallback (#247's uuid-only `ApiClient::fetch_patch`). Nothing is written: vex never takes the apply lock, never creates `.socket/`, never writes the manifest, and the `socket-patch.vendor.json` marker is never a record. Gates, applied before hashing and kept under `--no-verify` (which now skips only the hashing): - a vendor-ledger entry attests only while a lockfile still wires its artifact (`vendor_unwired`); a redirect-ledger record only while a lockfile wires its hosted patch (`redirect_unwired`); discovery is authoritative for every uuid a read file mentions, so a rejected mention keeps nothing alive; - `record_unavailable` (offline, 404, refused, transport error — the run continues), `record_mismatch`, `wiring_conflict` (lockfiles wire one package to several patches); - a malformed / unreadable `.socket/vendor/state.json` is the hard error `vendor_ledger_corrupt` (exit 2), mirroring `redirect_ledger_corrupt` — this supersedes #247's degrade-and-disclose posture for `vex` only; `setup --check` keeps #247's `vendor_context_from` / `warn_unreadable_vendor_state` path. Evidence: vendored refs hash the committed artifact (the ledger entry when it names the wired artifact, else one synthesized from the ref); hosted refs hash the installed copies the build CONSUMES (`vex_consumed::hosted_consumed_copies` → core `VendorContext::hosted` / `HostedCopies`: the Go replacement module, the Socket-registry cargo src dir, maven's suffixed version — never a pristine sibling), and with nothing installed a discovered pinned reference attests from its lockfile pin, like in-run `scan --mode hosted --vex`. Discovered refs bypass the Property 7 ecosystem filter. Commands: - `apply --vex` / `vendor --vex` with no manifest attest what the lockfiles and ledgers wire (nothing anywhere keeps the calm exit 0 and removes a stale document; `apply --check` and `--dry-run` never generate); #247's no-manifest lines ("No patch manifest found; nothing to apply.", "No manifest found, nothing to vendor.") are kept; - failed VEX runs carry the discovery diagnostics into `warnings[]` (standalone envelope, embedded envelopes, scan JSON — hosted included); - `manifest_not_found` now means no manifest AND nothing wired; - human output: `Note:` lines for superseded records / fetch failures, phrased omission reasons. Writer hardening: the manifest-driven standalone `vendor` now embeds the patch `record` in its ledger entries too (vendored mode already does, as `detached` entries). `detached` stays the "no manifest owner" flag, so the manifest reconcile, legacy-manifest migration and get/scan idempotency from #247 are unaffected. Every reader of embedded records shares one ownership rule (`commands::vendor_record_is_unowned`): a detached entry's record always stands alone, a standalone `vendor` entry's fallback copy only when no manifest entry covers it (by ledger key or base purl). `vex`, `list` and `setup --check` (`fold_vendor_records`, formerly `fold_detached_records`) all apply it, so one tree never lists "no patches" while its VEX document attests one. `repair` stays narrower: it keeps preferring a manifest that moved on to a newer uuid and falls back to the embedded copy only with no manifest at all. One liveness rule for vex and scan: `scan`'s cross-mode takeover classification (`classify_overlap_takeover`), its `hosted_wiring_retained` warning and `redirectState.wiringLive` ask the same core discovery (`commands::discover_wiring`) and liveness rule (`Discovery::redirect_record_live` / `vendor_entry_live` / `wires_package`, through one `LedgerLiveness` holder per call site) that gate attestation, replacing scan's private cargo / hosted / vendored checks and its looser text scan. The CLI also shares one purl splitter (`utils::purl::purl_parts`), one vendor-ledger lookup (`vendor::state::lookup_entry_kv`) and one npm alias-aware identity crawl (`ecosystem_dispatch::npm_paths_by_identity`) across vex, scan and vendor, and pairs PEP 723 script locks through `utils::python_lock::script_of_lock` like the rewriters do. Output follows the conventions from #248: `ui::plural` counts, a `ui::StatusLine` progress line for record fetches, and the shared `format_vex_written` / `format_vex_dry_run_skip` lines on the manifest-less `apply --vex` / `vendor --vex` paths. Manifest-less `vex` honours `--dry-run` and `-O -` like the manifest path. Product auto-detection adds go.mod, composer.json, pom.xml, a single `*.csproj` and a single `*.gemspec`, after the existing probes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(cli): manifest-less VEX suites, real-PM capstones and shared helpers Hermetic suites (wiremock patch API, run in the default `test` job on every OS): one `e2e_vex_lockfile_<pm>` per package manager — npm, pnpm, yarn (classic + berry), bun, cargo, golang, uv, poetry, pdm, hatch, pipenv, pip, gem, composer, maven, nuget, deno (negative) — covering spoofed hosts, record mismatch, pristine / tampered installs, contested and orphaned wiring, `--offline`, reverted locks (also `--no-verify`) and the embedded commands; plus the cross-cutting `e2e_vex_manifestless_ embedded`, `e2e_vex_redirect`, `e2e_vex_vendor` and the `vex_e2e_common_selftest` harness checks. Real-PM capstones (gated like the existing suites, `_REQUIRED` env pattern): every hosted / vendored build suite now ends in the manifest- less VEX matrix over a fresh checkout — manifest absent (ledgers kept), ledgers deleted (lockfile + API), offline (`record_unavailable`, zero requests), reverted lock (never attested, verify or not) — through shared helpers: `vex_e2e_common` (+ bun / uv matrices), `npm_e2e_common` (+ `manifestless`), `yarn_berry_common`, `common/yarn_classic_vex`, `common/bundler_e2e`, `cargo_e2e_matrix`, `golang_e2e_matrix`, `maven_build_common`, `composer_e2e_common`, `vex_pdm_hatch_common`, `vex_pipenv_pip_*`, `vex_pypi_real_common`. New real-toolchain suites: `e2e_redirect_{composer,maven,uv}_build`, `e2e_vendor_maven_build`, `e2e_golang_workspace_build`, `e2e_nuget_dotnet_build`, `e2e_poetry_vex_build`, `e2e_vex_{pdm,pip,pipenv,hatch}_build`, `e2e_deno_vex_build`; the docker / setup-matrix / production legs gain their manifest-less VEX steps. Re-pinned to #247 while rebasing: - vendored mode writes no manifest: the "manifest deleted" steps are naturally manifest-free, so the npm manifest-less matrix and the uv / poetry vendored suites add a `legacy-manifest` cell (the record a pre-5.0 vendored run left beside its ledger, via `vex_e2e_common::seed_legacy_manifest`, must attest the same way), the yarn 2/3 refusal suite seeds that legacy record explicitly (and still requires `not_applied`, never an attestation), and `--detached` twins assert no manifest in either spelling; - a fully reverted project keeps no `.socket/`: suites that plant a stale ledger back after a rollback recreate the directory first, and the hosted rollback suite checks that `vex` does not recreate it; - the no-manifest human lines follow #247's wording; - a corrupt vendor ledger on a manifest-free project is now `vendor_ledger_corrupt` (exit 2, the ledger named, never rewritten), replacing #247's disclose-then-`manifest_not_found` expectation; - repair: two tests pin the standalone-`vendor` embedded record (a moved-on manifest still wins; with no manifest the embedded copy stands in offline), replacing the pre-rebase test that assumed a non-detached `scan --vendor` entry. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * ci: run the manifest-less VEX real-PM matrices and backtest steps ci.yml: - `test` exports `SOCKET_PATCH_GO_E2E_REQUIRED` / `_VERSION` so the real-go hosted/vendored suites fail instead of skipping on the Go that is already installed for vexctl; - `e2e` gains pinned-toolchain legs, each ending in the manifest-less VEX matrix: composer 1 / 2.2 / 2 (hosted + vendored), bundler 1.17.3 → 4.0.21 (hosted + vendored + setup_matrix_gem), bun text/binary-lock eras, the named corepack pnpm legs (3 OS), uv 0.1.45 → 0.12.x hosted + vendored, poetry / pdm / hatch / pipenv / pip, maven 3.6.3 → 4.0.0-rc-6, dotnet 6 → 10, deno; npm legs hard-require npm where `Command::new("npm")` can resolve it; - new jobs: `yarn-classic-matrix` (1.0.2 → 1.22.22), `yarn-berry-e2e` (4.0.2 → 4.18.0 + macOS/Windows), `cargo-vex-matrix` (toolchains × Cargo.lock v1–v4); `e2e-docker` also runs the pypi vendored-PM suite. Compatibility workflows: npm (npm 6–12 capstones, new), go (1.18.10 → 1.26.3, new), poetry (new), and the pnpm / pdm / pipenv / bun ones run the manifest-less VEX steps and trigger on the vex sources. The bun workflow keeps #247's two modes (hosted, vendored — the vendored- detached leg collapsed into vendored). Scripts: the bun / pdm / pipenv / poetry / uv backtests gain the manifest-less VEX checks (a fresh checkout of the committed state must be attested, also with the ledgers deleted; `--offline` with no ledger omits `record_unavailable`; a reverted lock never attests, also under `--no-verify`; a refused lock format attests nothing — the pdm refusal check follows #247's manifest-free footprint), plus the `{uv,yarn-berry,yarn-classic}-vex-matrix.sh` drivers and harness unit tests. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * docs: manifest-less VEX, npm 12 allow-remote and the v5 changelog - CHANGELOG [Unreleased] (both entries are v5 MAJOR): the semver note now lists #247's changes and `vex` refusing stale ledger records and corrupt vendor ledgers; Changed (BREAKING) gains the ledger-liveness / `vendor_ledger_corrupt` entry (and #247's Fixed bullet that had `vex` disclose an unreadable ledger now points at it); Added gains the npm 12 `allow-remote` auto-config, manifest-less VEX, standalone `vendor` record embedding and the new product probes; Fixed gains the composer / gem / cargo v1 / yarn 4.0 / npm dual-lock rewriter fixes after #247's own entries. - CLI_CONTRACT.md: new "Manifest-less VEX (lockfile discovery)" section (inputs, per-ecosystem recognition table, record resolution, verification basis, liveness gates, run warnings), "Patch hosts", the embedded-VEX no-manifest rules, `manifest_not_found` for `vex`, the vendor ledger's `record` semantics (vendored-mode `detached` entries plus the standalone `vendor` fallback copy; the reconcile exemption keys on `detached`), and the new rollback warning code. - README: "No manifest needed for hosted and vendored patches", the rewritten `vex` how-it-works steps and product probes, "npm compatibility (hosted mode and npm 12)", the hosted `.npmrc` commit hint, and the ledger-liveness note under "Undo things". - docs/testing: npm-compatibility.md (npm 6–12, new), uv and bun tables, ecosystems.md. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(vex): golden snapshot of discovery over committed fixtures Pin the exact output of lockfile discovery over every committed fixture project, so a behavior change to discovery (or to the lock readers it shares with the rewriters and vendor::lock_inventory) shows up as a golden diff to review. - Corpus: 182 projects under crates/socket-patch-core/tests/fixtures/ — every redirect case's input/ and expected/ tree, pnpm-hosted, poetry, pipenv, bun-lockb captures, and each pdm-native lock staged as pdm.lock — run through the full orchestrator and checked against rule 11's recognition-covers-refs invariant. - Rendering (src/vex/discover/testing/golden.rs): every PatchedRef field (plus lockfile_basis_ok), diagnostics (tempdir root and OS error numbers normalized), recognized identities, unlocked pins, resolved-elsewhere entries, and the live hosted / vendored ledger claims. The destructuring is exhaustive, so a new field fails to compile until the golden covers it. - Goldens: one JSON per fixture family (redirect-<eco>, bun-lockb, pdm-native, pipenv, pnpm-hosted, poetry) under tests/fixtures/vex-discover-golden/, mapping fixture path to output. Missing and orphaned entries fail. Regenerate after an intended change with SOCKET_PATCH_UPDATE_GOLDEN=1 cargo test -p socket-patch-core --lib vex::discover::testing::golden. Unix only: Windows checkouts convert some fixtures' line endings. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
scan's progress lines garbled on a real terminal. Warnings also printed in the middle of an active progress line.An end-to-end audit ran every command under a pty, through pipes, and with
--json,--silentandNO_COLOR. It found 243 verified issues of the same kind across the CLI. This PR fixes them. It also moves all terminal-UI logic into one small, unit-tested module.New
crates/socket-patch-cli/src/ui/This module replaces
output.rsand drops the unusedindicatifdependency.StatusLine<W: Write>: a hand-rolled, deterministic status line. There is no ticker thread, so the output bytes are exact and testable.\r\x1b[2Kplus the message, truncated to the terminal width.printlnputs a line above an active status line.TERM=dumb, in debug mode, or under--json/--silent.confirm,confirm_or_proceed,select_one), built on a testableconfirm_withcore:--silent.color_enabledpolicy (NO_COLOR/CLICOLOR/CLICOLOR_FORCE/TERM=dumb), also passed to dialoguer. Cells are padded before they are painted, so colored table rows line up.plural, and atruncatethat is char-safe and cuts at word boundaries.utils::noticequiet switch. Info advisories (the proxy notice) are muted under--silent/--jsonand print once per process instead of 2–3 times. Warnings (token shape) are muted only by--silent.Per-command polish
Covers
scan(report-only / agent / hosted / vendored),get,apply,rollback,remove,vendor,repair,list, lock waits,--update, the update notifier,vexandsetup:(s).Error:/Warning:prefixes; progress and warnings go to stderr.--silentis errors-only, but a run that fails still prints why.--helpno longer shows developer notes (now pinned byhelp_text_hygiene.rsacross every subcommand page).get --dry-runandvex --dry-runused to write files; they no longer do.vex -O -used to create a file named-.scan --jsoncould stop at an interactive menu.Built on top of #247, which is on main now. #247's behavior and contract are unchanged. The merge was checked by running 46 flows side by side against #247's binary: exit codes, the final
.socket/trees, and the--jsonshapes were identical. Only the human-readable wording differs.Test plan
StatusLinebyte output, including the exact regression above, shorter-after-longer, println while active, clear on drop, no escapes off a TTY, and multibyte width truncation;color_enabledtruth table;confirm_withfor empty input, y/Y/yes/n, garbage, EOF, invalid UTF-8, and non-interactive with and without--silent;pluralandtruncate;format_*helpers.cargo test -p socket-patch-cli -p socket-patch-core --no-fail-fast: 0 failures. Counts: core 4191 passed; cli 3543 passed. The knownupdate_notifier_e2e::grace_budget_bounds_command_latencyload flake passes when run on its own.cargo clippy -p socket-patch-cli -p socket-patch-core --all-targets -- -D warningsis clean. This also clears 9 lints Rust 1.93 raised in files this PR doesn't otherwise touch.FlushConsoleInputBuffertypeahead path only compile undercfg(windows). They were checked in a stand-alone crate, but not in a full Windows build. CI's Windows leg is the first real run.Deferred (left out to keep scope; each is a product or contract call)
file:.socket/vendorentries.{code, message}, and per-purl events for partial apply.vendor --revert --dry-rundrift prediction;warnings[].detail; the./path prefix across commands;setup --check --silent; exit 1 on a declined--update.🤖 Generated with Claude Code
Note
Medium Risk
Touches interactive prompts, progress rendering, and dry-run paths across most subcommands; behavior is heavily tested but Windows console paths are CI-dependent and several contract/doc tweaks describe user-visible output changes.
Overview
Replaces
indicatifand scattered output helpers with a singlecrates/socket-patch-cli/src/ui/module: a testableStatusLinefor progress (self-clearing\rredraws, no drawing off-TTY/--json/--silent/debug/TERM=dumb), shared prompts (EOF declines, typeahead flush—including WindowsFlushConsoleInputBuffer), color policy (NO_COLOR/CLICOLOR*), andplural/truncate/ padded table cells.Human-facing behavior is aligned across commands: proper singular/plural wording,
Error:/Warning:on stderr, sorted formerly-random output,--helpgrouped under “Global options” without dev notes, and--silentstill surfacing failure reasons. Bug fixes:get --dry-run(all modes, including agent) andvex --dry-runno longer mutate disk;vex -O -writes to stdout;scan --jsonno longer blocks on menus; API errors show server messages instead of raw JSON.Docs/contracts (
CHANGELOG,README,CLI_CONTRACT.md) are updated to match (rollback prompt copy, non-TTY scan/hosted notes,getnarrowing display, VEX/listwarnings, npm update hints, new error/warning codes). Dependencies: dropindicatif, addconsole+windows-syson Windows.Reviewed by Cursor Bugbot for commit 37e8d06. Configure here.