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 if args.common.json {
159 println!("{}", serde_json::to_string_pretty(&serde_json::json!({
160 "status": "already_configured",
161 "updated": 0,
162 "alreadyConfigured": already_configured.len(),
163 "errors": errors.len(),
164 "files": preview_results.iter().map(|r| {
165 serde_json::json!({
166 "path": r.path,
167 "status": match r.status {
168 UpdateStatus::Updated => "updated",
169 UpdateStatus::AlreadyConfigured => "already_configured",
170 UpdateStatus::Error => "error",
171 },
172 "error": r.error,
173 })
174 }).collect::<Vec<_>>(),
175 })).unwrap());
176 } else {
177 println!("All package.json files are already configured with socket-patch!");
178 }
179 return 0;
180 }
181
182 if !args.common.dry_run {
184 if !args.common.yes && !args.common.json {
185 if !stdin_is_tty() {
186 eprintln!("Non-interactive mode detected, proceeding automatically.");
188 } else {
189 print!("Proceed with these changes? (y/N): ");
190 io::stdout().flush().unwrap();
191 let mut answer = String::new();
192 io::stdin().read_line(&mut answer).unwrap();
193 let answer = answer.trim().to_lowercase();
194 if answer != "y" && answer != "yes" {
195 println!("Aborted");
196 return 0;
197 }
198 }
199 }
200
201 if !args.common.json {
202 println!("\nApplying changes...");
203 }
204 let mut results = Vec::new();
205 for loc in &package_json_files {
206 let result = update_package_json(&loc.path, false, pm).await;
207 results.push(result);
208 }
209
210 let updated = results.iter().filter(|r| r.status == UpdateStatus::Updated).count();
211 let already = results.iter().filter(|r| r.status == UpdateStatus::AlreadyConfigured).count();
212 let errs = results.iter().filter(|r| r.status == UpdateStatus::Error).count();
213
214 if args.common.json {
215 println!("{}", serde_json::to_string_pretty(&serde_json::json!({
216 "status": if errs > 0 { "partial_failure" } else { "success" },
217 "updated": updated,
218 "alreadyConfigured": already,
219 "errors": errs,
220 "packageManager": match pm {
221 PackageManager::Npm => "npm",
222 PackageManager::Pnpm => "pnpm",
223 },
224 "files": results.iter().map(|r| {
225 serde_json::json!({
226 "path": r.path,
227 "status": match r.status {
228 UpdateStatus::Updated => "updated",
229 UpdateStatus::AlreadyConfigured => "already_configured",
230 UpdateStatus::Error => "error",
231 },
232 "error": r.error,
233 })
234 }).collect::<Vec<_>>(),
235 })).unwrap());
236 } else {
237 println!("\nSummary:");
238 println!(" {updated} file(s) updated");
239 println!(" {already} file(s) already configured");
240 if errs > 0 {
241 println!(" {errs} error(s)");
242 }
243 }
244
245 if errs > 0 { 1 } else { 0 }
246 } else {
247 let updated = preview_results.iter().filter(|r| r.status == UpdateStatus::Updated).count();
248 let already = preview_results.iter().filter(|r| r.status == UpdateStatus::AlreadyConfigured).count();
249 let errs = preview_results.iter().filter(|r| r.status == UpdateStatus::Error).count();
250
251 if args.common.json {
252 println!("{}", serde_json::to_string_pretty(&serde_json::json!({
253 "status": "dry_run",
254 "wouldUpdate": updated,
255 "alreadyConfigured": already,
256 "errors": errs,
257 "dryRun": true,
258 "packageManager": match pm {
259 PackageManager::Npm => "npm",
260 PackageManager::Pnpm => "pnpm",
261 },
262 "files": preview_results.iter().map(|r| {
263 serde_json::json!({
264 "path": r.path,
265 "status": match r.status {
266 UpdateStatus::Updated => "updated",
267 UpdateStatus::AlreadyConfigured => "already_configured",
268 UpdateStatus::Error => "error",
269 },
270 "oldScript": r.old_script,
271 "newScript": r.new_script,
272 "oldDependenciesScript": r.old_dependencies_script,
273 "newDependenciesScript": r.new_dependencies_script,
274 "error": r.error,
275 })
276 }).collect::<Vec<_>>(),
277 })).unwrap());
278 } else {
279 println!("\nSummary:");
280 println!(" {updated} file(s) would be updated");
281 println!(" {already} file(s) already configured");
282 if errs > 0 {
283 println!(" {errs} error(s)");
284 }
285 }
286 0
287 }
288}
289
290fn pathdiff(path: &str, base: &Path) -> String {
291 let p = Path::new(path);
292 p.strip_prefix(base)
293 .map(|r| r.display().to_string())
294 .unwrap_or_else(|_| path.to_string())
295}