Skip to content

Commit afa7253

Browse files
Add in-memory hosted engine and napi addon (#276)
* Add in-memory hosted scan engine Let hosts run hosted-mode patching without a working directory or network access. The new hosted_memory engine takes a map of repository files, finds every project root, looks up patches once across all roots through a pluggable PatchApi provider, and returns the rewritten lockfiles and redirect ledgers. Lockfile inventory now reads through a ProjectView, so the same parsers serve disk and in-memory projects; disk behavior is unchanged. The existing scan --mode hosted path keeps using ApiClient directly, which also implements PatchApi. Parity tests compare the engine against the real CLI binary across npm, pnpm, yarn, bun, pypi, cargo, composer, gem and golang fixtures. A hidden hosted-bundle command exposes the engine for debugging. Assisted-by: Claude Code:claude-opus-5-5 * Add napi addon for the hosted engine Expose the in-memory hosted engine to Node as a native addon, so a service can stream repository files into a scan session and serve patch lookups from its own data source instead of the Socket API. The package lives under crates/socket-patch-node/npm and is private, so the npm release tooling does not publish it. Release builds now build only the CLI crate, and a new CI job builds the addon and runs its Node smoke test. Assisted-by: Claude Code:claude-opus-5-5 * Use an External handle for addon sessions Export the scan session as an opaque External handle plus plain functions, not a napi class. The class wrapper's generated glue unwraps its native pointer through an FFI out-parameter, which CodeQL reports as a possible invalid pointer dereference. The public HostedScanSession in index.js is unchanged; it now calls the handle functions. Assisted-by: Claude Code:claude-opus-5-5 * Scan excluded nested Cargo projects Keep every directory with its own Cargo.lock as a project root during path selection. Whether a nested lock belongs to an enclosing workspace depends on that workspace's members and exclude lists, which only the engine can read. Crates the workspace excludes are independent projects and are now scanned; real members still have their unused lock ignored. Point the in-memory Pipenv warning at the pipenvMajor option, the only input the engine reads, instead of PATH or an environment variable. Assisted-by: Claude Code:claude-opus-5-5
1 parent 8c381ec commit afa7253

49 files changed

Lines changed: 9734 additions & 236 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/ci.yml‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,38 @@ jobs:
7474
exit 1
7575
fi
7676
77+
# The napi addon is only ever loaded by Node, so cargo's own tests never
78+
# exercise its JS loader or the engine/provider boundary.
79+
node-addon:
80+
runs-on: ubuntu-latest
81+
timeout-minutes: 30
82+
steps:
83+
- name: Checkout
84+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
85+
with:
86+
persist-credentials: false
87+
88+
- name: Install Rust
89+
run: rustup show
90+
91+
- name: Cache cargo
92+
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
93+
with:
94+
save-if: ${{ github.ref == 'refs/heads/main' }}
95+
96+
- name: Setup Node.js
97+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
98+
with:
99+
node-version: '20.20.2'
100+
101+
- name: Build addon
102+
env:
103+
SOCKET_PATCH_NODE_CARGO_PROFILE: dev
104+
run: node crates/socket-patch-node/npm/scripts/build-addon.mjs
105+
106+
- name: Smoke-test addon
107+
run: node --test crates/socket-patch-node/npm/test/smoke.mjs
108+
77109
# Lint the out-of-workspace packaging artifacts: the RubyGems CLI launcher
78110
# gem + the Bundler plugin gem (Ruby), and the curl|sh installer. Ruby is
79111
# pre-installed on the ubuntu-latest runner.

‎.github/workflows/release.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,11 +175,11 @@ jobs:
175175

176176
- name: Build (cargo)
177177
if: matrix.build-tool == 'cargo'
178-
run: cargo build --release --target ${{ matrix.target }}
178+
run: cargo build --release -p socket-patch-cli --target ${{ matrix.target }}
179179

180180
- name: Build (cross)
181181
if: matrix.build-tool == 'cross'
182-
run: cross build --release --target ${{ matrix.target }}
182+
run: cross build --release -p socket-patch-cli --target ${{ matrix.target }}
183183

184184
- name: Package (unix)
185185
if: matrix.archive == 'tar.gz'

‎Cargo.lock‎

Lines changed: 112 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22
members = [
33
"crates/socket-patch-core",
44
"crates/socket-patch-cli",
5+
"crates/socket-patch-node",
6+
]
7+
# Bare `cargo build` (release.yml's per-target builds) skips the Node addon,
8+
# which no release artifact ships; `--workspace` and `-p` still build it.
9+
default-members = [
10+
"crates/socket-patch-core",
11+
"crates/socket-patch-cli",
512
]
613
resolver = "2"
714

@@ -21,6 +28,7 @@ sha1 = "=0.10.6"
2128
hex = "=0.4.3"
2229
reqwest = { version = "=0.12.28", features = ["rustls-tls", "json"], default-features = false }
2330
tokio = { version = "=1.50.0", features = ["full"] }
31+
tokio-util = "=0.7.18"
2432
futures-util = { version = "=0.3.32", default-features = false, features = ["std"] }
2533
thiserror = "=2.0.18"
2634
walkdir = "=2.5.0"
@@ -48,6 +56,9 @@ wiremock = "=0.6.5"
4856
portable-pty = "=0.9.0"
4957
base64 = "=0.22.1"
5058
serial_test = "=3.4.0"
59+
napi = { version = "=3.13.0", features = ["napi8", "tokio_rt"] }
60+
napi-derive = "=3.6.9"
61+
napi-build = "=2.5.0"
5162

5263
[profile.release]
5364
strip = true
@@ -65,6 +76,12 @@ inherits = "release"
6576
lto = false
6677
strip = "none" # unstripped test binaries => usable backtraces on failure
6778

79+
# The Node addon (crates/socket-patch-node) for hosts that load it
80+
# in-process: release semantics without the slow full-LTO link.
81+
[profile.addon]
82+
inherits = "release"
83+
lto = "thin"
84+
6885
# Test-execution speed: `cargo test` builds dependencies with the dev
6986
# profile; at opt-level 0 the hash/compression/bsdiff hot loops are 10-100x
7087
# slower (the self_update fixture family measured 403s debug vs ~2s release

‎crates/socket-patch-cli/CLI_CONTRACT.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ This document defines the **public surface** of the `socket-patch` binary. Anyth
2727

2828
**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).
2929

