From 374de97782be3d110731b4a1f570f4eaa9d56e42 Mon Sep 17 00:00:00 2001 From: Jake Verbaten Date: Fri, 3 Apr 2026 10:14:17 +0100 Subject: [PATCH 1/3] Add explicit node types for TypeScript 6 compat (#51) TypeScript 6.0 changed the default types behavior so node types are no longer auto-included. This fixes builds in projects that use TS 6. Made-with: Cursor --- npm/socket-patch/tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/npm/socket-patch/tsconfig.json b/npm/socket-patch/tsconfig.json index 1b950f9..6998287 100644 --- a/npm/socket-patch/tsconfig.json +++ b/npm/socket-patch/tsconfig.json @@ -9,7 +9,8 @@ "rootDir": "src", "strict": true, "skipLibCheck": true, - "esModuleInterop": true + "esModuleInterop": true, + "types": ["node"] }, "include": ["src"] } From 2e973444ae9176058d1f673c3ec71203a11de5e5 Mon Sep 17 00:00:00 2001 From: Mikola Lysenko Date: Wed, 8 Apr 2026 17:29:01 -0400 Subject: [PATCH 2/3] fix: use npx @socketsecurity/socket-patch and add dependencies script Update the setup command to generate the correct npx/pnpx command prefix based on lockfile detection, and configure both postinstall and dependencies lifecycle scripts. - Add PackageManager enum (Npm/Pnpm) with lockfile detection - Generate `npx @socketsecurity/socket-patch apply` for npm projects - Generate `pnpx @socketsecurity/socket-patch apply` for pnpm projects - Add dependencies lifecycle script alongside postinstall - Thread PackageManager through detect -> update -> setup pipeline Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.lock | 4 +- crates/socket-patch-cli/src/commands/setup.rs | 38 +- .../src/package_json/detect.rs | 417 +++++++++++++----- .../src/package_json/find.rs | 45 ++ .../src/package_json/update.rs | 96 ++-- 5 files changed, 452 insertions(+), 148 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e89f326..0fc935b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1218,7 +1218,7 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket-patch-cli" -version = "1.7.1" +version = "2.0.0" dependencies = [ "clap", "dialoguer", @@ -1236,7 +1236,7 @@ dependencies = [ [[package]] name = "socket-patch-core" -version = "1.7.1" +version = "2.0.0" dependencies = [ "hex", "once_cell", diff --git a/crates/socket-patch-cli/src/commands/setup.rs b/crates/socket-patch-cli/src/commands/setup.rs index 2d06899..177bab0 100644 --- a/crates/socket-patch-cli/src/commands/setup.rs +++ b/crates/socket-patch-cli/src/commands/setup.rs @@ -1,5 +1,6 @@ use clap::Args; -use socket_patch_core::package_json::find::find_package_json_files; +use socket_patch_core::package_json::detect::PackageManager; +use socket_patch_core::package_json::find::{detect_package_manager, find_package_json_files}; use socket_patch_core::package_json::update::{update_package_json, UpdateStatus}; use std::io::{self, Write}; use std::path::{Path, PathBuf}; @@ -47,14 +48,20 @@ pub async fn run(args: SetupArgs) -> i32 { return 0; } + // Detect package manager from lockfiles in the project root. + let pm = detect_package_manager(&args.cwd).await; + if !args.json { println!("Found {} package.json file(s)", package_json_files.len()); + if pm == PackageManager::Pnpm { + println!("Detected pnpm project (using pnpx)"); + } } // Preview changes (always preview first) let mut preview_results = Vec::new(); for loc in &package_json_files { - let result = update_package_json(&loc.path, true).await; + let result = update_package_json(&loc.path, true, pm).await; preview_results.push(result); } @@ -81,11 +88,20 @@ pub async fn run(args: SetupArgs) -> i32 { let rel_path = pathdiff(&result.path, &args.cwd); println!(" + {rel_path}"); if result.old_script.is_empty() { - println!(" Current: (no postinstall script)"); + println!(" postinstall: (no script)"); + } else { + println!(" postinstall: \"{}\"", result.old_script); + } + println!(" -> postinstall: \"{}\"", result.new_script); + if result.old_dependencies_script.is_empty() { + println!(" dependencies: (no script)"); } else { - println!(" Current: \"{}\"", result.old_script); + println!(" dependencies: \"{}\"", result.old_dependencies_script); } - println!(" New: \"{}\"", result.new_script); + println!( + " -> dependencies: \"{}\"", + result.new_dependencies_script + ); } println!(); } @@ -162,7 +178,7 @@ pub async fn run(args: SetupArgs) -> i32 { } let mut results = Vec::new(); for loc in &package_json_files { - let result = update_package_json(&loc.path, false).await; + let result = update_package_json(&loc.path, false, pm).await; results.push(result); } @@ -176,6 +192,10 @@ pub async fn run(args: SetupArgs) -> i32 { "updated": updated, "alreadyConfigured": already, "errors": errs, + "packageManager": match pm { + PackageManager::Npm => "npm", + PackageManager::Pnpm => "pnpm", + }, "files": results.iter().map(|r| { serde_json::json!({ "path": r.path, @@ -210,6 +230,10 @@ pub async fn run(args: SetupArgs) -> i32 { "alreadyConfigured": already, "errors": errs, "dryRun": true, + "packageManager": match pm { + PackageManager::Npm => "npm", + PackageManager::Pnpm => "pnpm", + }, "files": preview_results.iter().map(|r| { serde_json::json!({ "path": r.path, @@ -220,6 +244,8 @@ pub async fn run(args: SetupArgs) -> i32 { }, "oldScript": r.old_script, "newScript": r.new_script, + "oldDependenciesScript": r.old_dependencies_script, + "newDependenciesScript": r.new_dependencies_script, "error": r.error, }) }).collect::>(), diff --git a/crates/socket-patch-core/src/package_json/detect.rs b/crates/socket-patch-core/src/package_json/detect.rs index f898dcd..dc38803 100644 --- a/crates/socket-patch-core/src/package_json/detect.rs +++ b/crates/socket-patch-core/src/package_json/detect.rs @@ -1,5 +1,19 @@ -/// The command to run for applying patches via socket CLI. -const SOCKET_PATCH_COMMAND: &str = "socket patch apply --silent --ecosystems npm"; +/// Package manager type for selecting the correct command prefix. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum PackageManager { + Npm, + Pnpm, +} + +/// Get the socket-patch apply command for the given package manager. +fn socket_patch_command(pm: PackageManager) -> &'static str { + match pm { + PackageManager::Npm => "npx @socketsecurity/socket-patch apply --silent --ecosystems npm", + PackageManager::Pnpm => { + "pnpx @socketsecurity/socket-patch apply --silent --ecosystems npm" + } + } +} /// Legacy command patterns to detect existing configurations. const LEGACY_PATCH_PATTERNS: &[&str] = &[ @@ -8,120 +22,176 @@ const LEGACY_PATCH_PATTERNS: &[&str] = &[ "socket patch apply", ]; -/// Status of postinstall script configuration. +/// Check if a script string contains any known socket-patch apply pattern. +fn script_is_configured(script: &str) -> bool { + LEGACY_PATCH_PATTERNS + .iter() + .any(|pattern| script.contains(pattern)) +} + +/// Status of setup script configuration (both postinstall and dependencies). #[derive(Debug, Clone)] -pub struct PostinstallStatus { - pub configured: bool, - pub current_script: String, +pub struct ScriptSetupStatus { + pub postinstall_configured: bool, + pub postinstall_script: String, + pub dependencies_configured: bool, + pub dependencies_script: String, pub needs_update: bool, } -/// Check if a postinstall script is properly configured for socket-patch. -pub fn is_postinstall_configured(package_json: &serde_json::Value) -> PostinstallStatus { - let current_script = package_json - .get("scripts") +/// Check if package.json scripts are properly configured for socket-patch. +/// Checks both the postinstall and dependencies lifecycle scripts. +pub fn is_setup_configured(package_json: &serde_json::Value) -> ScriptSetupStatus { + let scripts = package_json.get("scripts"); + + let postinstall_script = scripts .and_then(|s| s.get("postinstall")) .and_then(|v| v.as_str()) .unwrap_or("") .to_string(); + let postinstall_configured = script_is_configured(&postinstall_script); - let configured = LEGACY_PATCH_PATTERNS - .iter() - .any(|pattern| current_script.contains(pattern)); - - PostinstallStatus { - configured, - current_script, - needs_update: !configured, + let dependencies_script = scripts + .and_then(|s| s.get("dependencies")) + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + let dependencies_configured = script_is_configured(&dependencies_script); + + ScriptSetupStatus { + postinstall_configured, + postinstall_script, + dependencies_configured, + dependencies_script, + needs_update: !postinstall_configured || !dependencies_configured, } } -/// Check if a postinstall script string is configured for socket-patch. -pub fn is_postinstall_configured_str(content: &str) -> PostinstallStatus { +/// Check if a package.json content string is properly configured. +pub fn is_setup_configured_str(content: &str) -> ScriptSetupStatus { match serde_json::from_str::(content) { - Ok(val) => is_postinstall_configured(&val), - Err(_) => PostinstallStatus { - configured: false, - current_script: String::new(), + Ok(val) => is_setup_configured(&val), + Err(_) => ScriptSetupStatus { + postinstall_configured: false, + postinstall_script: String::new(), + dependencies_configured: false, + dependencies_script: String::new(), needs_update: true, }, } } -/// Generate an updated postinstall script that includes socket-patch. -pub fn generate_updated_postinstall(current_postinstall: &str) -> String { - let trimmed = current_postinstall.trim(); +/// Generate an updated script that includes the socket-patch apply command. +/// If already configured, returns unchanged. Otherwise prepends the command. +pub fn generate_updated_script(current_script: &str, pm: PackageManager) -> String { + let command = socket_patch_command(pm); + let trimmed = current_script.trim(); // If empty, just add the socket-patch command. if trimmed.is_empty() { - return SOCKET_PATCH_COMMAND.to_string(); + return command.to_string(); } // If any socket-patch variant is already present, return unchanged. - let already_configured = LEGACY_PATCH_PATTERNS - .iter() - .any(|pattern| trimmed.contains(pattern)); - if already_configured { + if script_is_configured(trimmed) { return trimmed.to_string(); } // Prepend socket-patch command so it runs first. - format!("{SOCKET_PATCH_COMMAND} && {trimmed}") + format!("{command} && {trimmed}") } -/// Update a package.json Value with the new postinstall script. -/// Returns (modified, new_script). +/// Update a package.json Value with socket-patch in both postinstall and +/// dependencies scripts. +/// Returns (modified, new_postinstall, new_dependencies). pub fn update_package_json_object( package_json: &mut serde_json::Value, -) -> (bool, String) { - let status = is_postinstall_configured(package_json); + pm: PackageManager, +) -> (bool, String, String) { + let status = is_setup_configured(package_json); if !status.needs_update { - return (false, status.current_script); + return ( + false, + status.postinstall_script, + status.dependencies_script, + ); } - let new_postinstall = generate_updated_postinstall(&status.current_script); - // Ensure scripts object exists if package_json.get("scripts").is_none() { package_json["scripts"] = serde_json::json!({}); } - package_json["scripts"]["postinstall"] = - serde_json::Value::String(new_postinstall.clone()); - - (true, new_postinstall) + let mut modified = false; + + let new_postinstall = if !status.postinstall_configured { + modified = true; + let s = generate_updated_script(&status.postinstall_script, pm); + package_json["scripts"]["postinstall"] = serde_json::Value::String(s.clone()); + s + } else { + status.postinstall_script + }; + + let new_dependencies = if !status.dependencies_configured { + modified = true; + let s = generate_updated_script(&status.dependencies_script, pm); + package_json["scripts"]["dependencies"] = serde_json::Value::String(s.clone()); + s + } else { + status.dependencies_script + }; + + (modified, new_postinstall, new_dependencies) } -/// Parse package.json content and update it with socket-patch postinstall. -/// Returns (modified, new_content, old_script, new_script). +/// Parse package.json content and update it with socket-patch scripts. +/// Returns (modified, new_content, old_postinstall, new_postinstall, +/// old_dependencies, new_dependencies). pub fn update_package_json_content( content: &str, -) -> Result<(bool, String, String, String), String> { + pm: PackageManager, +) -> Result<(bool, String, String, String, String, String), String> { let mut package_json: serde_json::Value = serde_json::from_str(content).map_err(|e| format!("Invalid package.json: {e}"))?; - let status = is_postinstall_configured(&package_json); + let status = is_setup_configured(&package_json); if !status.needs_update { return Ok(( false, content.to_string(), - status.current_script.clone(), - status.current_script, + status.postinstall_script.clone(), + status.postinstall_script, + status.dependencies_script.clone(), + status.dependencies_script, )); } - let (_, new_script) = update_package_json_object(&mut package_json); + let old_postinstall = status.postinstall_script.clone(); + let old_dependencies = status.dependencies_script.clone(); + + let (_, new_postinstall, new_dependencies) = + update_package_json_object(&mut package_json, pm); let new_content = serde_json::to_string_pretty(&package_json).unwrap() + "\n"; - Ok((true, new_content, status.current_script, new_script)) + Ok(( + true, + new_content, + old_postinstall, + new_postinstall, + old_dependencies, + new_dependencies, + )) } #[cfg(test)] mod tests { use super::*; + // ── is_setup_configured ───────────────────────────────────────── + #[test] fn test_not_configured() { let pkg: serde_json::Value = serde_json::json!({ @@ -130,145 +200,262 @@ mod tests { "build": "tsc" } }); - let status = is_postinstall_configured(&pkg); - assert!(!status.configured); + let status = is_setup_configured(&pkg); + assert!(!status.postinstall_configured); + assert!(!status.dependencies_configured); assert!(status.needs_update); } #[test] - fn test_already_configured() { + fn test_postinstall_configured_dependencies_not() { let pkg: serde_json::Value = serde_json::json!({ "name": "test", "scripts": { - "postinstall": "socket patch apply --silent --ecosystems npm" + "postinstall": "npx @socketsecurity/socket-patch apply --silent --ecosystems npm" } }); - let status = is_postinstall_configured(&pkg); - assert!(status.configured); + let status = is_setup_configured(&pkg); + assert!(status.postinstall_configured); + assert!(!status.dependencies_configured); + assert!(status.needs_update); + } + + #[test] + fn test_both_configured() { + let pkg: serde_json::Value = serde_json::json!({ + "name": "test", + "scripts": { + "postinstall": "npx @socketsecurity/socket-patch apply --silent --ecosystems npm", + "dependencies": "npx @socketsecurity/socket-patch apply --silent --ecosystems npm" + } + }); + let status = is_setup_configured(&pkg); + assert!(status.postinstall_configured); + assert!(status.dependencies_configured); assert!(!status.needs_update); } #[test] - fn test_generate_empty() { - assert_eq!( - generate_updated_postinstall(""), - "socket patch apply --silent --ecosystems npm" - ); + fn test_legacy_socket_patch_apply_recognized() { + let pkg: serde_json::Value = serde_json::json!({ + "scripts": { + "postinstall": "socket patch apply --silent --ecosystems npm", + "dependencies": "socket-patch apply" + } + }); + let status = is_setup_configured(&pkg); + assert!(status.postinstall_configured); + assert!(status.dependencies_configured); + assert!(!status.needs_update); } #[test] - fn test_generate_prepend() { - assert_eq!( - generate_updated_postinstall("echo done"), - "socket patch apply --silent --ecosystems npm && echo done" - ); + fn test_no_scripts() { + let pkg: serde_json::Value = serde_json::json!({"name": "test"}); + let status = is_setup_configured(&pkg); + assert!(!status.postinstall_configured); + assert!(status.postinstall_script.is_empty()); + assert!(!status.dependencies_configured); + assert!(status.dependencies_script.is_empty()); } #[test] - fn test_generate_already_configured() { - let current = "socket-patch apply && echo done"; - assert_eq!(generate_updated_postinstall(current), current); + fn test_no_postinstall() { + let pkg: serde_json::Value = serde_json::json!({ + "scripts": {"build": "tsc"} + }); + let status = is_setup_configured(&pkg); + assert!(!status.postinstall_configured); + assert!(status.postinstall_script.is_empty()); } - // ── Group 4: expanded edge cases ───────────────────────────────── + // ── is_setup_configured_str ───────────────────────────────────── #[test] - fn test_is_postinstall_configured_str_invalid_json() { - let status = is_postinstall_configured_str("not json"); - assert!(!status.configured); + fn test_configured_str_invalid_json() { + let status = is_setup_configured_str("not json"); + assert!(!status.postinstall_configured); assert!(status.needs_update); } #[test] - fn test_is_postinstall_configured_str_legacy_npx_pattern() { + fn test_configured_str_legacy_npx_pattern() { let content = r#"{"scripts":{"postinstall":"npx @socketsecurity/socket-patch apply --silent"}}"#; - let status = is_postinstall_configured_str(content); - // "npx @socketsecurity/socket-patch apply" contains "socket-patch apply" - assert!(status.configured); - assert!(!status.needs_update); + let status = is_setup_configured_str(content); + assert!(status.postinstall_configured); } #[test] - fn test_is_postinstall_configured_str_socket_dash_patch() { + fn test_configured_str_socket_dash_patch() { let content = r#"{"scripts":{"postinstall":"socket-patch apply --silent --ecosystems npm"}}"#; - let status = is_postinstall_configured_str(content); - assert!(status.configured); - assert!(!status.needs_update); + let status = is_setup_configured_str(content); + assert!(status.postinstall_configured); } #[test] - fn test_is_postinstall_configured_no_scripts() { - let pkg: serde_json::Value = serde_json::json!({"name": "test"}); - let status = is_postinstall_configured(&pkg); - assert!(!status.configured); - assert!(status.current_script.is_empty()); + fn test_configured_str_pnpx_pattern() { + let content = r#"{"scripts":{"postinstall":"pnpx @socketsecurity/socket-patch apply --silent --ecosystems npm"}}"#; + let status = is_setup_configured_str(content); + // "pnpx @socketsecurity/socket-patch apply" contains "socket-patch apply" + assert!(status.postinstall_configured); } + // ── generate_updated_script ───────────────────────────────────── + #[test] - fn test_is_postinstall_configured_no_postinstall() { - let pkg: serde_json::Value = serde_json::json!({ - "scripts": {"build": "tsc"} - }); - let status = is_postinstall_configured(&pkg); - assert!(!status.configured); - assert!(status.current_script.is_empty()); + fn test_generate_empty_npm() { + assert_eq!( + generate_updated_script("", PackageManager::Npm), + "npx @socketsecurity/socket-patch apply --silent --ecosystems npm" + ); + } + + #[test] + fn test_generate_empty_pnpm() { + assert_eq!( + generate_updated_script("", PackageManager::Pnpm), + "pnpx @socketsecurity/socket-patch apply --silent --ecosystems npm" + ); + } + + #[test] + fn test_generate_prepend_npm() { + assert_eq!( + generate_updated_script("echo done", PackageManager::Npm), + "npx @socketsecurity/socket-patch apply --silent --ecosystems npm && echo done" + ); + } + + #[test] + fn test_generate_prepend_pnpm() { + assert_eq!( + generate_updated_script("echo done", PackageManager::Pnpm), + "pnpx @socketsecurity/socket-patch apply --silent --ecosystems npm && echo done" + ); } + #[test] + fn test_generate_already_configured() { + let current = "socket-patch apply && echo done"; + assert_eq!( + generate_updated_script(current, PackageManager::Npm), + current + ); + } + + #[test] + fn test_generate_whitespace_only() { + let result = generate_updated_script(" \t ", PackageManager::Npm); + assert_eq!( + result, + "npx @socketsecurity/socket-patch apply --silent --ecosystems npm" + ); + } + + // ── update_package_json_object ────────────────────────────────── + #[test] fn test_update_object_creates_scripts() { let mut pkg: serde_json::Value = serde_json::json!({"name": "test"}); - let (modified, new_script) = update_package_json_object(&mut pkg); + let (modified, new_postinstall, new_dependencies) = + update_package_json_object(&mut pkg, PackageManager::Npm); assert!(modified); - assert!(new_script.contains("socket patch apply")); + assert!(new_postinstall.contains("npx @socketsecurity/socket-patch apply")); + assert!(new_dependencies.contains("npx @socketsecurity/socket-patch apply")); assert!(pkg.get("scripts").is_some()); assert!(pkg["scripts"]["postinstall"].is_string()); + assert!(pkg["scripts"]["dependencies"].is_string()); } #[test] - fn test_update_object_noop_when_configured() { + fn test_update_object_creates_scripts_pnpm() { + let mut pkg: serde_json::Value = serde_json::json!({"name": "test"}); + let (modified, new_postinstall, new_dependencies) = + update_package_json_object(&mut pkg, PackageManager::Pnpm); + assert!(modified); + assert!(new_postinstall.contains("pnpx @socketsecurity/socket-patch apply")); + assert!(new_dependencies.contains("pnpx @socketsecurity/socket-patch apply")); + } + + #[test] + fn test_update_object_noop_when_both_configured() { let mut pkg: serde_json::Value = serde_json::json!({ "scripts": { - "postinstall": "socket patch apply --silent --ecosystems npm" + "postinstall": "npx @socketsecurity/socket-patch apply --silent --ecosystems npm", + "dependencies": "npx @socketsecurity/socket-patch apply --silent --ecosystems npm" } }); - let (modified, existing) = update_package_json_object(&mut pkg); + let (modified, _, _) = update_package_json_object(&mut pkg, PackageManager::Npm); assert!(!modified); - assert!(existing.contains("socket patch apply")); } + #[test] + fn test_update_object_adds_dependencies_when_postinstall_exists() { + let mut pkg: serde_json::Value = serde_json::json!({ + "scripts": { + "postinstall": "npx @socketsecurity/socket-patch apply --silent --ecosystems npm" + } + }); + let (modified, _, new_dependencies) = + update_package_json_object(&mut pkg, PackageManager::Npm); + assert!(modified); + assert!(new_dependencies.contains("npx @socketsecurity/socket-patch apply")); + // postinstall should remain unchanged + assert_eq!( + pkg["scripts"]["postinstall"].as_str().unwrap(), + "npx @socketsecurity/socket-patch apply --silent --ecosystems npm" + ); + } + + // ── update_package_json_content ───────────────────────────────── + #[test] fn test_update_content_roundtrip_no_scripts() { let content = r#"{"name": "test"}"#; - let (modified, new_content, old_script, new_script) = - update_package_json_content(content).unwrap(); + let (modified, new_content, old_pi, new_pi, old_dep, new_dep) = + update_package_json_content(content, PackageManager::Npm).unwrap(); assert!(modified); - assert!(old_script.is_empty()); - assert!(new_script.contains("socket patch apply")); - // new_content should be valid JSON + assert!(old_pi.is_empty()); + assert!(new_pi.contains("npx @socketsecurity/socket-patch apply")); + assert!(old_dep.is_empty()); + assert!(new_dep.contains("npx @socketsecurity/socket-patch apply")); let parsed: serde_json::Value = serde_json::from_str(&new_content).unwrap(); assert!(parsed["scripts"]["postinstall"].is_string()); + assert!(parsed["scripts"]["dependencies"].is_string()); } #[test] fn test_update_content_already_configured() { - let content = r#"{"scripts":{"postinstall":"socket patch apply --silent --ecosystems npm"}}"#; - let (modified, _new_content, _old, _new) = - update_package_json_content(content).unwrap(); + let content = r#"{"scripts":{"postinstall":"socket patch apply --silent --ecosystems npm","dependencies":"socket patch apply --silent --ecosystems npm"}}"#; + let (modified, _, _, _, _, _) = + update_package_json_content(content, PackageManager::Npm).unwrap(); assert!(!modified); } #[test] fn test_update_content_invalid_json() { - let result = update_package_json_content("not json"); + let result = update_package_json_content("not json", PackageManager::Npm); assert!(result.is_err()); assert!(result.unwrap_err().contains("Invalid package.json")); } #[test] - fn test_generate_whitespace_only() { - // Whitespace-only string should be treated as empty after trim - let result = generate_updated_postinstall(" \t "); - assert_eq!(result, "socket patch apply --silent --ecosystems npm"); + fn test_update_content_pnpm() { + let content = r#"{"name": "test"}"#; + let (modified, new_content, _, new_pi, _, new_dep) = + update_package_json_content(content, PackageManager::Pnpm).unwrap(); + assert!(modified); + assert!(new_pi.contains("pnpx @socketsecurity/socket-patch apply")); + assert!(new_dep.contains("pnpx @socketsecurity/socket-patch apply")); + let parsed: serde_json::Value = serde_json::from_str(&new_content).unwrap(); + assert!(parsed["scripts"]["postinstall"] + .as_str() + .unwrap() + .contains("pnpx")); + assert!(parsed["scripts"]["dependencies"] + .as_str() + .unwrap() + .contains("pnpx")); } } diff --git a/crates/socket-patch-core/src/package_json/find.rs b/crates/socket-patch-core/src/package_json/find.rs index cfa30d2..a436154 100644 --- a/crates/socket-patch-core/src/package_json/find.rs +++ b/crates/socket-patch-core/src/package_json/find.rs @@ -1,6 +1,19 @@ use std::path::{Path, PathBuf}; use tokio::fs; +use super::detect::PackageManager; + +/// Detect the package manager based on lockfiles in the project root. +/// Checks for pnpm-lock.yaml, pnpm-lock.yml, and pnpm-workspace.yaml. +pub async fn detect_package_manager(start_path: &Path) -> PackageManager { + for name in &["pnpm-lock.yaml", "pnpm-lock.yml", "pnpm-workspace.yaml"] { + if fs::metadata(start_path.join(name)).await.is_ok() { + return PackageManager::Pnpm; + } + } + PackageManager::Npm +} + /// Workspace configuration type. #[derive(Debug, Clone)] pub enum WorkspaceType { @@ -602,4 +615,36 @@ mod tests { let results = find_package_json_files(dir.path()).await; assert_eq!(results.len(), 2); } + + // ── detect_package_manager ────────────────────────────────────── + + #[tokio::test] + async fn test_detect_npm_by_default() { + let dir = tempfile::tempdir().unwrap(); + let pm = detect_package_manager(dir.path()).await; + assert_eq!(pm, PackageManager::Npm); + } + + #[tokio::test] + async fn test_detect_pnpm_lock_yaml() { + let dir = tempfile::tempdir().unwrap(); + fs::write(dir.path().join("pnpm-lock.yaml"), "lockfileVersion: 9.0\n") + .await + .unwrap(); + let pm = detect_package_manager(dir.path()).await; + assert_eq!(pm, PackageManager::Pnpm); + } + + #[tokio::test] + async fn test_detect_pnpm_workspace_yaml() { + let dir = tempfile::tempdir().unwrap(); + fs::write( + dir.path().join("pnpm-workspace.yaml"), + "packages:\n - packages/*", + ) + .await + .unwrap(); + let pm = detect_package_manager(dir.path()).await; + assert_eq!(pm, PackageManager::Pnpm); + } } diff --git a/crates/socket-patch-core/src/package_json/update.rs b/crates/socket-patch-core/src/package_json/update.rs index 4c7486a..e3ff201 100644 --- a/crates/socket-patch-core/src/package_json/update.rs +++ b/crates/socket-patch-core/src/package_json/update.rs @@ -1,7 +1,7 @@ use std::path::Path; use tokio::fs; -use super::detect::{is_postinstall_configured_str, update_package_json_content}; +use super::detect::{is_setup_configured_str, update_package_json_content, PackageManager}; /// Result of updating a single package.json. #[derive(Debug, Clone)] @@ -10,6 +10,8 @@ pub struct UpdateResult { pub status: UpdateStatus, pub old_script: String, pub new_script: String, + pub old_dependencies_script: String, + pub new_dependencies_script: String, pub error: Option, } @@ -20,10 +22,11 @@ pub enum UpdateStatus { Error, } -/// Update a single package.json file with socket-patch postinstall script. +/// Update a single package.json file with socket-patch lifecycle scripts. pub async fn update_package_json( package_json_path: &Path, dry_run: bool, + pm: PackageManager, ) -> UpdateResult { let path_str = package_json_path.display().to_string(); @@ -35,30 +38,36 @@ pub async fn update_package_json( status: UpdateStatus::Error, old_script: String::new(), new_script: String::new(), + old_dependencies_script: String::new(), + new_dependencies_script: String::new(), error: Some(e.to_string()), }; } }; - let status = is_postinstall_configured_str(&content); + let status = is_setup_configured_str(&content); if !status.needs_update { return UpdateResult { path: path_str, status: UpdateStatus::AlreadyConfigured, - old_script: status.current_script.clone(), - new_script: status.current_script, + old_script: status.postinstall_script.clone(), + new_script: status.postinstall_script, + old_dependencies_script: status.dependencies_script.clone(), + new_dependencies_script: status.dependencies_script, error: None, }; } - match update_package_json_content(&content) { - Ok((modified, new_content, old_script, new_script)) => { + match update_package_json_content(&content, pm) { + Ok((modified, new_content, old_pi, new_pi, old_dep, new_dep)) => { if !modified { return UpdateResult { path: path_str, status: UpdateStatus::AlreadyConfigured, - old_script, - new_script, + old_script: old_pi, + new_script: new_pi, + old_dependencies_script: old_dep, + new_dependencies_script: new_dep, error: None, }; } @@ -68,8 +77,10 @@ pub async fn update_package_json( return UpdateResult { path: path_str, status: UpdateStatus::Error, - old_script, - new_script, + old_script: old_pi, + new_script: new_pi, + old_dependencies_script: old_dep, + new_dependencies_script: new_dep, error: Some(e.to_string()), }; } @@ -78,8 +89,10 @@ pub async fn update_package_json( UpdateResult { path: path_str, status: UpdateStatus::Updated, - old_script, - new_script, + old_script: old_pi, + new_script: new_pi, + old_dependencies_script: old_dep, + new_dependencies_script: new_dep, error: None, } } @@ -88,6 +101,8 @@ pub async fn update_package_json( status: UpdateStatus::Error, old_script: String::new(), new_script: String::new(), + old_dependencies_script: String::new(), + new_dependencies_script: String::new(), error: Some(e), }, } @@ -97,10 +112,11 @@ pub async fn update_package_json( pub async fn update_multiple_package_jsons( paths: &[&Path], dry_run: bool, + pm: PackageManager, ) -> Vec { let mut results = Vec::new(); for path in paths { - let result = update_package_json(path, dry_run).await; + let result = update_package_json(path, dry_run, pm).await; results.push(result); } results @@ -114,7 +130,7 @@ mod tests { async fn test_update_file_not_found() { let dir = tempfile::tempdir().unwrap(); let missing = dir.path().join("nonexistent.json"); - let result = update_package_json(&missing, false).await; + let result = update_package_json(&missing, false, PackageManager::Npm).await; assert_eq!(result.status, UpdateStatus::Error); assert!(result.error.is_some()); } @@ -125,11 +141,11 @@ mod tests { let pkg = dir.path().join("package.json"); fs::write( &pkg, - r#"{"name":"test","scripts":{"postinstall":"socket patch apply --silent --ecosystems npm"}}"#, + r#"{"name":"test","scripts":{"postinstall":"npx @socketsecurity/socket-patch apply --silent --ecosystems npm","dependencies":"npx @socketsecurity/socket-patch apply --silent --ecosystems npm"}}"#, ) .await .unwrap(); - let result = update_package_json(&pkg, false).await; + let result = update_package_json(&pkg, false, PackageManager::Npm).await; assert_eq!(result.status, UpdateStatus::AlreadyConfigured); } @@ -139,7 +155,7 @@ mod tests { let pkg = dir.path().join("package.json"); let original = r#"{"name":"test","scripts":{"build":"tsc"}}"#; fs::write(&pkg, original).await.unwrap(); - let result = update_package_json(&pkg, true).await; + let result = update_package_json(&pkg, true, PackageManager::Npm).await; assert_eq!(result.status, UpdateStatus::Updated); // File should remain unchanged let content = fs::read_to_string(&pkg).await.unwrap(); @@ -153,10 +169,12 @@ mod tests { fs::write(&pkg, r#"{"name":"test","scripts":{"build":"tsc"}}"#) .await .unwrap(); - let result = update_package_json(&pkg, false).await; + let result = update_package_json(&pkg, false, PackageManager::Npm).await; assert_eq!(result.status, UpdateStatus::Updated); let content = fs::read_to_string(&pkg).await.unwrap(); - assert!(content.contains("socket patch apply")); + assert!(content.contains("npx @socketsecurity/socket-patch apply")); + assert!(content.contains("postinstall")); + assert!(content.contains("dependencies")); } #[tokio::test] @@ -164,7 +182,7 @@ mod tests { let dir = tempfile::tempdir().unwrap(); let pkg = dir.path().join("package.json"); fs::write(&pkg, "not json!!!").await.unwrap(); - let result = update_package_json(&pkg, false).await; + let result = update_package_json(&pkg, false, PackageManager::Npm).await; assert_eq!(result.status, UpdateStatus::Error); assert!(result.error.is_some()); } @@ -174,11 +192,39 @@ mod tests { let dir = tempfile::tempdir().unwrap(); let pkg = dir.path().join("package.json"); fs::write(&pkg, r#"{"name":"x"}"#).await.unwrap(); - let result = update_package_json(&pkg, false).await; + let result = update_package_json(&pkg, false, PackageManager::Npm).await; assert_eq!(result.status, UpdateStatus::Updated); let content = fs::read_to_string(&pkg).await.unwrap(); assert!(content.contains("postinstall")); - assert!(content.contains("socket patch apply")); + assert!(content.contains("dependencies")); + assert!(content.contains("npx @socketsecurity/socket-patch apply")); + } + + #[tokio::test] + async fn test_update_pnpm() { + let dir = tempfile::tempdir().unwrap(); + let pkg = dir.path().join("package.json"); + fs::write(&pkg, r#"{"name":"x"}"#).await.unwrap(); + let result = update_package_json(&pkg, false, PackageManager::Pnpm).await; + assert_eq!(result.status, UpdateStatus::Updated); + let content = fs::read_to_string(&pkg).await.unwrap(); + assert!(content.contains("pnpx @socketsecurity/socket-patch apply")); + } + + #[tokio::test] + async fn test_update_adds_dependencies_when_postinstall_exists() { + let dir = tempfile::tempdir().unwrap(); + let pkg = dir.path().join("package.json"); + fs::write( + &pkg, + r#"{"name":"test","scripts":{"postinstall":"npx @socketsecurity/socket-patch apply --silent --ecosystems npm"}}"#, + ) + .await + .unwrap(); + let result = update_package_json(&pkg, false, PackageManager::Npm).await; + assert_eq!(result.status, UpdateStatus::Updated); + let content = fs::read_to_string(&pkg).await.unwrap(); + assert!(content.contains("dependencies")); } #[tokio::test] @@ -191,7 +237,7 @@ mod tests { let p2 = dir.path().join("b.json"); fs::write( &p2, - r#"{"name":"b","scripts":{"postinstall":"socket patch apply --silent --ecosystems npm"}}"#, + r#"{"name":"b","scripts":{"postinstall":"npx @socketsecurity/socket-patch apply --silent --ecosystems npm","dependencies":"npx @socketsecurity/socket-patch apply --silent --ecosystems npm"}}"#, ) .await .unwrap(); @@ -200,7 +246,7 @@ mod tests { // Don't create p3 — file not found let paths: Vec<&Path> = vec![p1.as_path(), p2.as_path(), p3.as_path()]; - let results = update_multiple_package_jsons(&paths, false).await; + let results = update_multiple_package_jsons(&paths, false, PackageManager::Npm).await; assert_eq!(results.len(), 3); assert_eq!(results[0].status, UpdateStatus::Updated); assert_eq!(results[1].status, UpdateStatus::AlreadyConfigured); From 8c5b2cae86ce03e7b7778ff0589e21ddd8b4a94c Mon Sep 17 00:00:00 2001 From: Mikola Lysenko Date: Wed, 8 Apr 2026 17:45:49 -0400 Subject: [PATCH 3/3] fix: use pnpm dlx instead of deprecated pnpx Replace pnpx with pnpm dlx for better compatibility. pnpx has been deprecated since pnpm v7 in favor of pnpm dlx. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/socket-patch-cli/src/commands/setup.rs | 2 +- .../src/package_json/detect.rs | 24 +++++++++---------- .../src/package_json/update.rs | 2 +- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/crates/socket-patch-cli/src/commands/setup.rs b/crates/socket-patch-cli/src/commands/setup.rs index 177bab0..cdf944d 100644 --- a/crates/socket-patch-cli/src/commands/setup.rs +++ b/crates/socket-patch-cli/src/commands/setup.rs @@ -54,7 +54,7 @@ pub async fn run(args: SetupArgs) -> i32 { if !args.json { println!("Found {} package.json file(s)", package_json_files.len()); if pm == PackageManager::Pnpm { - println!("Detected pnpm project (using pnpx)"); + println!("Detected pnpm project (using pnpm dlx)"); } } diff --git a/crates/socket-patch-core/src/package_json/detect.rs b/crates/socket-patch-core/src/package_json/detect.rs index dc38803..e90f742 100644 --- a/crates/socket-patch-core/src/package_json/detect.rs +++ b/crates/socket-patch-core/src/package_json/detect.rs @@ -10,7 +10,7 @@ fn socket_patch_command(pm: PackageManager) -> &'static str { match pm { PackageManager::Npm => "npx @socketsecurity/socket-patch apply --silent --ecosystems npm", PackageManager::Pnpm => { - "pnpx @socketsecurity/socket-patch apply --silent --ecosystems npm" + "pnpm dlx @socketsecurity/socket-patch apply --silent --ecosystems npm" } } } @@ -294,10 +294,10 @@ mod tests { } #[test] - fn test_configured_str_pnpx_pattern() { - let content = r#"{"scripts":{"postinstall":"pnpx @socketsecurity/socket-patch apply --silent --ecosystems npm"}}"#; + fn test_configured_str_pnpm_dlx_pattern() { + let content = r#"{"scripts":{"postinstall":"pnpm dlx @socketsecurity/socket-patch apply --silent --ecosystems npm"}}"#; let status = is_setup_configured_str(content); - // "pnpx @socketsecurity/socket-patch apply" contains "socket-patch apply" + // "pnpm dlx @socketsecurity/socket-patch apply" contains "socket-patch apply" assert!(status.postinstall_configured); } @@ -315,7 +315,7 @@ mod tests { fn test_generate_empty_pnpm() { assert_eq!( generate_updated_script("", PackageManager::Pnpm), - "pnpx @socketsecurity/socket-patch apply --silent --ecosystems npm" + "pnpm dlx @socketsecurity/socket-patch apply --silent --ecosystems npm" ); } @@ -331,7 +331,7 @@ mod tests { fn test_generate_prepend_pnpm() { assert_eq!( generate_updated_script("echo done", PackageManager::Pnpm), - "pnpx @socketsecurity/socket-patch apply --silent --ecosystems npm && echo done" + "pnpm dlx @socketsecurity/socket-patch apply --silent --ecosystems npm && echo done" ); } @@ -374,8 +374,8 @@ mod tests { let (modified, new_postinstall, new_dependencies) = update_package_json_object(&mut pkg, PackageManager::Pnpm); assert!(modified); - assert!(new_postinstall.contains("pnpx @socketsecurity/socket-patch apply")); - assert!(new_dependencies.contains("pnpx @socketsecurity/socket-patch apply")); + assert!(new_postinstall.contains("pnpm dlx @socketsecurity/socket-patch apply")); + assert!(new_dependencies.contains("pnpm dlx @socketsecurity/socket-patch apply")); } #[test] @@ -446,16 +446,16 @@ mod tests { let (modified, new_content, _, new_pi, _, new_dep) = update_package_json_content(content, PackageManager::Pnpm).unwrap(); assert!(modified); - assert!(new_pi.contains("pnpx @socketsecurity/socket-patch apply")); - assert!(new_dep.contains("pnpx @socketsecurity/socket-patch apply")); + assert!(new_pi.contains("pnpm dlx @socketsecurity/socket-patch apply")); + assert!(new_dep.contains("pnpm dlx @socketsecurity/socket-patch apply")); let parsed: serde_json::Value = serde_json::from_str(&new_content).unwrap(); assert!(parsed["scripts"]["postinstall"] .as_str() .unwrap() - .contains("pnpx")); + .contains("pnpm dlx")); assert!(parsed["scripts"]["dependencies"] .as_str() .unwrap() - .contains("pnpx")); + .contains("pnpm dlx")); } } diff --git a/crates/socket-patch-core/src/package_json/update.rs b/crates/socket-patch-core/src/package_json/update.rs index e3ff201..f8b859a 100644 --- a/crates/socket-patch-core/src/package_json/update.rs +++ b/crates/socket-patch-core/src/package_json/update.rs @@ -208,7 +208,7 @@ mod tests { let result = update_package_json(&pkg, false, PackageManager::Pnpm).await; assert_eq!(result.status, UpdateStatus::Updated); let content = fs::read_to_string(&pkg).await.unwrap(); - assert!(content.contains("pnpx @socketsecurity/socket-patch apply")); + assert!(content.contains("pnpm dlx @socketsecurity/socket-patch apply")); } #[tokio::test]