Skip to main content

command_stream/development/
cli.rs

1use super::{dev, start_repl, DevEvent, DevOptions, ReplOptions};
2use std::io::{self, IsTerminal};
3use std::path::Path;
4
5/// Handle development commands while preserving ordinary shell CLI arguments.
6pub async fn cli(args: &[String]) -> io::Result<Option<i32>> {
7    match args.first().map(String::as_str) {
8        Some("--help" | "-h") => {
9            println!("Usage: command-stream <command> [args...]\n  repl [--crate PATH] [--disable-readline]    Rust REPL (requires Evcxr)\n  dev Cargo.toml [watch-glob ...] [--repl]    Restart Cargo on changes\n  fy input.sh output.mjs                     Translate a shell script");
10            Ok(Some(0))
11        }
12        Some("repl") => {
13            let mut options = ReplOptions {
14                disable_readline: !io::stdin().is_terminal(),
15                ..Default::default()
16            };
17            let mut arguments = args[1..].iter();
18            while let Some(argument) = arguments.next() {
19                match argument.as_str() {
20                    "--crate" => {
21                        options.crate_path = Some(arguments.next().ok_or_else(usage)?.into())
22                    }
23                    "--disable-readline" => options.disable_readline = true,
24                    _ => return Err(usage()),
25                }
26            }
27            let piped = options.disable_readline;
28            let mut repl = start_repl(options)?;
29            loop {
30                tokio::select! {
31                    result = repl.wait() => return Ok(Some(result?.code().unwrap_or(1))),
32                    signal = tokio::signal::ctrl_c() => {
33                        signal?;
34                        if piped { repl.close().await?; return Ok(Some(0)); }
35                    },
36                    result = terminated() => { result?; repl.close().await?; return Ok(Some(0)); },
37                }
38            }
39        }
40        Some("dev") => {
41            let manifest = Path::new(args.get(1).ok_or_else(usage)?).canonicalize()?;
42            if !manifest.is_file() {
43                return Err(usage());
44            }
45            let cwd = manifest.parent().ok_or_else(usage)?.to_path_buf();
46            let watch = args[2..]
47                .iter()
48                .filter(|argument| argument.as_str() != "--repl")
49                .cloned()
50                .collect::<Vec<_>>();
51            let mut options = DevOptions {
52                cwd: cwd.clone(),
53                command: vec![
54                    "cargo".into(),
55                    "run".into(),
56                    "--manifest-path".into(),
57                    manifest.to_string_lossy().into_owned(),
58                ],
59                ..Default::default()
60            };
61            if !watch.is_empty() {
62                options.watch = watch;
63            }
64            let mut repl = if args.iter().any(|argument| argument == "--repl") {
65                Some(start_repl(ReplOptions {
66                    disable_readline: !io::stdin().is_terminal(),
67                    ..Default::default()
68                })?)
69            } else {
70                None
71            };
72            println!(
73                "command-stream Development Mode\nWatching patterns: {}",
74                options.watch.join(", ")
75            );
76            let mut controller = dev(options).await?;
77            loop {
78                tokio::select! {
79                    result = tokio::signal::ctrl_c() => { result?; break; }
80                    result = terminated() => { result?; break; }
81                    event = controller.next_event() => match event {
82                        Some(DevEvent::Error(message)) => eprintln!("Development: {message}"),
83                        Some(DevEvent::Restarted { .. }) => {},
84                        None => break,
85                    },
86                    _ = async { repl.as_mut().unwrap().wait().await }, if repl.is_some() => break,
87                }
88            }
89            if let Some(mut repl) = repl {
90                repl.close().await?;
91            }
92            controller.close().await?;
93            Ok(Some(0))
94        }
95        _ => Ok(None),
96    }
97}
98
99fn usage() -> io::Error {
100    io::Error::new(io::ErrorKind::InvalidInput, "Usage: command-stream repl [--crate PATH] [--disable-readline] | dev Cargo.toml [watch-glob ...] [--repl]")
101}
102
103async fn terminated() -> io::Result<()> {
104    #[cfg(unix)]
105    {
106        tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?
107            .recv()
108            .await;
109        Ok(())
110    }
111    #[cfg(not(unix))]
112    std::future::pending().await
113}