command-stream 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/js/src/$.ansi.mjs +147 -0
  2. package/js/src/$.mjs +49 -6382
  3. package/js/src/$.process-runner-base.mjs +563 -0
  4. package/js/src/$.process-runner-execution.mjs +1497 -0
  5. package/js/src/$.process-runner-orchestration.mjs +250 -0
  6. package/js/src/$.process-runner-pipeline.mjs +1162 -0
  7. package/js/src/$.process-runner-stream-kill.mjs +312 -0
  8. package/js/src/$.process-runner-virtual.mjs +297 -0
  9. package/js/src/$.quote.mjs +161 -0
  10. package/js/src/$.result.mjs +23 -0
  11. package/js/src/$.shell-settings.mjs +84 -0
  12. package/js/src/$.shell.mjs +157 -0
  13. package/js/src/$.state.mjs +401 -0
  14. package/js/src/$.stream-emitter.mjs +111 -0
  15. package/js/src/$.stream-utils.mjs +390 -0
  16. package/js/src/$.trace.mjs +36 -0
  17. package/js/src/$.utils.mjs +2 -23
  18. package/js/src/$.virtual-commands.mjs +113 -0
  19. package/js/src/commands/$.which.mjs +3 -1
  20. package/js/src/commands/index.mjs +24 -0
  21. package/js/src/shell-parser.mjs +125 -83
  22. package/js/tests/resource-cleanup-internals.test.mjs +22 -24
  23. package/js/tests/sigint-cleanup.test.mjs +3 -0
  24. package/package.json +1 -1
  25. package/rust/src/ansi.rs +194 -0
  26. package/rust/src/events.rs +305 -0
  27. package/rust/src/lib.rs +71 -60
  28. package/rust/src/macros.rs +165 -0
  29. package/rust/src/pipeline.rs +411 -0
  30. package/rust/src/quote.rs +161 -0
  31. package/rust/src/state.rs +333 -0
  32. package/rust/src/stream.rs +369 -0
  33. package/rust/src/trace.rs +152 -0
  34. package/rust/src/utils.rs +53 -158
  35. package/rust/tests/events.rs +207 -0
  36. package/rust/tests/macros.rs +77 -0
  37. package/rust/tests/pipeline.rs +93 -0
  38. package/rust/tests/state.rs +207 -0
  39. package/rust/tests/stream.rs +102 -0
