1use crate::commands::{CommandContext, StreamChunk, VirtualCommandRegistry};
3use crate::{CommandBuilder, CommandResult, OutputChunk, StreamingRunner};
4use serde::Deserialize;
5use std::collections::HashMap;
6use std::future::Future;
7use std::path::{Path, PathBuf};
8use std::pin::Pin;
9use std::sync::Arc;
10use tokio::sync::mpsc;
11
12mod marketplace;
13mod watch;
14pub use marketplace::{Marketplace, PackageMetadata, SearchOptions};
15pub use watch::{watch_package, PackageWatcher};
16
17pub type HandlerFuture = Pin<Box<dyn Future<Output = CommandResult> + Send>>;
18pub type PackageHandler = Arc<dyn Fn(CommandContext) -> HandlerFuture + Send + Sync>;
19
20pub fn argv_handler(file: impl Into<String>, args: Vec<String>) -> PackageHandler {
22 let file = file.into();
23 Arc::new(move |context| Box::pin(run_manifest(file.clone(), args.clone(), context)))
24}
25
26#[derive(Clone)]
27pub struct CommandPackage {
28 pub name: String,
29 pub version: String,
30 pub commands: HashMap<String, PackageHandler>,
31}
32
33#[derive(Deserialize)]
34struct Manifest {
35 name: String,
36 version: String,
37 commands: HashMap<String, Specification>,
38}
39#[derive(Deserialize)]
40struct Specification {
41 file: String,
42 #[serde(default)]
43 args: Vec<String>,
44}
45
46impl CommandPackage {
47 pub async fn load(path: impl AsRef<Path>) -> std::io::Result<Self> {
49 let manifest: Manifest = serde_json::from_str(&tokio::fs::read_to_string(path).await?)?;
50 let commands = manifest
51 .commands
52 .into_iter()
53 .map(|(name, specification)| {
54 let handler = argv_handler(specification.file, specification.args);
55 (name, handler)
56 })
57 .collect();
58 Ok(Self {
59 name: manifest.name,
60 version: manifest.version,
61 commands,
62 })
63 }
64}
65
66async fn run_manifest(
67 file: String,
68 mut args: Vec<String>,
69 context: CommandContext,
70) -> CommandResult {
71 if context.is_cancelled() {
72 return CommandResult::error_with_code("Command cancelled", 130);
73 }
74 let cancelled: Option<Arc<dyn Fn() -> bool + Send + Sync>> =
75 context.is_cancelled.map(Arc::from);
76 args.extend(context.args);
77 let mut runner =
78 StreamingRunner::from_argv(file, args).stdin(context.stdin.unwrap_or_default());
79 if let Some(cwd) = context.cwd {
80 runner = runner.cwd(cwd);
81 }
82 if let Some(env) = context.env {
83 runner = runner.env(env);
84 }
85 let mut stream = runner.stream();
86 let mut stdout = Vec::new();
87 let mut stderr = Vec::new();
88 let mut code = 0;
89 let mut stopped = false;
90 let mut stdout_decoder = Decoder::default();
91 let mut stderr_decoder = Decoder::default();
92 loop {
93 let chunk = tokio::select! {
94 chunk = stream.next() => chunk,
95 () = wait_cancel(&cancelled), if !stopped && cancelled.is_some() => {
96 stream.kill();
97 stopped = true;
98 continue;
99 }
100 };
101 let Some(chunk) = chunk else {
102 break;
103 };
104 let output = match chunk {
105 OutputChunk::Stdout(data) => {
106 if context.output_tx.is_none() {
107 stdout.extend(&data);
108 }
109 let text = stdout_decoder.push(&data);
110 if text.is_empty() {
111 None
112 } else {
113 Some(StreamChunk::Stdout(text))
114 }
115 }
116 OutputChunk::Stderr(data) => {
117 if context.output_tx.is_none() {
118 stderr.extend(&data);
119 }
120 let text = stderr_decoder.push(&data);
121 if text.is_empty() {
122 None
123 } else {
124 Some(StreamChunk::Stderr(text))
125 }
126 }
127 OutputChunk::Exit(status) => {
128 code = status;
129 None
130 }
131 };
132 if let (Some(tx), Some(chunk)) = (&context.output_tx, output) {
133 if send_chunk(tx, chunk, &cancelled).await.is_err() {
134 stream.kill();
135 return CommandResult::error_with_code(
136 "Command cancelled or output consumer closed",
137 130,
138 );
139 }
140 }
141 }
142 if let Some(tx) = &context.output_tx {
143 for chunk in [
144 StreamChunk::Stdout(stdout_decoder.finish()),
145 StreamChunk::Stderr(stderr_decoder.finish()),
146 ] {
147 let empty = match &chunk {
148 StreamChunk::Stdout(text) | StreamChunk::Stderr(text) => text.is_empty(),
149 };
150 if !empty && send_chunk(tx, chunk, &cancelled).await.is_err() {
151 return CommandResult::error_with_code(
152 "Command cancelled or output consumer closed",
153 130,
154 );
155 }
156 }
157 }
158 CommandResult::new(
159 String::from_utf8_lossy(&stdout).into_owned(),
160 String::from_utf8_lossy(&stderr).into_owned(),
161 code,
162 )
163}
164
165async fn wait_cancel(cancelled: &Option<Arc<dyn Fn() -> bool + Send + Sync>>) {
166 if let Some(cancelled) = cancelled {
167 loop {
168 if cancelled() {
169 return;
170 }
171 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
172 }
173 }
174 std::future::pending::<()>().await;
175}
176
177async fn send_chunk(
178 tx: &mpsc::Sender<StreamChunk>,
179 chunk: StreamChunk,
180 cancelled: &Option<Arc<dyn Fn() -> bool + Send + Sync>>,
181) -> Result<(), ()> {
182 tokio::select! {
183 result = tx.send(chunk) => result.map_err(|_| ()),
184 () = wait_cancel(cancelled) => Err(()),
185 }
186}
187
188#[derive(Default)]
190struct Decoder {
191 pending: Vec<u8>,
192}
193impl Decoder {
194 fn push(&mut self, data: &[u8]) -> String {
195 self.pending.extend(data);
196 let mut text = String::new();
197 let mut consumed = 0;
198 while consumed < self.pending.len() {
199 let remaining = &self.pending[consumed..];
200 match std::str::from_utf8(remaining) {
201 Ok(valid) => {
202 text.push_str(valid);
203 consumed = self.pending.len();
204 }
205 Err(error) => {
206 let valid = error.valid_up_to();
207 text.push_str(
208 std::str::from_utf8(&remaining[..valid]).expect("valid UTF-8 prefix"),
209 );
210 consumed += valid;
211 match error.error_len() {
212 Some(length) => {
213 text.push('\u{fffd}');
214 consumed += length;
215 }
216 None => break,
217 }
218 }
219 }
220 }
221 self.pending.drain(..consumed);
222 text
223 }
224 fn finish(&mut self) -> String {
225 let text = String::from_utf8_lossy(&self.pending).into_owned();
226 self.pending.clear();
227 text
228 }
229}
230
231#[derive(Default, Clone, Copy)]
232pub struct InstallOptions {
233 pub replace: bool,
235}
236
237struct Installed {
238 package: CommandPackage,
239 previous: HashMap<String, (PackageHandler, Option<String>)>,
240}
241
242#[derive(Default)]
243pub struct CommandEnvironment {
244 commands: HashMap<String, PackageHandler>,
245 owners: HashMap<String, String>,
246 packages: HashMap<String, Installed>,
247 files: HashMap<PathBuf, String>,
248}
249
250impl CommandEnvironment {
251 pub fn new() -> Self {
252 Self::default()
253 }
254 pub fn create<F, Fut>(&mut self, name: &str, handler: F) -> Result<(), String>
269 where
270 F: Fn(CommandContext) -> Fut + Send + Sync + 'static,
271 Fut: Future<Output = CommandResult> + Send + 'static,
272 {
273 validate(name)?;
274 self.owners.remove(name);
275 self.commands.insert(
276 name.into(),
277 Arc::new(move |context| Box::pin(handler(context))),
278 );
279 Ok(())
280 }
281 pub fn list(&self) -> Vec<String> {
282 let mut names = self.commands.keys().cloned().collect::<Vec<_>>();
283 names.sort();
284 names
285 }
286 pub fn unregister(&mut self, name: &str) -> bool {
287 self.owners.remove(name);
288 self.commands.remove(name).is_some()
289 }
290 pub fn installed(&self, name: &str) -> Option<&CommandPackage> {
291 self.packages.get(name).map(|installed| &installed.package)
292 }
293 pub fn get(&self, name: &str) -> Option<PackageHandler> {
294 self.commands.get(name).cloned().or_else(|| {
295 let handler = *VirtualCommandRegistry::with_builtins().get(name)?;
296 Some(Arc::new(handler) as PackageHandler)
297 })
298 }
299 pub async fn run(&self, name: &str, context: CommandContext) -> CommandResult {
300 match self.get(name) {
301 Some(handler) => handler(context).await,
302 None => CommandResult::error_with_code(format!("Unknown command: {name}"), 127),
303 }
304 }
305 pub fn extend<F>(&mut self, name: &str, middleware: F) -> Result<(), String>
306 where
307 F: Fn(CommandContext, PackageHandler) -> HandlerFuture + Send + Sync + 'static,
308 {
309 let parent = self
310 .get(name)
311 .ok_or_else(|| format!("Unknown command: {name}"))?;
312 self.commands.insert(
313 name.into(),
314 Arc::new(move |context| middleware(context, parent.clone())),
315 );
316 Ok(())
317 }
318 pub fn inherit(&mut self, name: &str, parent: &str, args: Vec<String>) -> Result<(), String> {
319 let handler = self
320 .get(parent)
321 .ok_or_else(|| format!("Unknown command: {parent}"))?;
322 self.create(name, move |mut context| {
323 let mut prefix = args.clone();
324 prefix.extend(context.args);
325 context.args = prefix;
326 handler(context)
327 })
328 }
329 pub fn compose(&mut self, name: &str, names: &[&str]) -> Result<(), String> {
330 if names.is_empty() {
331 return Err("Composition requires commands".into());
332 }
333 let handlers = names
334 .iter()
335 .map(|name| {
336 self.get(name)
337 .ok_or_else(|| format!("Unknown command: {name}"))
338 })
339 .collect::<Result<Vec<_>, _>>()?;
340 self.create(name, move |context| {
341 let handlers = handlers.clone();
342 Box::pin(async move {
343 let mut result = CommandResult::success(context.stdin.unwrap_or_default());
344 let cancelled: Option<Arc<dyn Fn() -> bool + Send + Sync>> =
345 context.is_cancelled.map(Arc::from);
346 for handler in handlers {
347 let (tx, mut rx) = mpsc::channel(16);
348 let cancelled = cancelled.clone();
349 let next = CommandContext {
350 args: context.args.clone(),
351 stdin: Some(result.stdout.to_string()),
352 cwd: context.cwd.clone(),
353 env: context.env.clone(),
354 output_tx: Some(tx),
355 is_cancelled: cancelled.map(|cancelled| {
356 Box::new(move || cancelled()) as Box<dyn Fn() -> bool + Send + Sync>
357 }),
358 };
359 let collect = async {
362 let mut stdout = String::new();
363 let mut stderr = String::new();
364 while let Some(chunk) = rx.recv().await {
365 match chunk {
366 StreamChunk::Stdout(chunk) => stdout.push_str(&chunk),
367 StreamChunk::Stderr(chunk) => stderr.push_str(&chunk),
368 }
369 }
370 (stdout, stderr)
371 };
372 let (mut next, (stdout, stderr)) = tokio::join!(handler(next), collect);
373 if next.stdout.is_empty() {
374 next.stdout = stdout.into();
375 }
376 if next.stderr.is_empty() {
377 next.stderr = stderr.into();
378 }
379 result = next;
380 if result.code != 0 {
381 break;
382 }
383 }
384 result
385 })
386 })
387 }
388 pub fn install(&mut self, package: CommandPackage) -> Result<(), String> {
390 self.install_with_options(package, InstallOptions::default())
391 }
392 pub fn install_with_options(
393 &mut self,
394 package: CommandPackage,
395 options: InstallOptions,
396 ) -> Result<(), String> {
397 if package.commands.is_empty() {
398 return Err("Empty command package".into());
399 }
400 for name in package.commands.keys() {
401 validate(name)?;
402 let owned = self
403 .packages
404 .get(&package.name)
405 .and_then(|installed| installed.package.commands.get(name))
406 .zip(self.commands.get(name))
407 .is_some_and(|(owned, current)| Arc::ptr_eq(owned, current));
408 if self.get(name).is_some() && !owned && !options.replace {
409 return Err(format!("Command already registered: {name}"));
410 }
411 }
412 self.remove_package(&package.name);
413 let mut previous = HashMap::new();
414 for (name, handler) in &package.commands {
415 if let Some(displaced) = self.commands.get(name) {
416 previous.insert(
417 name.clone(),
418 (displaced.clone(), self.owners.get(name).cloned()),
419 );
420 }
421 self.owners.insert(name.clone(), package.name.clone());
422 self.commands.insert(name.clone(), handler.clone());
423 }
424 crate::trace::trace_lazy("CommandPackages", || {
425 format!("Installed {}@{}", package.name, package.version)
426 });
427 self.packages
428 .insert(package.name.clone(), Installed { package, previous });
429 Ok(())
430 }
431 pub fn uninstall(&mut self, package: &str) {
432 self.remove_package(package);
433 self.files.retain(|_, name| name != package);
434 }
435 fn remove_package(&mut self, package: &str) {
436 if let Some(installed) = self.packages.remove(package) {
437 for (name, handler) in installed.package.commands {
438 if !self
439 .commands
440 .get(&name)
441 .is_some_and(|current| Arc::ptr_eq(current, &handler))
442 {
443 continue;
444 }
445 self.owners.remove(&name);
446 self.commands.remove(&name);
447 if let Some((previous, owner)) = installed.previous.get(&name) {
448 self.commands.insert(name.clone(), previous.clone());
449 if let Some(owner) = owner {
450 self.owners.insert(name, owner.clone());
451 }
452 }
453 }
454 }
455 }
456 pub async fn install_file(&mut self, path: impl AsRef<Path>) -> Result<String, String> {
458 let file = tokio::fs::canonicalize(path)
459 .await
460 .map_err(|error| error.to_string())?;
461 if let Some(name) = self.files.get(&file) {
462 return Ok(name.clone());
463 }
464 self.reload_file(file).await
465 }
466 pub async fn install_npm(
471 &mut self,
472 specifier: &str,
473 cwd: impl AsRef<Path>,
474 install: bool,
475 ) -> Result<String, String> {
476 static NAME: once_cell::sync::Lazy<regex::Regex> = once_cell::sync::Lazy::new(|| {
477 regex::Regex::new(r"^(@[a-z0-9._-]+/)?[a-z0-9._-]+(?:@[^\s]+)?$").unwrap()
478 });
479 if !NAME.is_match(specifier) {
480 return Err("Expected an npm package name".into());
481 }
482 let name = match specifier.rfind('@') {
483 Some(index) if index > 0 => &specifier[..index],
484 _ => specifier,
485 };
486 if name
487 .split('/')
488 .any(|part| part == "." || part == ".." || part == "@..")
489 {
490 return Err("Invalid npm package name".into());
491 }
492 let cwd = cwd.as_ref();
493 let file = cwd
494 .join("node_modules")
495 .join(name)
496 .join("command-stream.json");
497 if !file.exists() && install {
498 let npm = if cfg!(windows) { "npm.cmd" } else { "npm" };
499 let result = CommandBuilder::new(npm)
500 .args([
501 "install",
502 "--ignore-scripts",
503 "--no-audit",
504 "--no-fund",
505 "--",
506 specifier,
507 ])
508 .cwd(cwd)
509 .run()
510 .await
511 .map_err(|error| error.to_string())?;
512 if result.code != 0 {
513 return Err(format!("Package installation failed: {}", result.stderr));
514 }
515 }
516 self.install_file(file).await
517 }
518 pub async fn reload_file(&mut self, path: impl AsRef<Path>) -> Result<String, String> {
520 let file = tokio::fs::canonicalize(path)
521 .await
522 .map_err(|error| error.to_string())?;
523 let package = CommandPackage::load(&file)
524 .await
525 .map_err(|error| error.to_string())?;
526 if self
527 .files
528 .iter()
529 .any(|(other, name)| other != &file && name == &package.name)
530 {
531 return Err("Package already installed from another file".into());
532 }
533 if self
534 .files
535 .get(&file)
536 .is_some_and(|name| name != &package.name)
537 {
538 return Err("Reload cannot change a package's name".into());
539 }
540 let name = package.name.clone();
541 self.install(package)?;
542 self.files.insert(file, name.clone());
543 Ok(name)
544 }
545}
546
547fn validate(name: &str) -> Result<(), String> {
548 if name.is_empty()
549 || !name
550 .chars()
551 .next()
552 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
553 || !name
554 .chars()
555 .all(|c| c.is_ascii_alphanumeric() || "_:.-".contains(c))
556 {
557 return Err(format!("Invalid command: {name}"));
558 }
559 Ok(())
560}