30+
**Internal `hosted-bundle` subcommand.** `socket-patch hosted-bundle` is a hidden, INTERNAL parity/debug harness for the in-memory hosted engine (`src/hosted_memory/`, the engine the Node addon embeds): it reads a JSON bundle `{"files": {path: text}, "binaryFiles"?: {path: base64}, "presentOnly"?: [path], "symlinks"?: [path], "projectRoots"?: [dir], "pipenvMajor"?: n, "batchSize"?: n}` on stdin, queries the authenticated org API built from `--api-url` / `--api-token` / `--org` only (both of the latter are required; no public-proxy fallback), and prints the engine result — or `{"status":"error","error":{"code","message"}}` with exit 1 (exit 2 for unusable input or missing credentials). It never touches the filesystem. Its name, input and output carry NO stability guarantee; do not script it.
31+
3032
## Global arguments
3133

3234
In v3.0 every subcommand accepts the same set of "global" flags via a single shared `GlobalArgs` struct that's `#[command(flatten)]`-ed into each per-command struct (`crates/socket-patch-cli/src/args.rs`). Subcommands that don't actually consume a given flag accept it silently — e.g. `list --global` parses fine and is a no-op. Every flag also has an environment-variable binding; precedence is **CLI arg > env var > default** — and for exactly three keys (`--api-token`, `--org`, `--api-url`) the JS socket-cli's persisted login sits between env var and default: **CLI arg > env var (canonical, then `SOCKET_CLI_*` alias) > socket-cli `config.json` > default**. See "Persisted configuration" under Environment variables.

‎crates/socket-patch-cli/Cargo.toml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ clap = { workspace = true }
2222
serde = { workspace = true }
2323
serde_json = { workspace = true }
2424
tokio = { workspace = true }
25+
tokio-util = { workspace = true }
26+
base64 = { workspace = true }
2527
futures-util = { workspace = true }
2628
console = { workspace = true }
2729
dialoguer = { workspace = true }

0 commit comments

Comments
 (0)