1use clap::Parser;
2use serde_json::Value;
3use std::env;
4use std::fs;
5use std::path::{Path, PathBuf};
6use std::process::Command;
7use std::process::Stdio;
8use crate::pm::{PmError, install_from};
9
10#[derive(Parser, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
11pub struct Args {
12 pub package: String,
13 #[arg(last = true)]
14 pub args: Vec<String>,
15}
16
17pub fn parse_package(package: &str) -> Result<(String, String), PmError> {
18 let package = package.trim();
19 if package.is_empty() {
20 return Err(PmError::InvalidPackageSpec {
21 spec: package.to_string(),
22 });
23 }
24
25 if package.starts_with('@') {
26 let slash = package.find('/').ok_or_else(|| PmError::InvalidPackageSpec {
27 spec: package.to_string(),
28 })?;
29 let tail = &package[slash + 1..];
30
31 if let Some(at) = tail.rfind('@') {
32 let split_index = slash + 1 + at;
33 let name = &package[..split_index];
34 let version = &package[split_index + 1..];
35 if version.is_empty() {
36 return Err(PmError::InvalidPackageSpec {
37 spec: package.to_string(),
38 });
39 }
40 return Ok((name.to_string(), version.to_string()));
41 }
42
43 return Ok((package.to_string(), "latest".to_string()));
44 }
45
46 if let Some((name, version)) = package.rsplit_once('@') {
47 if name.is_empty() || version.is_empty() {
48 return Err(PmError::InvalidPackageSpec {
49 spec: package.to_string(),
50 });
51 }
52 return Ok((name.to_string(), version.to_string()));
53 }
54
55 Ok((package.to_string(), "latest".to_string()))
56}
57
58pub fn process(package: &str, version: &str, args: &[String]) -> Result<(), PmError> {
59 let temp = tempfile::tempdir().map_err(|source| PmError::CreateTempDir { source })?;
60 let path = temp.path().to_owned();
61 let current_dir = env::current_dir().map_err(|source| PmError::CurrentDir { source })?;
62
63 let manifest = format!(
64 r#"{{
65 "name": "____o-x-generated____",
66 "version": "1.0.0",
67 "private": true,
68 "dependencies": {{
69 "{}": "{}"
70 }}
71}}"#,
72 package, version
73 );
74
75 let manifest_path = path.join("package.json");
76 fs::write(&manifest_path, manifest).map_err(|source| PmError::WriteGeneratedManifest {
77 path: manifest_path.clone(),
78 source,
79 })?;
80
81 let path_str = path
82 .to_str()
83 .ok_or_else(|| PmError::InvalidTempPath { path: path.clone() })?;
84
85 install_from(path_str)?;
86
87 let package_dir = install_dir(&path.join("node_modules"), package);
88 let package_json_path = package_dir.join("package.json");
89 let command_name = resolve_bin_command(package, &package_json_path)?;
90 let command_path = resolve_shim_path(&path.join("node_modules"), &command_name);
91 if !command_path.is_file() {
92 return Err(PmError::MissingPackageBinary {
93 package: package.to_string(),
94 command: command_name,
95 path: command_path,
96 });
97 }
98
99 let status = Command::new(&command_path)
100 .args(args)
101 .current_dir(¤t_dir)
102 .stdin(Stdio::inherit())
103 .stdout(Stdio::inherit())
104 .stderr(Stdio::inherit())
105 .status()
106 .map_err(|source| PmError::SpawnPackageBinary {
107 package: package.to_string(),
108 command: command_path.clone(),
109 source,
110 })?;
111
112 if !status.success() {
113 return Err(PmError::PackageBinaryFailed {
114 package: package.to_string(),
115 command: command_path,
116 status: status
117 .code()
118 .map(|code| code.to_string())
119 .unwrap_or_else(|| status.to_string()),
120 stderr: None,
121 });
122 }
123
124 Ok(())
125}
126
127fn resolve_bin_command(package: &str, package_json_path: &Path) -> Result<String, PmError> {
128 let source =
129 fs::read_to_string(package_json_path).map_err(|source| PmError::ReadInstalledManifest {
130 path: package_json_path.to_path_buf(),
131 source,
132 })?;
133 let value: Value = serde_json::from_str(&source).map_err(|source| PmError::ParseManifest {
134 path: package_json_path.to_path_buf(),
135 source,
136 })?;
137
138 let default_name = default_bin_name(package);
139 let Some(bin_value) = value.get("bin") else {
140 return Ok(default_name);
141 };
142
143 match bin_value {
144 Value::String(_) => Ok(default_name),
145 Value::Object(entries) => {
146 if entries.contains_key(&default_name) {
147 return Ok(default_name);
148 }
149
150 if entries.len() == 1 {
151 if let Some((name, _)) = entries.iter().next() {
152 return Ok(name.clone());
153 }
154 }
155
156 Err(PmError::AmbiguousBinEntry {
157 package: package.to_string(),
158 path: package_json_path.to_path_buf(),
159 available: entries.keys().cloned().collect(),
160 })
161 }
162 Value::Null => Ok(default_name),
163 _ => Err(PmError::InvalidBinField {
164 path: package_json_path.to_path_buf(),
165 }),
166 }
167}
168
169fn default_bin_name(package: &str) -> String {
170 package
171 .rsplit_once('/')
172 .map(|(_, name)| name.to_string())
173 .unwrap_or_else(|| package.to_string())
174}
175
176fn install_dir(node_modules_dir: &Path, package_name: &str) -> PathBuf {
177 if let Some((scope, name)) = package_name.split_once('/') {
178 node_modules_dir.join(scope).join(name)
179 } else {
180 node_modules_dir.join(package_name)
181 }
182}
183
184#[cfg(unix)]
185fn resolve_shim_path(node_modules_dir: &Path, command_name: &str) -> PathBuf {
186 node_modules_dir.join(".bin").join(command_name)
187}
188
189#[cfg(windows)]
190fn resolve_shim_path(node_modules_dir: &Path, command_name: &str) -> PathBuf {
191 node_modules_dir
192 .join(".bin")
193 .join(format!("{command_name}.cmd"))
194}