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