socket_patch_cli/commands/
setup.rs1use clap::Args;
2use socket_patch_core::package_json::detect::PackageManager;
3use socket_patch_core::package_json::find::{
4 detect_package_manager, find_package_json_files, WorkspaceType,
5};
6use socket_patch_core::package_json::update::{update_package_json, UpdateStatus};
7use socket_patch_core::utils::telemetry::track_patch_setup;
8use std::io::{self, Write};
9use std::path::Path;
10
11use crate::args::GlobalArgs;
12use crate::output::stdin_is_tty;
13
14fn manager_name(pm: PackageManager) -> &'static str {
16 match pm {
17 PackageManager::Npm => "npm",
18 PackageManager::Pnpm => "pnpm",
19 }
20}
21
22#[derive(Args)]
23pub struct SetupArgs {
24 #[command(flatten)]
25 pub common: GlobalArgs,
26}
27
28pub async fn run(args: SetupArgs) -> i32 {
29 if !args.common.json {
30 println!("Searching for package.json files...");
31 }
32
33 let find_result = find_package_json_files(&args.common.cwd).await;
34
35 let package_json_files = match find_result.workspace_type {
42 WorkspaceType::Pnpm => find_result
43 .files
44 .into_iter()
45 .filter(|loc| loc.is_root)
46 .collect(),
47 _ => find_result.files,
48 };
49
50 if package_json_files.is_empty() {
51 if args.common.json {
52 println!("{}", serde_json::to_string_pretty(&serde_json::json!({
53 "status": "no_files",
54 "updated": 0,
55 "alreadyConfigured": 0,
56 "errors": 0,
57 "files": [],
58 })).unwrap());
59 } else {
60 println!("No package.json files found");
61 }
62 return 0;
63 }
64
65 let pm = detect_package_manager(&args.common.cwd).await;
67
68 track_patch_setup(
73 manager_name(pm),
74 args.common.api_token.as_deref(),
75 args.common.org.as_deref(),
76 )
77 .await;
78
79 if !args.common.json {
80 println!("Found {} package.json file(s)", package_json_files.len());
81 if pm == PackageManager::Pnpm {
82 println!("Detected pnpm project (using pnpm dlx)");
83 }
84 }
85
86 let mut preview_results = Vec::new();
88 for loc in &package_json_files {
89 let result = update_package_json(&loc.path, true, pm).await;
90 preview_results.push(result);
91 }
92
93 let to_update: Vec<_> = preview_results
95 .iter()
96 .filter(|r| r.status == UpdateStatus::Updated)
97 .collect();
98 let already_configured: Vec<_> = preview_results
99 .iter()
100 .filter(|r| r.status == UpdateStatus::AlreadyConfigured)
101 .collect();
102 let errors: Vec<_> = preview_results
103 .iter()
104 .filter(|r| r.status == UpdateStatus::Error)
105 .collect();
106
107 if !args.common.json {
108 println!("\nPackage.json files to be updated:\n");
109
110 if !to_update.is_empty() {
111 println!("Will update:");
112 for result in &to_update {
113 let rel_path = pathdiff(&result.path, &args.common.cwd);
114 println!(" + {rel_path}");
115 if result.old_script.is_empty() {
116 println!(" postinstall: (no script)");
117 } else {
118 println!(" postinstall: \"{}\"", result.old_script);
119 }
120 println!(" -> postinstall: \"{}\"", result.new_script);
121 if result.old_dependencies_script.is_empty() {
122 println!(" dependencies: (no script)");
123 } else {
124 println!(" dependencies: \"{}\"", result.old_dependencies_script);
125 }
126 println!(
127 " -> dependencies: \"{}\"",
128 result.new_dependencies_script
129 );
130 }
131 println!();
132 }
133
134 if !already_configured.is_empty() {
135 println!("Already configured (will skip):");
136 for result in &already_configured {
137 let rel_path = pathdiff(&result.path, &args.common.cwd);
138 println!(" = {rel_path}");
139 }
140 println!();
141 }
142
143 if !errors.is_empty() {
144 println!("Errors:");
145 for result in &errors {
146 let rel_path = pathdiff(&result.path, &args.common.cwd);
147 println!(
148 " ! {}: {}",
149 rel_path,
150 result.error.as_deref().unwrap_or("unknown error")
151 );
152 }
153 println!();
154 }
155 }
156
157 if to_update.is_empty() {
158 let errs = errors.len();
164 if args.common.json {
165 println!("{}", serde_json::to_string_pretty(&serde_json::json!({
166 "status": if errs > 0 { "error" } else { "already_configured" },
167 "updated": 0,
168 "alreadyConfigured": already_configured.len(),
169 "errors": errs,
170 "files": preview_results.iter().map(|r| {
171 serde_json::json!({
172 "path": r.path,
173 "status": match r.status {
174 UpdateStatus::Updated => "updated",
175 UpdateStatus::AlreadyConfigured => "already_configured",
176 UpdateStatus::Error => "error",
177 },
178 "error": r.error,
179 })
180 }).collect::<Vec<_>>(),
181 })).unwrap());
182 } else if errs > 0 {
183 println!(
185 "No files were updated; {errs} file(s) could not be processed (see errors above)."
186 );
187 } else {
188 println!("All package.json files are already configured with socket-patch!");
189 }
190 return if errs > 0 { 1 } else { 0 };
191 }
192
193 if !args.common.dry_run {
195 if !args.common.yes && !args.common.json {
196 if !stdin_is_tty() {
197 eprintln!("Non-interactive mode detected, proceeding automatically.");
199 } else {
200 print!("Proceed with these changes? (y/N): ");
201 io::stdout().flush().unwrap();
202 let mut answer = String::new();
203 io::stdin().read_line(&mut answer).unwrap();
204 let answer = answer.trim().to_lowercase();
205 if answer != "y" && answer != "yes" {
206 println!("Aborted");
207 return 0;
208 }
209 }
210 }
211
212 if !args.common.json {
213 println!("\nApplying changes...");
214 }
215 let mut results = Vec::new();
216 for loc in &package_json_files {
217 let result = update_package_json(&loc.path, false, pm).await;
218 results.push(result);
219 }
220
221 let updated = results.iter().filter(|r| r.status == UpdateStatus::Updated).count();
222 let already = results.iter().filter(|r| r.status == UpdateStatus::AlreadyConfigured).count();
223 let errs = results.iter().filter(|r| r.status == UpdateStatus::Error).count();
224
225 if args.common.json {
226 println!("{}", serde_json::to_string_pretty(&serde_json::json!({
227 "status": if errs > 0 { "partial_failure" } else { "success" },
228 "updated": updated,
229 "alreadyConfigured": already,
230 "errors": errs,
231 "packageManager": match pm {
232 PackageManager::Npm => "npm",
233 PackageManager::Pnpm => "pnpm",
234 },
235 "files": results.iter().map(|r| {
236 serde_json::json!({
237 "path": r.path,
238 "status": match r.status {
239 UpdateStatus::Updated => "updated",
240 UpdateStatus::AlreadyConfigured => "already_configured",
241 UpdateStatus::Error => "error",
242 },
243 "error": r.error,
244 })
245 }).collect::<Vec<_>>(),
246 })).unwrap());
247 } else {
248 println!("\nSummary:");
249 println!(" {updated} file(s) updated");
250 println!(" {already} file(s) already configured");
251 if errs > 0 {
252 println!(" {errs} error(s)");
253 }
254 }
255
256 if errs > 0 { 1 } else { 0 }
257 } else {
258 let updated = preview_results.iter().filter(|r| r.status == UpdateStatus::Updated).count();
259 let already = preview_results.iter().filter(|r| r.status == UpdateStatus::AlreadyConfigured).count();
260 let errs = preview_results.iter().filter(|r| r.status == UpdateStatus::Error).count();
261
262 if args.common.json {
263 println!("{}", serde_json::to_string_pretty(&serde_json::json!({
264 "status": "dry_run",
265 "wouldUpdate": updated,
266 "alreadyConfigured": already,
267 "errors": errs,
268 "dryRun": true,
269 "packageManager": match pm {
270 PackageManager::Npm => "npm",
271 PackageManager::Pnpm => "pnpm",
272 },
273 "files": preview_results.iter().map(|r| {
274 serde_json::json!({
275 "path": r.path,
276 "status": match r.status {
277 UpdateStatus::Updated => "updated",
278 UpdateStatus::AlreadyConfigured => "already_configured",
279 UpdateStatus::Error => "error",
280 },
281 "oldScript": r.old_script,
282 "newScript": r.new_script,
283 "oldDependenciesScript": r.old_dependencies_script,
284 "newDependenciesScript": r.new_dependencies_script,
285 "error": r.error,
286 })
287 }).collect::<Vec<_>>(),
288 })).unwrap());
289 } else {
290 println!("\nSummary:");
291 println!(" {updated} file(s) would be updated");
292 println!(" {already} file(s) already configured");
293 if errs > 0 {
294 println!(" {errs} error(s)");
295 }
296 }
297 if errs > 0 { 1 } else { 0 }
300 }
301}
302
303fn pathdiff(path: &str, base: &Path) -> String {
304 let p = Path::new(path);
305 p.strip_prefix(base)
306 .map(|r| r.display().to_string())
307 .unwrap_or_else(|_| path.to_string())
308}