@@ -0,0 +1,333 @@
1
+ //! Global state management for command-stream
2
+ //!
3
+ //! This module handles signal handlers, process tracking, and cleanup,
4
+ //! similar to the JavaScript $.state.mjs module.
5
+
6
+ use std::collections::HashSet;
7
+ use std::sync::atomic::{AtomicBool, Ordering};
8
+ use std::sync::Arc;
9
+ use tokio::sync::RwLock;
10
+
11
+ use crate::trace::trace_lazy;
12
+
13
+ /// Shell settings for controlling execution behavior
14
+ #[derive(Debug, Clone, Default)]
15
+ pub struct ShellSettings {
16
+ /// Exit immediately if a command exits with non-zero status (set -e)
17
+ pub errexit: bool,
18
+ /// Print commands as they are executed (set -v)
19
+ pub verbose: bool,
20
+ /// Print trace of commands (set -x)
21
+ pub xtrace: bool,
22
+ /// Return value of a pipeline is the status of the last command to exit with non-zero (set -o pipefail)
23
+ pub pipefail: bool,
24
+ /// Treat unset variables as an error (set -u)
25
+ pub nounset: bool,
26
+ /// Disable filename globbing (set -f)
27
+ pub noglob: bool,
28
+ /// Export all variables (set -a)
29
+ pub allexport: bool,
30
+ }
31
+
32
+ impl ShellSettings {
33
+ /// Create new shell settings with defaults
34
+ pub fn new() -> Self {
35
+ Self::default()
36
+ }
37
+
38
+ /// Reset all settings to their defaults
39
+ pub fn reset(&mut self) {
40
+ *self = Self::default();
41
+ }
42
+
43
+ /// Set a shell option by name
44
+ ///
45
+ /// Supports both short flags (e, v, x, u, f, a) and long names
46
+ pub fn set(&mut self, option: &str, value: bool) {
47
+ match option {
48
+ "e" | "errexit" => self.errexit = value,
49
+ "v" | "verbose" => self.verbose = value,
50
+ "x" | "xtrace" => self.xtrace = value,
51
+ "u" | "nounset" => self.nounset = value,
52
+ "f" | "noglob" => self.noglob = value,
53
+ "a" | "allexport" => self.allexport = value,
54
+ "o pipefail" | "pipefail" => self.pipefail = value,
55
+ _ => {
56
+ trace_lazy("ShellSettings", || {
57
+ format!("Unknown shell option: {}", option)
58
+ });
59
+ }
60
+ }
61
+ }
62
+
63
+ /// Enable a shell option
64
+ pub fn enable(&mut self, option: &str) {
65
+ self.set(option, true);
66
+ }
67
+
68
+ /// Disable a shell option
69
+ pub fn disable(&mut self, option: &str) {
70
+ self.set(option, false);
71
+ }
72
+ }
73
+
74
+ /// Global state for the command-stream library
75
+ pub struct GlobalState {
76
+ /// Current shell settings
77
+ shell_settings: RwLock<ShellSettings>,
78
+ /// Set of active process runner IDs
79
+ active_runners: RwLock<HashSet<u64>>,
80
+ /// Counter for generating runner IDs
81
+ next_runner_id: std::sync::atomic::AtomicU64,
82
+ /// Whether signal handlers are installed
83
+ signal_handlers_installed: AtomicBool,
84
+ /// Whether virtual commands are enabled
85
+ virtual_commands_enabled: AtomicBool,
86
+ /// Initial working directory
87
+ initial_cwd: RwLock<Option<std::path::PathBuf>>,
88
+ }
89
+
90
+ impl Default for GlobalState {
91
+ fn default() -> Self {
92
+ Self::new()
93
+ }
94
+ }
95
+
96
+ impl GlobalState {
97
+ /// Create a new global state
98
+ pub fn new() -> Self {
99
+ let initial_cwd = std::env::current_dir().ok();
100
+
101
+ GlobalState {
102
+ shell_settings: RwLock::new(ShellSettings::new()),
103
+ active_runners: RwLock::new(HashSet::new()),
104
+ next_runner_id: std::sync::atomic::AtomicU64::new(1),
105
+ signal_handlers_installed: AtomicBool::new(false),
106
+ virtual_commands_enabled: AtomicBool::new(true),
107
+ initial_cwd: RwLock::new(initial_cwd),
108
+ }
109
+ }
110
+
111
+ /// Get the current shell settings
112
+ pub async fn get_shell_settings(&self) -> ShellSettings {
113
+ self.shell_settings.read().await.clone()
114
+ }
115
+
116
+ /// Set shell settings
117
+ pub async fn set_shell_settings(&self, settings: ShellSettings) {
118
+ *self.shell_settings.write().await = settings;
119
+ }
120
+
121
+ /// Modify shell settings with a closure
122
+ pub async fn with_shell_settings<F>(&self, f: F)
123
+ where
124
+ F: FnOnce(&mut ShellSettings),
125
+ {
126
+ let mut settings = self.shell_settings.write().await;
127
+ f(&mut settings);
128
+ }
129
+
130
+ /// Enable a shell option
131
+ pub async fn enable_shell_option(&self, option: &str) {
132
+ self.shell_settings.write().await.enable(option);
133
+ }
134
+
135
+ /// Disable a shell option
136
+ pub async fn disable_shell_option(&self, option: &str) {
137
+ self.shell_settings.write().await.disable(option);
138
+ }
139
+
140
+ /// Register a new active runner and return its ID
141
+ pub async fn register_runner(&self) -> u64 {
142
+ let id = self
143
+ .next_runner_id
144
+ .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
145
+ self.active_runners.write().await.insert(id);
146
+
147
+ trace_lazy("GlobalState", || {
148
+ format!("Registered runner {}", id)
149
+ });
150
+
151
+ id
152
+ }
153
+
154
+ /// Unregister an active runner
155
+ pub async fn unregister_runner(&self, id: u64) {
156
+ self.active_runners.write().await.remove(&id);
157
+
158
+ trace_lazy("GlobalState", || {
159
+ format!("Unregistered runner {}", id)
160
+ });
161
+ }
162
+
163
+ /// Get the count of active runners
164
+ pub async fn active_runner_count(&self) -> usize {
165
+ self.active_runners.read().await.len()
166
+ }
167
+
168
+ /// Check if signal handlers are installed
169
+ pub fn are_signal_handlers_installed(&self) -> bool {
170
+ self.signal_handlers_installed.load(Ordering::SeqCst)
171
+ }
172
+
173
+ /// Mark signal handlers as installed
174
+ pub fn set_signal_handlers_installed(&self, installed: bool) {
175
+ self.signal_handlers_installed.store(installed, Ordering::SeqCst);
176
+ }
177
+
178
+ /// Check if virtual commands are enabled
179
+ pub fn are_virtual_commands_enabled(&self) -> bool {
180
+ self.virtual_commands_enabled.load(Ordering::SeqCst)
181
+ }
182
+
183
+ /// Enable virtual commands
184
+ pub fn enable_virtual_commands(&self) {
185
+ self.virtual_commands_enabled.store(true, Ordering::SeqCst);
186
+ trace_lazy("GlobalState", || "Virtual commands enabled".to_string());
187
+ }
188
+
189
+ /// Disable virtual commands
190
+ pub fn disable_virtual_commands(&self) {
191
+ self.virtual_commands_enabled.store(false, Ordering::SeqCst);
192
+ trace_lazy("GlobalState", || "Virtual commands disabled".to_string());
193
+ }
194
+
195
+ /// Get the initial working directory
196
+ pub async fn get_initial_cwd(&self) -> Option<std::path::PathBuf> {
197
+ self.initial_cwd.read().await.clone()
198
+ }
199
+
200
+ /// Reset global state to defaults
201
+ pub async fn reset(&self) {
202
+ // Reset shell settings
203
+ *self.shell_settings.write().await = ShellSettings::new();
204
+
205
+ // Clear active runners
206
+ self.active_runners.write().await.clear();
207
+
208
+ // Reset virtual commands flag
209
+ self.virtual_commands_enabled.store(true, Ordering::SeqCst);
210
+
211
+ // Don't reset signal handlers installed flag - that's managed separately
212
+
213
+ trace_lazy("GlobalState", || "Global state reset completed".to_string());
214
+ }
215
+
216
+ /// Restore working directory to initial
217
+ pub async fn restore_cwd(&self) -> std::io::Result<()> {
218
+ if let Some(ref initial) = *self.initial_cwd.read().await {
219
+ if initial.exists() {
220
+ std::env::set_current_dir(initial)?;
221
+ }
222
+ }
223
+ Ok(())
224
+ }
225
+ }
226
+
227
+ /// Global state singleton
228
+ static GLOBAL_STATE: std::sync::OnceLock<Arc<GlobalState>> = std::sync::OnceLock::new();
229
+
230
+ /// Get the global state instance
231
+ pub fn global_state() -> Arc<GlobalState> {
232
+ GLOBAL_STATE
233
+ .get_or_init(|| Arc::new(GlobalState::new()))
234
+ .clone()
235
+ }
236
+
237
+ /// Reset the global state (for testing)
238
+ pub async fn reset_global_state() {
239
+ global_state().reset().await;
240
+ }
241
+
242
+ /// Get current shell settings
243
+ pub async fn get_shell_settings() -> ShellSettings {
244
+ global_state().get_shell_settings().await
245
+ }
246
+
247
+ /// Enable a shell option globally
248
+ pub async fn set_shell_option(option: &str) {
249
+ global_state().enable_shell_option(option).await;
250
+ }
251
+
252
+ /// Disable a shell option globally
253
+ pub async fn unset_shell_option(option: &str) {
254
+ global_state().disable_shell_option(option).await;
255
+ }
256
+
257
+ #[cfg(test)]
258
+ mod tests {
259
+ use super::*;
260
+
261
+ #[test]
262
+ fn test_shell_settings_default() {
263
+ let settings = ShellSettings::new();
264
+ assert!(!settings.errexit);
265
+ assert!(!settings.verbose);
266
+ assert!(!settings.xtrace);
267
+ assert!(!settings.pipefail);
268
+ assert!(!settings.nounset);
269
+ }
270
+
271
+ #[test]
272
+ fn test_shell_settings_set() {
273
+ let mut settings = ShellSettings::new();
274
+
275
+ settings.set("e", true);
276
+ assert!(settings.errexit);
277
+
278
+ settings.set("errexit", false);
279
+ assert!(!settings.errexit);
280
+
281
+ settings.set("o pipefail", true);
282
+ assert!(settings.pipefail);
283
+ }
284
+
285
+ #[tokio::test]
286
+ async fn test_global_state_runners() {
287
+ let state = GlobalState::new();
288
+
289
+ let id1 = state.register_runner().await;
290
+ let id2 = state.register_runner().await;
291
+
292
+ assert_eq!(state.active_runner_count().await, 2);
293
+ assert!(id1 != id2);
294
+
295
+ state.unregister_runner(id1).await;
296
+ assert_eq!(state.active_runner_count().await, 1);
297
+
298
+ state.unregister_runner(id2).await;
299
+ assert_eq!(state.active_runner_count().await, 0);
300
+ }
301
+
302
+ #[tokio::test]
303
+ async fn test_global_state_virtual_commands() {
304
+ let state = GlobalState::new();
305
+
306
+ assert!(state.are_virtual_commands_enabled());
307
+
308
+ state.disable_virtual_commands();
309
+ assert!(!state.are_virtual_commands_enabled());
310
+
311
+ state.enable_virtual_commands();
312
+ assert!(state.are_virtual_commands_enabled());
313
+ }
314
+
315
+ #[tokio::test]
316
+ async fn test_global_state_reset() {
317
+ let state = GlobalState::new();
318
+
319
+ // Modify state
320
+ state.enable_shell_option("errexit").await;
321
+ state.register_runner().await;
322
+ state.disable_virtual_commands();
323
+
324
+ // Reset
325
+ state.reset().await;
326
+
327
+ // Verify reset
328
+ let settings = state.get_shell_settings().await;
329
+ assert!(!settings.errexit);
330
+ assert_eq!(state.active_runner_count().await, 0);
331
+ assert!(state.are_virtual_commands_enabled());
332
+ }
333
+ }
@@ -0,0 +1,369 @@
1
+ //! Streaming and async iteration support
2
+ //!
3
+ //! This module provides async streaming capabilities similar to JavaScript's
4
+ //! async iterators and stream handling in `$.stream-utils.mjs`.
5
+ //!
6
+ //! ## Usage
7
+ //!
8
+ //! ```rust,no_run
9
+ //! use command_stream::{StreamingRunner, OutputChunk};
10
+ //!
11
+ //! #[tokio::main]
12
+ //! async fn main() -> Result<(), Box<dyn std::error::Error>> {
13
+ //! let runner = StreamingRunner::new("yes hello");
14
+ //!
15
+ //! // Stream output as it arrives
16
+ //! let mut stream = runner.stream();
17
+ //! let mut count = 0;
18
+ //! while let Some(chunk) = stream.next().await {
19
+ //! match chunk {
20
+ //! OutputChunk::Stdout(data) => {
21
+ //! print!("{}", String::from_utf8_lossy(&data));
22
+ //! count += 1;
23
+ //! if count >= 5 {
24
+ //! break;
25
+ //! }
26
+ //! }
27
+ //! OutputChunk::Stderr(data) => {
28
+ //! eprint!("{}", String::from_utf8_lossy(&data));
29
+ //! }
30
+ //! OutputChunk::Exit(code) => {
31
+ //! println!("Process exited with code: {}", code);
32
+ //! break;
33
+ //! }
34
+ //! }
35
+ //! }
36
+ //!
37
+ //! Ok(())
38
+ //! }
39
+ //! ```
40
+
41
+ use std::collections::HashMap;
42
+ use std::path::PathBuf;
43
+ use std::process::Stdio;
44
+ use tokio::io::BufReader;
45
+ use tokio::process::Command;
46
+ use tokio::sync::mpsc;
47
+
48
+ use crate::trace::trace_lazy;
49
+ use crate::{CommandResult, Result};
50
+
51
+ /// A chunk of output from a streaming process
52
+ #[derive(Debug, Clone)]
53
+ pub enum OutputChunk {
54
+ /// Stdout data
55
+ Stdout(Vec<u8>),
56
+ /// Stderr data
57
+ Stderr(Vec<u8>),
58
+ /// Process exit code
59
+ Exit(i32),
60
+ }
61
+
62
+ /// A streaming process runner that allows async iteration over output
63
+ pub struct StreamingRunner {
64
+ command: String,
65
+ cwd: Option<PathBuf>,
66
+ env: Option<HashMap<String, String>>,
67
+ stdin_content: Option<String>,
68
+ }
69
+
70
+ impl StreamingRunner {
71
+ /// Create a new streaming runner
72
+ pub fn new(command: impl Into<String>) -> Self {
73
+ StreamingRunner {
74
+ command: command.into(),
75
+ cwd: None,
76
+ env: None,
77
+ stdin_content: None,
78
+ }
79
+ }
80
+
81
+ /// Set the working directory
82
+ pub fn cwd(mut self, path: impl Into<PathBuf>) -> Self {
83
+ self.cwd = Some(path.into());
84
+ self
85
+ }
86
+
87
+ /// Set environment variables
88
+ pub fn env(mut self, env: HashMap<String, String>) -> Self {
89
+ self.env = Some(env);
90
+ self
91
+ }
92
+
93
+ /// Set stdin content
94
+ pub fn stdin(mut self, content: impl Into<String>) -> Self {
95
+ self.stdin_content = Some(content.into());
96
+ self
97
+ }
98
+
99
+ /// Start the process and return a stream of output chunks
100
+ pub fn stream(mut self) -> OutputStream {
101
+ let (tx, rx) = mpsc::channel(1024);
102
+
103
+ // Spawn the process handling task
104
+ let command = self.command.clone();
105
+ let cwd = self.cwd.take();
106
+ let env = self.env.take();
107
+ let stdin_content = self.stdin_content.take();
108
+
109
+ tokio::spawn(async move {
110
+ if let Err(e) = run_streaming_process(command, cwd, env, stdin_content, tx.clone()).await {
111
+ trace_lazy("StreamingRunner", || format!("Error: {}", e));
112
+ }
113
+ });
114
+
115
+ OutputStream { rx }
116
+ }
117
+
118
+ /// Run to completion and collect all output
119
+ pub async fn collect(self) -> Result<CommandResult> {
120
+ let mut stdout = Vec::new();
121
+ let mut stderr = Vec::new();
122
+ let mut exit_code = 0;
123
+
124
+ let mut stream = self.stream();
125
+ while let Some(chunk) = stream.rx.recv().await {
126
+ match chunk {
127
+ OutputChunk::Stdout(data) => stdout.extend(data),
128
+ OutputChunk::Stderr(data) => stderr.extend(data),
129
+ OutputChunk::Exit(code) => exit_code = code,
130
+ }
131
+ }
132
+
133
+ Ok(CommandResult {
134
+ stdout: String::from_utf8_lossy(&stdout).to_string(),
135
+ stderr: String::from_utf8_lossy(&stderr).to_string(),
136
+ code: exit_code,
137
+ })
138
+ }
139
+ }
140
+
141
+ /// Stream of output chunks from a process
142
+ pub struct OutputStream {
143
+ rx: mpsc::Receiver<OutputChunk>,
144
+ }
145
+
146
+ impl OutputStream {
147
+ /// Receive the next chunk
148
+ pub async fn next(&mut self) -> Option<OutputChunk> {
149
+ self.rx.recv().await
150
+ }
151
+
152
+ /// Collect all remaining output into vectors
153
+ pub async fn collect(mut self) -> (Vec<u8>, Vec<u8>, i32) {
154
+ let mut stdout = Vec::new();
155
+ let mut stderr = Vec::new();
156
+ let mut exit_code = 0;
157
+
158
+ while let Some(chunk) = self.rx.recv().await {
159
+ match chunk {
160
+ OutputChunk::Stdout(data) => stdout.extend(data),
161
+ OutputChunk::Stderr(data) => stderr.extend(data),
162
+ OutputChunk::Exit(code) => exit_code = code,
163
+ }
164
+ }
165
+
166
+ (stdout, stderr, exit_code)
167
+ }
168
+
169
+ /// Collect stdout only, discarding stderr
170
+ pub async fn collect_stdout(mut self) -> Vec<u8> {
171
+ let mut stdout = Vec::new();
172
+
173
+ while let Some(chunk) = self.rx.recv().await {
174
+ if let OutputChunk::Stdout(data) = chunk {
175
+ stdout.extend(data);
176
+ }
177
+ }
178
+
179
+ stdout
180
+ }
181
+ }
182
+
183
+ /// Run a streaming process and send output to the channel
184
+ async fn run_streaming_process(
185
+ command: String,
186
+ cwd: Option<PathBuf>,
187
+ env: Option<HashMap<String, String>>,
188
+ stdin_content: Option<String>,
189
+ tx: mpsc::Sender<OutputChunk>,
190
+ ) -> Result<()> {
191
+ trace_lazy("StreamingRunner", || format!("Starting: {}", command));
192
+
193
+ let shell = find_available_shell();
194
+ let mut cmd = Command::new(&shell.cmd);
195
+ for arg in &shell.args {
196
+ cmd.arg(arg);
197
+ }
198
+ cmd.arg(&command);
199
+
200
+ // Configure stdio
201
+ if stdin_content.is_some() {
202
+ cmd.stdin(Stdio::piped());
203
+ } else {
204
+ cmd.stdin(Stdio::null());
205
+ }
206
+ cmd.stdout(Stdio::piped());
207
+ cmd.stderr(Stdio::piped());
208
+
209
+ // Set working directory
210
+ if let Some(ref cwd) = cwd {
211
+ cmd.current_dir(cwd);
212
+ }
213
+
214
+ // Set environment
215
+ if let Some(ref env_vars) = env {
216
+ for (key, value) in env_vars {
217
+ cmd.env(key, value);
218
+ }
219
+ }
220
+
221
+ // Spawn the process
222
+ let mut child = cmd.spawn()?;
223
+
224
+ // Write stdin if needed
225
+ if let Some(content) = stdin_content {
226
+ if let Some(mut stdin) = child.stdin.take() {
227
+ use tokio::io::AsyncWriteExt;
228
+ let _ = stdin.write_all(content.as_bytes()).await;
229
+ let _ = stdin.shutdown().await;
230
+ }
231
+ }
232
+
233
+ // Spawn stdout reader
234
+ let stdout = child.stdout.take();
235
+ let tx_stdout = tx.clone();
236
+ let stdout_handle = if let Some(stdout) = stdout {
237
+ Some(tokio::spawn(async move {
238
+ let mut reader = BufReader::new(stdout);
239
+ let mut buf = vec![0u8; 8192];
240
+ loop {
241
+ use tokio::io::AsyncReadExt;
242
+ match reader.read(&mut buf).await {
243
+ Ok(0) => break,
244
+ Ok(n) => {
245
+ if tx_stdout.send(OutputChunk::Stdout(buf[..n].to_vec())).await.is_err() {
246
+ break;
247
+ }
248
+ }
249
+ Err(_) => break,
250
+ }
251
+ }
252
+ }))
253
+ } else {
254
+ None
255
+ };
256
+
257
+ // Spawn stderr reader
258
+ let stderr = child.stderr.take();
259
+ let tx_stderr = tx.clone();
260
+ let stderr_handle = if let Some(stderr) = stderr {
261
+ Some(tokio::spawn(async move {
262
+ let mut reader = BufReader::new(stderr);
263
+ let mut buf = vec![0u8; 8192];
264
+ loop {
265
+ use tokio::io::AsyncReadExt;
266
+ match reader.read(&mut buf).await {
267
+ Ok(0) => break,
268
+ Ok(n) => {
269
+ if tx_stderr.send(OutputChunk::Stderr(buf[..n].to_vec())).await.is_err() {
270
+ break;
271
+ }
272
+ }
273
+ Err(_) => break,
274
+ }
275
+ }
276
+ }))
277
+ } else {
278
+ None
279
+ };
280
+
281
+ // Wait for readers to complete
282
+ if let Some(handle) = stdout_handle {
283
+ let _ = handle.await;
284
+ }
285
+ if let Some(handle) = stderr_handle {
286
+ let _ = handle.await;
287
+ }
288
+
289
+ // Wait for process to exit
290
+ let status = child.wait().await?;
291
+ let code = status.code().unwrap_or(-1);
292
+
293
+ // Send exit code
294
+ let _ = tx.send(OutputChunk::Exit(code)).await;
295
+
296
+ trace_lazy("StreamingRunner", || format!("Exited with code: {}", code));
297
+
298
+ Ok(())
299
+ }
300
+
301
+ /// Shell configuration
302
+ #[derive(Debug, Clone)]
303
+ struct ShellConfig {
304
+ cmd: String,
305
+ args: Vec<String>,
306
+ }
307
+
308
+ /// Find an available shell
309
+ fn find_available_shell() -> ShellConfig {
310
+ let is_windows = cfg!(windows);
311
+
312
+ if is_windows {
313
+ ShellConfig {
314
+ cmd: "cmd.exe".to_string(),
315
+ args: vec!["/c".to_string()],
316
+ }
317
+ } else {
318
+ let shells = [
319
+ ("/bin/sh", "-c"),
320
+ ("/usr/bin/sh", "-c"),
321
+ ("/bin/bash", "-c"),
322
+ ];
323
+
324
+ for (cmd, arg) in shells {
325
+ if std::path::Path::new(cmd).exists() {
326
+ return ShellConfig {
327
+ cmd: cmd.to_string(),
328
+ args: vec![arg.to_string()],
329
+ };
330
+ }
331
+ }
332
+
333
+ ShellConfig {
334
+ cmd: "/bin/sh".to_string(),
335
+ args: vec!["-c".to_string()],
336
+ }
337
+ }
338
+ }
339
+
340
+ /// Async iterator trait for output streams
341
+ #[async_trait::async_trait]
342
+ pub trait AsyncIterator {
343
+ type Item;
344
+
345
+ /// Get the next item from the iterator
346
+ async fn next(&mut self) -> Option<Self::Item>;
347
+ }
348
+
349
+ #[async_trait::async_trait]
350
+ impl AsyncIterator for OutputStream {
351
+ type Item = OutputChunk;
352
+
353
+ async fn next(&mut self) -> Option<Self::Item> {
354
+ self.rx.recv().await
355
+ }
356
+ }
357
+
358
+ /// Extension trait to convert ProcessRunner into a stream
359
+ pub trait IntoStream {
360
+ /// Convert into an output stream
361
+ fn into_stream(self) -> OutputStream;
362
+ }
363
+
364
+ impl IntoStream for crate::ProcessRunner {
365
+ fn into_stream(self) -> OutputStream {
366
+ let streaming = StreamingRunner::new(self.command().to_string());
367
+ streaming.stream()
368
+ }
369
+ }