diff --git a/.changeset/rust-translation.md b/.changeset/rust-translation.md new file mode 100644 index 0000000..3452d91 --- /dev/null +++ b/.changeset/rust-translation.md @@ -0,0 +1,14 @@ +--- +'command-stream': minor +--- + +Add Rust translation and reorganize codebase + +- Reorganize JavaScript source files into `js/` folder structure +- Move tests from root `tests/` to `js/tests/` +- Add complete Rust translation in `rust/` folder with: + - Shell parser supporting &&, ||, ;, |, (), and redirections + - All 21 virtual commands (cat, cp, mv, rm, touch, mkdir, ls, cd, pwd, echo, yes, seq, sleep, env, which, test, exit, basename, dirname, true, false) + - ProcessRunner for async command execution with tokio + - Comprehensive test suite mirroring JavaScript tests + - Case study documentation in docs/case-studies/issue-146/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 585e6f6..916f824 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -151,7 +151,7 @@ jobs: - name: Run tests (Bun) if: matrix.runtime == 'bun' - run: bun test tests/ + run: bun test js/tests/ env: COMMAND_STREAM_VERBOSE: true @@ -166,7 +166,7 @@ jobs: run: | node -e " try { - const { $ } = require('./src/\$.mjs'); + const { $ } = require('./js/src/\$.mjs'); console.log('✅ Module loads successfully in Node.js ${{ matrix.node-version }}'); } catch (error) { console.error('❌ Module failed to load:', error.message); diff --git a/.gitignore b/.gitignore index a6d35d7..9042777 100644 --- a/.gitignore +++ b/.gitignore @@ -142,4 +142,7 @@ vite.config.ts.timestamp-* .persisted-configs # jscpd reports -reports/ \ No newline at end of file +reports/ + +# Rust build artifacts +rust/target/ diff --git a/bunfig.toml b/bunfig.toml index b8ab7a8..5ba5351 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -1,8 +1,8 @@ # Bun configuration file [test] -# Only run tests from the tests/ directory -root = "./tests" +# Only run tests from the js/tests/ directory +root = "./js/tests" # Increase timeout from default 5000ms to 10000ms (2x) for CI stability timeout = 10000 diff --git a/claude-profiles.mjs b/claude-profiles.mjs index 95b89e0..b2fb02f 100755 --- a/claude-profiles.mjs +++ b/claude-profiles.mjs @@ -14,7 +14,7 @@ * - Verbose logging and file logging support */ -import { $ } from './src/$.mjs'; +import { $ } from './js/src/$.mjs'; import fs, { createWriteStream, promises as fsPromises } from 'fs'; import path from 'path'; import os from 'os'; diff --git a/docs/case-studies/issue-146/README.md b/docs/case-studies/issue-146/README.md new file mode 100644 index 0000000..3b32008 --- /dev/null +++ b/docs/case-studies/issue-146/README.md @@ -0,0 +1,244 @@ +# Case Study: JavaScript to Rust Translation (Issue #146) + +## Summary + +This document provides a comprehensive analysis of the process, challenges, and lessons learned from translating the command-stream JavaScript library to Rust. + +## Project Overview + +### Original JavaScript Codebase + +- **Main file**: `src/$.mjs` (~6,765 lines) +- **Shell parser**: `src/shell-parser.mjs` (~403 lines) +- **Utilities**: `src/$.utils.mjs` (~101 lines) +- **Virtual commands**: 21 command files in `src/commands/` +- **Total**: ~8,400 lines of JavaScript + +### Rust Translation + +- **Main library**: `rust/src/lib.rs` +- **Shell parser**: `rust/src/shell_parser.rs` +- **Utilities**: `rust/src/utils.rs` +- **Virtual commands**: 21 command modules in `rust/src/commands/` + +## Timeline of Development + +### Phase 1: Code Organization + +1. Created `js/` folder structure to house JavaScript code +2. Updated `package.json` to point to new `js/src/` location +3. Updated all import statements in tests and examples + +### Phase 2: Rust Project Setup + +1. Created `rust/` folder with Cargo.toml +2. Defined dependencies: + - `tokio` for async runtime + - `which` for command lookup + - `nix` for Unix signal handling + - `regex` for pattern matching + - `chrono` for timestamps + - `filetime` for file timestamp operations + +### Phase 3: Core Translation + +1. Translated shell parser (tokenizer, parser, AST types) +2. Translated utilities (tracing, command results, ANSI handling) +3. Translated main library (ProcessRunner, shell detection) +4. Translated all 21 virtual commands + +## Key Translation Patterns + +### 1. JavaScript Async to Rust Async + +**JavaScript:** + +```javascript +async function sleep({ args, abortSignal }) { + const seconds = parseFloat(args[0] || 0); + await new Promise((resolve) => setTimeout(resolve, seconds * 1000)); + return { stdout: '', code: 0 }; +} +``` + +**Rust:** + +```rust +pub async fn sleep(ctx: CommandContext) -> CommandResult { + let seconds: f64 = ctx.args.first() + .and_then(|s| s.parse().ok()) + .unwrap_or(0.0); + + tokio::time::sleep(Duration::from_secs_f64(seconds)).await; + CommandResult::success_empty() +} +``` + +### 2. JavaScript Object Literals to Rust Structs + +**JavaScript:** + +```javascript +const result = { + stdout: output, + stderr: '', + code: 0, + async text() { + return this.stdout; + }, +}; +``` + +**Rust:** + +```rust +#[derive(Debug, Clone)] +pub struct CommandResult { + pub stdout: String, + pub stderr: String, + pub code: i32, +} + +impl CommandResult { + pub fn success(stdout: impl Into) -> Self { + CommandResult { + stdout: stdout.into(), + stderr: String::new(), + code: 0, + } + } +} +``` + +### 3. JavaScript Closures to Rust Trait Objects + +**JavaScript:** + +```javascript +function trace(category, messageOrFunc) { + const message = + typeof messageOrFunc === 'function' ? messageOrFunc() : messageOrFunc; + console.error(`[TRACE] [${category}] ${message}`); +} +``` + +**Rust:** + +```rust +pub fn trace_lazy(category: &str, message_fn: F) +where + F: FnOnce() -> String, +{ + if !is_trace_enabled() { + return; + } + trace(category, &message_fn()); +} +``` + +### 4. JavaScript Error Handling to Rust Result Types + +**JavaScript:** + +```javascript +try { + const content = fs.readFileSync(path, 'utf8'); + return { stdout: content, code: 0 }; +} catch (error) { + if (error.code === 'ENOENT') { + return { stderr: `cat: ${file}: No such file or directory`, code: 1 }; + } + throw error; +} +``` + +**Rust:** + +```rust +match fs::read_to_string(&path) { + Ok(content) => CommandResult::success(content), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + CommandResult::error(format!("cat: {}: No such file or directory\n", file)) + } + Err(e) => CommandResult::error(format!("cat: {}: {}\n", file, e)), +} +``` + +## Challenges Encountered + +### 1. Tagged Template Literals + +JavaScript's tagged template literal syntax `$\`echo hello\``has no direct Rust equivalent. We implemented the`$()` function as a regular function call instead. + +### 2. Event Emitter Pattern + +JavaScript's EventEmitter pattern required translation to Rust's channel-based communication using `tokio::sync::mpsc`. + +### 3. Process Group Handling + +Unix process group management differs between Node.js and Rust. We used the `nix` crate for proper signal handling. + +### 4. Async Iterator Pattern + +JavaScript's `for await (const chunk of stream)` was translated to Rust's async stream patterns using channels. + +## Lessons Learned + +### 1. Type Safety Benefits + +Rust's type system caught several edge cases that existed in the JavaScript code: + +- Null/undefined handling became explicit with `Option` +- Error handling became explicit with `Result` +- String encoding issues were caught at compile time + +### 2. Memory Management + +Rust's ownership model required explicit decisions about: + +- When to clone vs borrow data +- Lifetime of process handles +- Cleanup of resources on cancellation + +### 3. Cross-Platform Considerations + +Both JavaScript and Rust require platform-specific code for: + +- Shell detection (Windows vs Unix) +- Signal handling (SIGINT, SIGTERM) +- File permissions + +### 4. Testing Strategy + +Unit tests were essential for: + +- Verifying parity with JavaScript behavior +- Catching edge cases early +- Documenting expected behavior + +## Architecture Comparison + +| Component | JavaScript | Rust | +| --------------- | ---------------------- | ----------------------- | +| Async Runtime | Node.js/Bun event loop | Tokio | +| Process Spawn | child_process.spawn | tokio::process::Command | +| Channels | EventEmitter | mpsc channels | +| Error Handling | try/catch | Result | +| String Handling | UTF-16 strings | UTF-8 String | +| File I/O | fs module | std::fs | +| Signal Handling | process.on('SIGINT') | tokio::signal | + +## Future Improvements + +1. **Streaming Improvements**: Implement async iterator traits for better streaming support +2. **Error Types**: Create more specific error types for different failure modes +3. **Performance**: Benchmark and optimize critical paths +4. **Platform Support**: Add more Windows-specific implementations +5. **CI/CD**: Add Rust builds to existing CI pipeline + +## References + +- Original Issue: https://github.com/link-foundation/command-stream/issues/146 +- Pull Request: https://github.com/link-foundation/command-stream/pull/147 +- Rust Book: https://doc.rust-lang.org/book/ +- Tokio Documentation: https://tokio.rs/ diff --git a/eslint.config.js b/eslint.config.js index 970e9f9..45be7aa 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -120,7 +120,14 @@ export default [ }, { // Test files have different requirements - files: ['tests/**/*.js', 'tests/**/*.mjs', '**/*.test.js', '**/*.test.mjs'], + files: [ + 'tests/**/*.js', + 'tests/**/*.mjs', + 'js/tests/**/*.js', + 'js/tests/**/*.mjs', + '**/*.test.js', + '**/*.test.mjs', + ], rules: { 'no-unused-vars': 'off', // Tests often have unused vars for demonstration or intentional non-use 'require-await': 'off', // Async functions without await are common in tests @@ -138,7 +145,13 @@ export default [ }, { // Example and debug files are more lenient - files: ['examples/**/*.js', 'examples/**/*.mjs', 'claude-profiles.mjs'], + files: [ + 'examples/**/*.js', + 'examples/**/*.mjs', + 'js/examples/**/*.js', + 'js/examples/**/*.mjs', + 'claude-profiles.mjs', + ], rules: { 'no-unused-vars': 'off', // Examples often have unused vars for demonstration 'require-await': 'off', // Async functions without await are common in examples @@ -160,7 +173,7 @@ export default [ }, { // Virtual command implementations have specific interface requirements - files: ['src/commands/**/*.mjs'], + files: ['src/commands/**/*.mjs', 'js/src/commands/**/*.mjs'], rules: { 'require-await': 'off', // Commands must be async to match interface even if they don't await complexity: 'off', // Commands can be complex due to argument parsing and validation diff --git a/examples/01-basic-streaming.mjs b/js/examples/01-basic-streaming.mjs similarity index 100% rename from examples/01-basic-streaming.mjs rename to js/examples/01-basic-streaming.mjs diff --git a/examples/02-async-iterator.mjs b/js/examples/02-async-iterator.mjs similarity index 100% rename from examples/02-async-iterator.mjs rename to js/examples/02-async-iterator.mjs diff --git a/examples/03-file-and-console.mjs b/js/examples/03-file-and-console.mjs similarity index 100% rename from examples/03-file-and-console.mjs rename to js/examples/03-file-and-console.mjs diff --git a/examples/04-claude-jq-pipe.mjs b/js/examples/04-claude-jq-pipe.mjs similarity index 100% rename from examples/04-claude-jq-pipe.mjs rename to js/examples/04-claude-jq-pipe.mjs diff --git a/examples/CI-DEBUG-README.md b/js/examples/CI-DEBUG-README.md similarity index 92% rename from examples/CI-DEBUG-README.md rename to js/examples/CI-DEBUG-README.md index 7020afa..86cdc84 100644 --- a/examples/CI-DEBUG-README.md +++ b/js/examples/CI-DEBUG-README.md @@ -11,7 +11,7 @@ This directory contains examples for debugging common issues that occur in CI en **Solution**: Force stdout flush when not in TTY mode. ```bash -node examples/ci-debug-stdout-buffering.mjs +node js/examples/ci-debug-stdout-buffering.mjs ``` ### 2. ES Module Loading Failures (`ci-debug-es-module-loading.mjs`) @@ -21,7 +21,7 @@ node examples/ci-debug-stdout-buffering.mjs **Solution**: Use different module loading strategies or fallback to shell commands. ```bash -node examples/ci-debug-es-module-loading.mjs +node js/examples/ci-debug-es-module-loading.mjs ``` ### 3. Signal Handling (`ci-debug-signal-handling.mjs`) @@ -31,7 +31,7 @@ node examples/ci-debug-es-module-loading.mjs **Solution**: Proper signal forwarding and environment-aware cleanup strategies. ```bash -node examples/ci-debug-signal-handling.mjs +node js/examples/ci-debug-signal-handling.mjs ``` ### 4. Test Timeouts (`ci-debug-test-timeouts.mjs`) @@ -41,7 +41,7 @@ node examples/ci-debug-signal-handling.mjs **Solution**: Add explicit timeouts to all tests and implement timeout strategies. ```bash -node examples/ci-debug-test-timeouts.mjs +node js/examples/ci-debug-test-timeouts.mjs ``` ### 5. Baseline vs Library Testing (`ci-debug-baseline-vs-library.mjs`) @@ -51,7 +51,7 @@ node examples/ci-debug-test-timeouts.mjs **Solution**: Test both raw spawn and library functionality for comparison. ```bash -node examples/ci-debug-baseline-vs-library.mjs +node js/examples/ci-debug-baseline-vs-library.mjs ``` ## Quick Debugging Checklist @@ -113,7 +113,7 @@ process.on('exit', cleanup); ```bash # Run all CI debugging examples -for f in examples/ci-debug-*.mjs; do +for f in js/examples/ci-debug-*.mjs; do echo "Running $f..." node "$f" echo "" diff --git a/examples/README-examples.mjs b/js/examples/README-examples.mjs similarity index 100% rename from examples/README-examples.mjs rename to js/examples/README-examples.mjs diff --git a/examples/README.md b/js/examples/README.md similarity index 98% rename from examples/README.md rename to js/examples/README.md index 9a0df93..07bbf25 100644 --- a/examples/README.md +++ b/js/examples/README.md @@ -318,19 +318,19 @@ The simplest examples to get started: ```bash # Run a basic example -bun examples/ping-streaming-simple.mjs +bun js/examples/ping-streaming-simple.mjs # Test ANSI color handling -node examples/colors-default-preserved.mjs +node js/examples/colors-default-preserved.mjs # Try CTRL+C signal handling -node examples/ctrl-c-long-running-command.mjs +node js/examples/ctrl-c-long-running-command.mjs # Test streaming with options -node examples/options-streaming-silent.mjs +node js/examples/options-streaming-silent.mjs # Event-based processing -node examples/events-log-processing.mjs +node js/examples/events-log-processing.mjs ``` ## File Naming Convention diff --git a/examples/STREAMING_INTERFACES_SUMMARY.md b/js/examples/STREAMING_INTERFACES_SUMMARY.md similarity index 100% rename from examples/STREAMING_INTERFACES_SUMMARY.md rename to js/examples/STREAMING_INTERFACES_SUMMARY.md diff --git a/examples/add-test-timeouts.js b/js/examples/add-test-timeouts.js similarity index 100% rename from examples/add-test-timeouts.js rename to js/examples/add-test-timeouts.js diff --git a/examples/ansi-default-preserved.mjs b/js/examples/ansi-default-preserved.mjs similarity index 100% rename from examples/ansi-default-preserved.mjs rename to js/examples/ansi-default-preserved.mjs diff --git a/examples/ansi-global-config.mjs b/js/examples/ansi-global-config.mjs similarity index 100% rename from examples/ansi-global-config.mjs rename to js/examples/ansi-global-config.mjs diff --git a/examples/ansi-reset-default.mjs b/js/examples/ansi-reset-default.mjs similarity index 100% rename from examples/ansi-reset-default.mjs rename to js/examples/ansi-reset-default.mjs diff --git a/examples/ansi-strip-utils.mjs b/js/examples/ansi-strip-utils.mjs similarity index 100% rename from examples/ansi-strip-utils.mjs rename to js/examples/ansi-strip-utils.mjs diff --git a/examples/baseline-child-process.mjs b/js/examples/baseline-child-process.mjs similarity index 100% rename from examples/baseline-child-process.mjs rename to js/examples/baseline-child-process.mjs diff --git a/examples/baseline-claude-test.mjs b/js/examples/baseline-claude-test.mjs similarity index 100% rename from examples/baseline-claude-test.mjs rename to js/examples/baseline-claude-test.mjs diff --git a/examples/baseline-working.mjs b/js/examples/baseline-working.mjs similarity index 100% rename from examples/baseline-working.mjs rename to js/examples/baseline-working.mjs diff --git a/examples/capture-mirror-comparison.mjs b/js/examples/capture-mirror-comparison.mjs similarity index 100% rename from examples/capture-mirror-comparison.mjs rename to js/examples/capture-mirror-comparison.mjs diff --git a/examples/capture-mirror-default.mjs b/js/examples/capture-mirror-default.mjs similarity index 100% rename from examples/capture-mirror-default.mjs rename to js/examples/capture-mirror-default.mjs diff --git a/examples/capture-mirror-performance.mjs b/js/examples/capture-mirror-performance.mjs similarity index 100% rename from examples/capture-mirror-performance.mjs rename to js/examples/capture-mirror-performance.mjs diff --git a/examples/capture-mirror-show-only.mjs b/js/examples/capture-mirror-show-only.mjs similarity index 100% rename from examples/capture-mirror-show-only.mjs rename to js/examples/capture-mirror-show-only.mjs diff --git a/examples/capture-mirror-silent-processing.mjs b/js/examples/capture-mirror-silent-processing.mjs similarity index 100% rename from examples/capture-mirror-silent-processing.mjs rename to js/examples/capture-mirror-silent-processing.mjs diff --git a/examples/ci-debug-baseline-vs-library.mjs b/js/examples/ci-debug-baseline-vs-library.mjs similarity index 100% rename from examples/ci-debug-baseline-vs-library.mjs rename to js/examples/ci-debug-baseline-vs-library.mjs diff --git a/examples/ci-debug-es-module-loading.mjs b/js/examples/ci-debug-es-module-loading.mjs similarity index 100% rename from examples/ci-debug-es-module-loading.mjs rename to js/examples/ci-debug-es-module-loading.mjs diff --git a/examples/ci-debug-signal-handling.mjs b/js/examples/ci-debug-signal-handling.mjs similarity index 100% rename from examples/ci-debug-signal-handling.mjs rename to js/examples/ci-debug-signal-handling.mjs diff --git a/examples/ci-debug-stdout-buffering.mjs b/js/examples/ci-debug-stdout-buffering.mjs similarity index 100% rename from examples/ci-debug-stdout-buffering.mjs rename to js/examples/ci-debug-stdout-buffering.mjs diff --git a/examples/ci-debug-test-timeouts.mjs b/js/examples/ci-debug-test-timeouts.mjs similarity index 100% rename from examples/ci-debug-test-timeouts.mjs rename to js/examples/ci-debug-test-timeouts.mjs diff --git a/examples/claude-exact-file-output.mjs b/js/examples/claude-exact-file-output.mjs similarity index 100% rename from examples/claude-exact-file-output.mjs rename to js/examples/claude-exact-file-output.mjs diff --git a/examples/claude-exact-jq.mjs b/js/examples/claude-exact-jq.mjs similarity index 100% rename from examples/claude-exact-jq.mjs rename to js/examples/claude-exact-jq.mjs diff --git a/examples/claude-exact-streaming.mjs b/js/examples/claude-exact-streaming.mjs similarity index 100% rename from examples/claude-exact-streaming.mjs rename to js/examples/claude-exact-streaming.mjs diff --git a/examples/claude-jq-pipeline.mjs b/js/examples/claude-jq-pipeline.mjs similarity index 100% rename from examples/claude-jq-pipeline.mjs rename to js/examples/claude-jq-pipeline.mjs diff --git a/examples/claude-json-stream.mjs b/js/examples/claude-json-stream.mjs similarity index 100% rename from examples/claude-json-stream.mjs rename to js/examples/claude-json-stream.mjs diff --git a/examples/claude-streaming-basic.mjs b/js/examples/claude-streaming-basic.mjs similarity index 100% rename from examples/claude-streaming-basic.mjs rename to js/examples/claude-streaming-basic.mjs diff --git a/examples/claude-streaming-demo.mjs b/js/examples/claude-streaming-demo.mjs similarity index 100% rename from examples/claude-streaming-demo.mjs rename to js/examples/claude-streaming-demo.mjs diff --git a/examples/claude-streaming-final.mjs b/js/examples/claude-streaming-final.mjs similarity index 100% rename from examples/claude-streaming-final.mjs rename to js/examples/claude-streaming-final.mjs diff --git a/examples/cleanup-verification-test.mjs b/js/examples/cleanup-verification-test.mjs similarity index 100% rename from examples/cleanup-verification-test.mjs rename to js/examples/cleanup-verification-test.mjs diff --git a/examples/colors-buffer-processing.mjs b/js/examples/colors-buffer-processing.mjs similarity index 100% rename from examples/colors-buffer-processing.mjs rename to js/examples/colors-buffer-processing.mjs diff --git a/examples/colors-default-preserved.mjs b/js/examples/colors-default-preserved.mjs similarity index 100% rename from examples/colors-default-preserved.mjs rename to js/examples/colors-default-preserved.mjs diff --git a/examples/colors-per-command-config.mjs b/js/examples/colors-per-command-config.mjs similarity index 100% rename from examples/colors-per-command-config.mjs rename to js/examples/colors-per-command-config.mjs diff --git a/examples/colors-strip-ansi.mjs b/js/examples/colors-strip-ansi.mjs similarity index 100% rename from examples/colors-strip-ansi.mjs rename to js/examples/colors-strip-ansi.mjs diff --git a/examples/commandstream-jq.mjs b/js/examples/commandstream-jq.mjs similarity index 100% rename from examples/commandstream-jq.mjs rename to js/examples/commandstream-jq.mjs diff --git a/examples/commandstream-working.mjs b/js/examples/commandstream-working.mjs similarity index 100% rename from examples/commandstream-working.mjs rename to js/examples/commandstream-working.mjs diff --git a/examples/comprehensive-streams-demo.mjs b/js/examples/comprehensive-streams-demo.mjs similarity index 100% rename from examples/comprehensive-streams-demo.mjs rename to js/examples/comprehensive-streams-demo.mjs diff --git a/examples/ctrl-c-concurrent-processes.mjs b/js/examples/ctrl-c-concurrent-processes.mjs similarity index 100% rename from examples/ctrl-c-concurrent-processes.mjs rename to js/examples/ctrl-c-concurrent-processes.mjs diff --git a/examples/ctrl-c-long-running-command.mjs b/js/examples/ctrl-c-long-running-command.mjs similarity index 100% rename from examples/ctrl-c-long-running-command.mjs rename to js/examples/ctrl-c-long-running-command.mjs diff --git a/examples/ctrl-c-real-system-command.mjs b/js/examples/ctrl-c-real-system-command.mjs similarity index 100% rename from examples/ctrl-c-real-system-command.mjs rename to js/examples/ctrl-c-real-system-command.mjs diff --git a/examples/ctrl-c-sleep-command.mjs b/js/examples/ctrl-c-sleep-command.mjs similarity index 100% rename from examples/ctrl-c-sleep-command.mjs rename to js/examples/ctrl-c-sleep-command.mjs diff --git a/examples/ctrl-c-stdin-forwarding.mjs b/js/examples/ctrl-c-stdin-forwarding.mjs similarity index 100% rename from examples/ctrl-c-stdin-forwarding.mjs rename to js/examples/ctrl-c-stdin-forwarding.mjs diff --git a/examples/ctrl-c-virtual-command.mjs b/js/examples/ctrl-c-virtual-command.mjs similarity index 100% rename from examples/ctrl-c-virtual-command.mjs rename to js/examples/ctrl-c-virtual-command.mjs diff --git a/examples/debug-already-started.mjs b/js/examples/debug-already-started.mjs similarity index 100% rename from examples/debug-already-started.mjs rename to js/examples/debug-already-started.mjs diff --git a/examples/debug-ansi-processing.mjs b/js/examples/debug-ansi-processing.mjs similarity index 100% rename from examples/debug-ansi-processing.mjs rename to js/examples/debug-ansi-processing.mjs diff --git a/examples/debug-basic-streaming.mjs b/js/examples/debug-basic-streaming.mjs similarity index 100% rename from examples/debug-basic-streaming.mjs rename to js/examples/debug-basic-streaming.mjs diff --git a/examples/debug-buildshellcommand.mjs b/js/examples/debug-buildshellcommand.mjs similarity index 100% rename from examples/debug-buildshellcommand.mjs rename to js/examples/debug-buildshellcommand.mjs diff --git a/examples/debug-child-process.mjs b/js/examples/debug-child-process.mjs similarity index 100% rename from examples/debug-child-process.mjs rename to js/examples/debug-child-process.mjs diff --git a/examples/debug-child-state.mjs b/js/examples/debug-child-state.mjs similarity index 100% rename from examples/debug-child-state.mjs rename to js/examples/debug-child-state.mjs diff --git a/examples/debug-chunking.mjs b/js/examples/debug-chunking.mjs similarity index 100% rename from examples/debug-chunking.mjs rename to js/examples/debug-chunking.mjs diff --git a/examples/debug-command-parsing.mjs b/js/examples/debug-command-parsing.mjs similarity index 100% rename from examples/debug-command-parsing.mjs rename to js/examples/debug-command-parsing.mjs diff --git a/examples/debug-complete-consolidation.mjs b/js/examples/debug-complete-consolidation.mjs similarity index 100% rename from examples/debug-complete-consolidation.mjs rename to js/examples/debug-complete-consolidation.mjs diff --git a/examples/debug-echo-args.mjs b/js/examples/debug-echo-args.mjs similarity index 100% rename from examples/debug-echo-args.mjs rename to js/examples/debug-echo-args.mjs diff --git a/examples/debug-emit-timing.mjs b/js/examples/debug-emit-timing.mjs similarity index 100% rename from examples/debug-emit-timing.mjs rename to js/examples/debug-emit-timing.mjs diff --git a/examples/debug-end-event.mjs b/js/examples/debug-end-event.mjs similarity index 100% rename from examples/debug-end-event.mjs rename to js/examples/debug-end-event.mjs diff --git a/examples/debug-errexit.mjs b/js/examples/debug-errexit.mjs similarity index 100% rename from examples/debug-errexit.mjs rename to js/examples/debug-errexit.mjs diff --git a/examples/debug-event-emission.mjs b/js/examples/debug-event-emission.mjs similarity index 100% rename from examples/debug-event-emission.mjs rename to js/examples/debug-event-emission.mjs diff --git a/examples/debug-event-timing.mjs b/js/examples/debug-event-timing.mjs similarity index 100% rename from examples/debug-event-timing.mjs rename to js/examples/debug-event-timing.mjs diff --git a/examples/debug-event-vs-result.mjs b/js/examples/debug-event-vs-result.mjs similarity index 100% rename from examples/debug-event-vs-result.mjs rename to js/examples/debug-event-vs-result.mjs diff --git a/examples/debug-exact-command.mjs b/js/examples/debug-exact-command.mjs similarity index 100% rename from examples/debug-exact-command.mjs rename to js/examples/debug-exact-command.mjs diff --git a/examples/debug-exact-test-scenario.mjs b/js/examples/debug-exact-test-scenario.mjs similarity index 100% rename from examples/debug-exact-test-scenario.mjs rename to js/examples/debug-exact-test-scenario.mjs diff --git a/examples/debug-execution-path.mjs b/js/examples/debug-execution-path.mjs similarity index 100% rename from examples/debug-execution-path.mjs rename to js/examples/debug-execution-path.mjs diff --git a/examples/debug-exit-command.mjs b/js/examples/debug-exit-command.mjs similarity index 100% rename from examples/debug-exit-command.mjs rename to js/examples/debug-exit-command.mjs diff --git a/examples/debug-exit-virtual.mjs b/js/examples/debug-exit-virtual.mjs similarity index 100% rename from examples/debug-exit-virtual.mjs rename to js/examples/debug-exit-virtual.mjs diff --git a/examples/debug-finish-consolidation.mjs b/js/examples/debug-finish-consolidation.mjs similarity index 100% rename from examples/debug-finish-consolidation.mjs rename to js/examples/debug-finish-consolidation.mjs diff --git a/examples/debug-force-cleanup.mjs b/js/examples/debug-force-cleanup.mjs similarity index 100% rename from examples/debug-force-cleanup.mjs rename to js/examples/debug-force-cleanup.mjs diff --git a/examples/debug-getter-basic.mjs b/js/examples/debug-getter-basic.mjs similarity index 100% rename from examples/debug-getter-basic.mjs rename to js/examples/debug-getter-basic.mjs diff --git a/examples/debug-getter-direct.mjs b/js/examples/debug-getter-direct.mjs similarity index 100% rename from examples/debug-getter-direct.mjs rename to js/examples/debug-getter-direct.mjs diff --git a/examples/debug-getter-internals.mjs b/js/examples/debug-getter-internals.mjs similarity index 100% rename from examples/debug-getter-internals.mjs rename to js/examples/debug-getter-internals.mjs diff --git a/examples/debug-handler-detection.mjs b/js/examples/debug-handler-detection.mjs similarity index 100% rename from examples/debug-handler-detection.mjs rename to js/examples/debug-handler-detection.mjs diff --git a/examples/debug-idempotent-finish.mjs b/js/examples/debug-idempotent-finish.mjs similarity index 100% rename from examples/debug-idempotent-finish.mjs rename to js/examples/debug-idempotent-finish.mjs diff --git a/examples/debug-idempotent-kill.mjs b/js/examples/debug-idempotent-kill.mjs similarity index 100% rename from examples/debug-idempotent-kill.mjs rename to js/examples/debug-idempotent-kill.mjs diff --git a/examples/debug-interpolation-individual.mjs b/js/examples/debug-interpolation-individual.mjs similarity index 100% rename from examples/debug-interpolation-individual.mjs rename to js/examples/debug-interpolation-individual.mjs diff --git a/examples/debug-interpolation-issue.mjs b/js/examples/debug-interpolation-issue.mjs similarity index 100% rename from examples/debug-interpolation-issue.mjs rename to js/examples/debug-interpolation-issue.mjs diff --git a/examples/debug-jq-streaming.mjs b/js/examples/debug-jq-streaming.mjs similarity index 100% rename from examples/debug-jq-streaming.mjs rename to js/examples/debug-jq-streaming.mjs diff --git a/examples/debug-jq-tty-colors.mjs b/js/examples/debug-jq-tty-colors.mjs similarity index 100% rename from examples/debug-jq-tty-colors.mjs rename to js/examples/debug-jq-tty-colors.mjs diff --git a/examples/debug-kill-cleanup.mjs b/js/examples/debug-kill-cleanup.mjs similarity index 100% rename from examples/debug-kill-cleanup.mjs rename to js/examples/debug-kill-cleanup.mjs diff --git a/examples/debug-kill-method.mjs b/js/examples/debug-kill-method.mjs similarity index 100% rename from examples/debug-kill-method.mjs rename to js/examples/debug-kill-method.mjs diff --git a/examples/debug-listener-interference.mjs b/js/examples/debug-listener-interference.mjs similarity index 100% rename from examples/debug-listener-interference.mjs rename to js/examples/debug-listener-interference.mjs diff --git a/examples/debug-listener-lifecycle.mjs b/js/examples/debug-listener-lifecycle.mjs similarity index 100% rename from examples/debug-listener-lifecycle.mjs rename to js/examples/debug-listener-lifecycle.mjs diff --git a/examples/debug-listener-timing.mjs b/js/examples/debug-listener-timing.mjs similarity index 100% rename from examples/debug-listener-timing.mjs rename to js/examples/debug-listener-timing.mjs diff --git a/examples/debug-listeners-property.mjs b/js/examples/debug-listeners-property.mjs similarity index 100% rename from examples/debug-listeners-property.mjs rename to js/examples/debug-listeners-property.mjs diff --git a/examples/debug-map-methods.mjs b/js/examples/debug-map-methods.mjs similarity index 100% rename from examples/debug-map-methods.mjs rename to js/examples/debug-map-methods.mjs diff --git a/examples/debug-not-awaited-cleanup.mjs b/js/examples/debug-not-awaited-cleanup.mjs similarity index 100% rename from examples/debug-not-awaited-cleanup.mjs rename to js/examples/debug-not-awaited-cleanup.mjs diff --git a/examples/debug-off-method.mjs b/js/examples/debug-off-method.mjs similarity index 100% rename from examples/debug-off-method.mjs rename to js/examples/debug-off-method.mjs diff --git a/examples/debug-option-merging.mjs b/js/examples/debug-option-merging.mjs similarity index 100% rename from examples/debug-option-merging.mjs rename to js/examples/debug-option-merging.mjs diff --git a/examples/debug-options.mjs b/js/examples/debug-options.mjs similarity index 100% rename from examples/debug-options.mjs rename to js/examples/debug-options.mjs diff --git a/examples/debug-output.mjs b/js/examples/debug-output.mjs similarity index 100% rename from examples/debug-output.mjs rename to js/examples/debug-output.mjs diff --git a/examples/debug-pattern-matching.mjs b/js/examples/debug-pattern-matching.mjs similarity index 100% rename from examples/debug-pattern-matching.mjs rename to js/examples/debug-pattern-matching.mjs diff --git a/examples/debug-pipeline-cat.mjs b/js/examples/debug-pipeline-cat.mjs similarity index 100% rename from examples/debug-pipeline-cat.mjs rename to js/examples/debug-pipeline-cat.mjs diff --git a/examples/debug-pipeline-cleanup.mjs b/js/examples/debug-pipeline-cleanup.mjs similarity index 100% rename from examples/debug-pipeline-cleanup.mjs rename to js/examples/debug-pipeline-cleanup.mjs diff --git a/examples/debug-pipeline-error-detailed.mjs b/js/examples/debug-pipeline-error-detailed.mjs similarity index 100% rename from examples/debug-pipeline-error-detailed.mjs rename to js/examples/debug-pipeline-error-detailed.mjs diff --git a/examples/debug-pipeline-error.mjs b/js/examples/debug-pipeline-error.mjs similarity index 100% rename from examples/debug-pipeline-error.mjs rename to js/examples/debug-pipeline-error.mjs diff --git a/examples/debug-pipeline-issue.mjs b/js/examples/debug-pipeline-issue.mjs similarity index 100% rename from examples/debug-pipeline-issue.mjs rename to js/examples/debug-pipeline-issue.mjs diff --git a/examples/debug-pipeline-method.mjs b/js/examples/debug-pipeline-method.mjs similarity index 100% rename from examples/debug-pipeline-method.mjs rename to js/examples/debug-pipeline-method.mjs diff --git a/examples/debug-pipeline-stream.mjs b/js/examples/debug-pipeline-stream.mjs similarity index 100% rename from examples/debug-pipeline-stream.mjs rename to js/examples/debug-pipeline-stream.mjs diff --git a/examples/debug-pipeline.mjs b/js/examples/debug-pipeline.mjs similarity index 100% rename from examples/debug-pipeline.mjs rename to js/examples/debug-pipeline.mjs diff --git a/examples/debug-process-exit-trace.mjs b/js/examples/debug-process-exit-trace.mjs similarity index 100% rename from examples/debug-process-exit-trace.mjs rename to js/examples/debug-process-exit-trace.mjs diff --git a/examples/debug-process-path.mjs b/js/examples/debug-process-path.mjs similarity index 100% rename from examples/debug-process-path.mjs rename to js/examples/debug-process-path.mjs diff --git a/examples/debug-property-check.mjs b/js/examples/debug-property-check.mjs similarity index 100% rename from examples/debug-property-check.mjs rename to js/examples/debug-property-check.mjs diff --git a/examples/debug-resource-cleanup.mjs b/js/examples/debug-resource-cleanup.mjs similarity index 100% rename from examples/debug-resource-cleanup.mjs rename to js/examples/debug-resource-cleanup.mjs diff --git a/examples/debug-shell-args.mjs b/js/examples/debug-shell-args.mjs similarity index 100% rename from examples/debug-shell-args.mjs rename to js/examples/debug-shell-args.mjs diff --git a/examples/debug-sigint-child-handler.mjs b/js/examples/debug-sigint-child-handler.mjs similarity index 100% rename from examples/debug-sigint-child-handler.mjs rename to js/examples/debug-sigint-child-handler.mjs diff --git a/examples/debug-sigint-forwarding.mjs b/js/examples/debug-sigint-forwarding.mjs similarity index 100% rename from examples/debug-sigint-forwarding.mjs rename to js/examples/debug-sigint-forwarding.mjs diff --git a/examples/debug-sigint-handler-install.mjs b/js/examples/debug-sigint-handler-install.mjs similarity index 100% rename from examples/debug-sigint-handler-install.mjs rename to js/examples/debug-sigint-handler-install.mjs diff --git a/examples/debug-sigint-handler-order.mjs b/js/examples/debug-sigint-handler-order.mjs similarity index 100% rename from examples/debug-sigint-handler-order.mjs rename to js/examples/debug-sigint-handler-order.mjs diff --git a/examples/debug-sigint-listeners.mjs b/js/examples/debug-sigint-listeners.mjs similarity index 100% rename from examples/debug-sigint-listeners.mjs rename to js/examples/debug-sigint-listeners.mjs diff --git a/examples/debug-sigint-start-pattern.mjs b/js/examples/debug-sigint-start-pattern.mjs similarity index 100% rename from examples/debug-sigint-start-pattern.mjs rename to js/examples/debug-sigint-start-pattern.mjs diff --git a/examples/debug-sigint-timer.mjs b/js/examples/debug-sigint-timer.mjs similarity index 100% rename from examples/debug-sigint-timer.mjs rename to js/examples/debug-sigint-timer.mjs diff --git a/examples/debug-simple-command.mjs b/js/examples/debug-simple-command.mjs similarity index 100% rename from examples/debug-simple-command.mjs rename to js/examples/debug-simple-command.mjs diff --git a/examples/debug-simple-getter.mjs b/js/examples/debug-simple-getter.mjs similarity index 100% rename from examples/debug-simple-getter.mjs rename to js/examples/debug-simple-getter.mjs diff --git a/examples/debug-simple.mjs b/js/examples/debug-simple.mjs similarity index 100% rename from examples/debug-simple.mjs rename to js/examples/debug-simple.mjs diff --git a/examples/debug-simplified-finished.mjs b/js/examples/debug-simplified-finished.mjs similarity index 100% rename from examples/debug-simplified-finished.mjs rename to js/examples/debug-simplified-finished.mjs diff --git a/examples/debug-stack-overflow.mjs b/js/examples/debug-stack-overflow.mjs similarity index 100% rename from examples/debug-stack-overflow.mjs rename to js/examples/debug-stack-overflow.mjs diff --git a/examples/debug-stdin-simple.mjs b/js/examples/debug-stdin-simple.mjs similarity index 100% rename from examples/debug-stdin-simple.mjs rename to js/examples/debug-stdin-simple.mjs diff --git a/examples/debug-stdin.mjs b/js/examples/debug-stdin.mjs similarity index 100% rename from examples/debug-stdin.mjs rename to js/examples/debug-stdin.mjs diff --git a/examples/debug-stream-emitter-isolated.mjs b/js/examples/debug-stream-emitter-isolated.mjs similarity index 100% rename from examples/debug-stream-emitter-isolated.mjs rename to js/examples/debug-stream-emitter-isolated.mjs diff --git a/examples/debug-stream-emitter.mjs b/js/examples/debug-stream-emitter.mjs similarity index 100% rename from examples/debug-stream-emitter.mjs rename to js/examples/debug-stream-emitter.mjs diff --git a/examples/debug-stream-events.mjs b/js/examples/debug-stream-events.mjs similarity index 100% rename from examples/debug-stream-events.mjs rename to js/examples/debug-stream-events.mjs diff --git a/examples/debug-stream-generator.mjs b/js/examples/debug-stream-generator.mjs similarity index 100% rename from examples/debug-stream-generator.mjs rename to js/examples/debug-stream-generator.mjs diff --git a/examples/debug-stream-getter-issue.mjs b/js/examples/debug-stream-getter-issue.mjs similarity index 100% rename from examples/debug-stream-getter-issue.mjs rename to js/examples/debug-stream-getter-issue.mjs diff --git a/examples/debug-stream-getter.mjs b/js/examples/debug-stream-getter.mjs similarity index 100% rename from examples/debug-stream-getter.mjs rename to js/examples/debug-stream-getter.mjs diff --git a/examples/debug-stream-internals.mjs b/js/examples/debug-stream-internals.mjs similarity index 100% rename from examples/debug-stream-internals.mjs rename to js/examples/debug-stream-internals.mjs diff --git a/examples/debug-stream-method.mjs b/js/examples/debug-stream-method.mjs similarity index 100% rename from examples/debug-stream-method.mjs rename to js/examples/debug-stream-method.mjs diff --git a/examples/debug-stream-object.mjs b/js/examples/debug-stream-object.mjs similarity index 100% rename from examples/debug-stream-object.mjs rename to js/examples/debug-stream-object.mjs diff --git a/examples/debug-stream-properties.mjs b/js/examples/debug-stream-properties.mjs similarity index 100% rename from examples/debug-stream-properties.mjs rename to js/examples/debug-stream-properties.mjs diff --git a/examples/debug-stream-timing.mjs b/js/examples/debug-stream-timing.mjs similarity index 100% rename from examples/debug-stream-timing.mjs rename to js/examples/debug-stream-timing.mjs diff --git a/examples/debug-streaming.mjs b/js/examples/debug-streaming.mjs similarity index 100% rename from examples/debug-streaming.mjs rename to js/examples/debug-streaming.mjs diff --git a/examples/debug-test-isolation.mjs b/js/examples/debug-test-isolation.mjs similarity index 100% rename from examples/debug-test-isolation.mjs rename to js/examples/debug-test-isolation.mjs diff --git a/examples/debug-test-state.mjs b/js/examples/debug-test-state.mjs similarity index 100% rename from examples/debug-test-state.mjs rename to js/examples/debug-test-state.mjs diff --git a/examples/debug-user-sigint.mjs b/js/examples/debug-user-sigint.mjs similarity index 100% rename from examples/debug-user-sigint.mjs rename to js/examples/debug-user-sigint.mjs diff --git a/examples/debug-virtual-disable.mjs b/js/examples/debug-virtual-disable.mjs similarity index 100% rename from examples/debug-virtual-disable.mjs rename to js/examples/debug-virtual-disable.mjs diff --git a/examples/debug-virtual-vs-real.mjs b/js/examples/debug-virtual-vs-real.mjs similarity index 100% rename from examples/debug-virtual-vs-real.mjs rename to js/examples/debug-virtual-vs-real.mjs diff --git a/examples/debug-with-trace.mjs b/js/examples/debug-with-trace.mjs similarity index 100% rename from examples/debug-with-trace.mjs rename to js/examples/debug-with-trace.mjs diff --git a/examples/debug_parent_stream.mjs b/js/examples/debug_parent_stream.mjs similarity index 100% rename from examples/debug_parent_stream.mjs rename to js/examples/debug_parent_stream.mjs diff --git a/examples/emulate-claude-stream.mjs b/js/examples/emulate-claude-stream.mjs similarity index 100% rename from examples/emulate-claude-stream.mjs rename to js/examples/emulate-claude-stream.mjs diff --git a/examples/emulated-streaming-direct.mjs b/js/examples/emulated-streaming-direct.mjs similarity index 83% rename from examples/emulated-streaming-direct.mjs rename to js/examples/emulated-streaming-direct.mjs index bd77c1a..ee15ce9 100755 --- a/examples/emulated-streaming-direct.mjs +++ b/js/examples/emulated-streaming-direct.mjs @@ -7,7 +7,7 @@ import { $ } from '../src/$.mjs'; console.log('Direct execution of emulator:'); const start = Date.now(); -for await (const chunk of $`bun run examples/emulate-claude-stream.mjs`.stream()) { +for await (const chunk of $`bun run js/examples/emulate-claude-stream.mjs`.stream()) { if (chunk.type === 'stdout') { const elapsed = Date.now() - start; const lines = chunk.data diff --git a/examples/emulated-streaming-jq-pipe.mjs b/js/examples/emulated-streaming-jq-pipe.mjs similarity index 81% rename from examples/emulated-streaming-jq-pipe.mjs rename to js/examples/emulated-streaming-jq-pipe.mjs index 27ee877..8beb849 100755 --- a/examples/emulated-streaming-jq-pipe.mjs +++ b/js/examples/emulated-streaming-jq-pipe.mjs @@ -7,7 +7,7 @@ import { $ } from '../src/$.mjs'; console.log('Emulator piped through jq:'); const start = Date.now(); -for await (const chunk of $`bun run examples/emulate-claude-stream.mjs | jq .`.stream()) { +for await (const chunk of $`bun run js/examples/emulate-claude-stream.mjs | jq .`.stream()) { if (chunk.type === 'stdout') { const elapsed = Date.now() - start; const lines = chunk.data diff --git a/examples/emulated-streaming-sh-pipe.mjs b/js/examples/emulated-streaming-sh-pipe.mjs similarity index 85% rename from examples/emulated-streaming-sh-pipe.mjs rename to js/examples/emulated-streaming-sh-pipe.mjs index 6f10dae..ee64b6a 100755 --- a/examples/emulated-streaming-sh-pipe.mjs +++ b/js/examples/emulated-streaming-sh-pipe.mjs @@ -7,7 +7,7 @@ import { $ } from '../src/$.mjs'; console.log('Using sh -c with pipe:'); const start = Date.now(); -const cmd = $`sh -c 'bun run examples/emulate-claude-stream.mjs | jq .'`; +const cmd = $`sh -c 'bun run js/examples/emulate-claude-stream.mjs | jq .'`; for await (const chunk of cmd.stream()) { if (chunk.type === 'stdout') { const elapsed = Date.now() - start; diff --git a/examples/events-build-process.mjs b/js/examples/events-build-process.mjs similarity index 100% rename from examples/events-build-process.mjs rename to js/examples/events-build-process.mjs diff --git a/examples/events-concurrent-streams.mjs b/js/examples/events-concurrent-streams.mjs similarity index 100% rename from examples/events-concurrent-streams.mjs rename to js/examples/events-concurrent-streams.mjs diff --git a/examples/events-error-handling.mjs b/js/examples/events-error-handling.mjs similarity index 100% rename from examples/events-error-handling.mjs rename to js/examples/events-error-handling.mjs diff --git a/examples/events-file-monitoring.mjs b/js/examples/events-file-monitoring.mjs similarity index 100% rename from examples/events-file-monitoring.mjs rename to js/examples/events-file-monitoring.mjs diff --git a/examples/events-interactive-simulation.mjs b/js/examples/events-interactive-simulation.mjs similarity index 100% rename from examples/events-interactive-simulation.mjs rename to js/examples/events-interactive-simulation.mjs diff --git a/examples/events-log-processing.mjs b/js/examples/events-log-processing.mjs similarity index 100% rename from examples/events-log-processing.mjs rename to js/examples/events-log-processing.mjs diff --git a/examples/events-network-monitoring.mjs b/js/examples/events-network-monitoring.mjs similarity index 100% rename from examples/events-network-monitoring.mjs rename to js/examples/events-network-monitoring.mjs diff --git a/examples/events-ping-basic.mjs b/js/examples/events-ping-basic.mjs similarity index 100% rename from examples/events-ping-basic.mjs rename to js/examples/events-ping-basic.mjs diff --git a/examples/events-progress-tracking.mjs b/js/examples/events-progress-tracking.mjs similarity index 100% rename from examples/events-progress-tracking.mjs rename to js/examples/events-progress-tracking.mjs diff --git a/examples/events-stdin-input.mjs b/js/examples/events-stdin-input.mjs similarity index 100% rename from examples/events-stdin-input.mjs rename to js/examples/events-stdin-input.mjs diff --git a/examples/example-ansi-ls.mjs b/js/examples/example-ansi-ls.mjs similarity index 100% rename from examples/example-ansi-ls.mjs rename to js/examples/example-ansi-ls.mjs diff --git a/examples/example-top.mjs b/js/examples/example-top.mjs similarity index 100% rename from examples/example-top.mjs rename to js/examples/example-top.mjs diff --git a/examples/final-ping-stdin-proof.mjs b/js/examples/final-ping-stdin-proof.mjs similarity index 100% rename from examples/final-ping-stdin-proof.mjs rename to js/examples/final-ping-stdin-proof.mjs diff --git a/examples/final-test-shell-operators.mjs b/js/examples/final-test-shell-operators.mjs similarity index 100% rename from examples/final-test-shell-operators.mjs rename to js/examples/final-test-shell-operators.mjs diff --git a/examples/final-working-examples.mjs b/js/examples/final-working-examples.mjs similarity index 100% rename from examples/final-working-examples.mjs rename to js/examples/final-working-examples.mjs diff --git a/examples/gh-auth-test.mjs b/js/examples/gh-auth-test.mjs similarity index 100% rename from examples/gh-auth-test.mjs rename to js/examples/gh-auth-test.mjs diff --git a/examples/gh-delete-hang-test.mjs b/js/examples/gh-delete-hang-test.mjs similarity index 100% rename from examples/gh-delete-hang-test.mjs rename to js/examples/gh-delete-hang-test.mjs diff --git a/examples/gh-gist-creation-test.mjs b/js/examples/gh-gist-creation-test.mjs similarity index 100% rename from examples/gh-gist-creation-test.mjs rename to js/examples/gh-gist-creation-test.mjs diff --git a/examples/gh-gist-minimal-test.mjs b/js/examples/gh-gist-minimal-test.mjs similarity index 100% rename from examples/gh-gist-minimal-test.mjs rename to js/examples/gh-gist-minimal-test.mjs diff --git a/examples/gh-hang-exact-original.mjs b/js/examples/gh-hang-exact-original.mjs similarity index 100% rename from examples/gh-hang-exact-original.mjs rename to js/examples/gh-hang-exact-original.mjs diff --git a/examples/gh-hang-reproduction.mjs b/js/examples/gh-hang-reproduction.mjs similarity index 100% rename from examples/gh-hang-reproduction.mjs rename to js/examples/gh-hang-reproduction.mjs diff --git a/examples/gh-hang-test-with-redirect.mjs b/js/examples/gh-hang-test-with-redirect.mjs similarity index 100% rename from examples/gh-hang-test-with-redirect.mjs rename to js/examples/gh-hang-test-with-redirect.mjs diff --git a/examples/gh-hang-test-without-redirect.mjs b/js/examples/gh-hang-test-without-redirect.mjs similarity index 100% rename from examples/gh-hang-test-without-redirect.mjs rename to js/examples/gh-hang-test-without-redirect.mjs diff --git a/examples/gh-minimal-hang-check.mjs b/js/examples/gh-minimal-hang-check.mjs similarity index 100% rename from examples/gh-minimal-hang-check.mjs rename to js/examples/gh-minimal-hang-check.mjs diff --git a/examples/gh-operations-with-cd.mjs b/js/examples/gh-operations-with-cd.mjs similarity index 100% rename from examples/gh-operations-with-cd.mjs rename to js/examples/gh-operations-with-cd.mjs diff --git a/examples/gh-output-test.mjs b/js/examples/gh-output-test.mjs similarity index 100% rename from examples/gh-output-test.mjs rename to js/examples/gh-output-test.mjs diff --git a/examples/gh-reproduce-hang.mjs b/js/examples/gh-reproduce-hang.mjs similarity index 100% rename from examples/gh-reproduce-hang.mjs rename to js/examples/gh-reproduce-hang.mjs diff --git a/examples/git-operations-with-cd.mjs b/js/examples/git-operations-with-cd.mjs similarity index 100% rename from examples/git-operations-with-cd.mjs rename to js/examples/git-operations-with-cd.mjs diff --git a/examples/interactive-math-calc.mjs b/js/examples/interactive-math-calc.mjs similarity index 100% rename from examples/interactive-math-calc.mjs rename to js/examples/interactive-math-calc.mjs diff --git a/examples/interactive-top-fixed.mjs b/js/examples/interactive-top-fixed.mjs similarity index 100% rename from examples/interactive-top-fixed.mjs rename to js/examples/interactive-top-fixed.mjs diff --git a/examples/interactive-top-improved.mjs b/js/examples/interactive-top-improved.mjs similarity index 100% rename from examples/interactive-top-improved.mjs rename to js/examples/interactive-top-improved.mjs diff --git a/examples/interactive-top-pty-logging.mjs b/js/examples/interactive-top-pty-logging.mjs similarity index 100% rename from examples/interactive-top-pty-logging.mjs rename to js/examples/interactive-top-pty-logging.mjs diff --git a/examples/interactive-top-pty.mjs b/js/examples/interactive-top-pty.mjs similarity index 100% rename from examples/interactive-top-pty.mjs rename to js/examples/interactive-top-pty.mjs diff --git a/examples/interactive-top-with-logging.mjs b/js/examples/interactive-top-with-logging.mjs similarity index 100% rename from examples/interactive-top-with-logging.mjs rename to js/examples/interactive-top-with-logging.mjs diff --git a/examples/interactive-top.mjs b/js/examples/interactive-top.mjs similarity index 100% rename from examples/interactive-top.mjs rename to js/examples/interactive-top.mjs diff --git a/examples/jq-color-demo.mjs b/js/examples/jq-color-demo.mjs similarity index 100% rename from examples/jq-color-demo.mjs rename to js/examples/jq-color-demo.mjs diff --git a/examples/jq-colors-streaming.mjs b/js/examples/jq-colors-streaming.mjs similarity index 100% rename from examples/jq-colors-streaming.mjs rename to js/examples/jq-colors-streaming.mjs diff --git a/examples/manual-ctrl-c-test.mjs b/js/examples/manual-ctrl-c-test.mjs similarity index 100% rename from examples/manual-ctrl-c-test.mjs rename to js/examples/manual-ctrl-c-test.mjs diff --git a/examples/methods-multiple-options.mjs b/js/examples/methods-multiple-options.mjs similarity index 100% rename from examples/methods-multiple-options.mjs rename to js/examples/methods-multiple-options.mjs diff --git a/examples/methods-run-basic.mjs b/js/examples/methods-run-basic.mjs similarity index 100% rename from examples/methods-run-basic.mjs rename to js/examples/methods-run-basic.mjs diff --git a/examples/methods-start-basic.mjs b/js/examples/methods-start-basic.mjs similarity index 100% rename from examples/methods-start-basic.mjs rename to js/examples/methods-start-basic.mjs diff --git a/examples/node-compat-data-events.mjs b/js/examples/node-compat-data-events.mjs similarity index 90% rename from examples/node-compat-data-events.mjs rename to js/examples/node-compat-data-events.mjs index 1458eea..b3f17a9 100755 --- a/examples/node-compat-data-events.mjs +++ b/js/examples/node-compat-data-events.mjs @@ -9,7 +9,7 @@ console.log('Node.js spawn with on("data") events:'); const start = Date.now(); let chunkCount = 0; -const proc1 = spawn('bun', ['run', 'examples/emulate-claude-stream.mjs']); +const proc1 = spawn('bun', ['run', 'js/examples/emulate-claude-stream.mjs']); const proc2 = spawn('jq', ['.'], { stdio: ['pipe', 'pipe', 'pipe'], }); diff --git a/examples/node-compat-readable-event.mjs b/js/examples/node-compat-readable-event.mjs similarity index 88% rename from examples/node-compat-readable-event.mjs rename to js/examples/node-compat-readable-event.mjs index 075bedf..747999a 100755 --- a/examples/node-compat-readable-event.mjs +++ b/js/examples/node-compat-readable-event.mjs @@ -9,7 +9,7 @@ console.log('Using readable event:'); const start = Date.now(); let chunkCount = 0; -const proc1 = spawn('bun', ['run', 'examples/emulate-claude-stream.mjs']); +const proc1 = spawn('bun', ['run', 'js/examples/emulate-claude-stream.mjs']); const proc2 = spawn('jq', ['.'], { stdio: ['pipe', 'pipe', 'pipe'], }); diff --git a/examples/node-compat-small-buffer.mjs b/js/examples/node-compat-small-buffer.mjs similarity index 90% rename from examples/node-compat-small-buffer.mjs rename to js/examples/node-compat-small-buffer.mjs index 204ad5d..53fb931 100755 --- a/examples/node-compat-small-buffer.mjs +++ b/js/examples/node-compat-small-buffer.mjs @@ -9,7 +9,7 @@ console.log('Reading with small buffer size:'); const start = Date.now(); let chunkCount = 0; -const proc1 = spawn('bun', ['run', 'examples/emulate-claude-stream.mjs']); +const proc1 = spawn('bun', ['run', 'js/examples/emulate-claude-stream.mjs']); const proc2 = spawn('jq', ['.'], { stdio: ['pipe', 'pipe', 'pipe'], }); diff --git a/examples/options-capture-false.mjs b/js/examples/options-capture-false.mjs similarity index 100% rename from examples/options-capture-false.mjs rename to js/examples/options-capture-false.mjs diff --git a/examples/options-combined-settings.mjs b/js/examples/options-combined-settings.mjs similarity index 100% rename from examples/options-combined-settings.mjs rename to js/examples/options-combined-settings.mjs diff --git a/examples/options-custom-input.mjs b/js/examples/options-custom-input.mjs similarity index 100% rename from examples/options-custom-input.mjs rename to js/examples/options-custom-input.mjs diff --git a/examples/options-default-behavior.mjs b/js/examples/options-default-behavior.mjs similarity index 100% rename from examples/options-default-behavior.mjs rename to js/examples/options-default-behavior.mjs diff --git a/examples/options-maximum-performance.mjs b/js/examples/options-maximum-performance.mjs similarity index 100% rename from examples/options-maximum-performance.mjs rename to js/examples/options-maximum-performance.mjs diff --git a/examples/options-mirror-false.mjs b/js/examples/options-mirror-false.mjs similarity index 100% rename from examples/options-mirror-false.mjs rename to js/examples/options-mirror-false.mjs diff --git a/examples/options-performance-mode.mjs b/js/examples/options-performance-mode.mjs similarity index 100% rename from examples/options-performance-mode.mjs rename to js/examples/options-performance-mode.mjs diff --git a/examples/options-performance-optimization.mjs b/js/examples/options-performance-optimization.mjs similarity index 100% rename from examples/options-performance-optimization.mjs rename to js/examples/options-performance-optimization.mjs diff --git a/examples/options-run-alias-demo.mjs b/js/examples/options-run-alias-demo.mjs similarity index 100% rename from examples/options-run-alias-demo.mjs rename to js/examples/options-run-alias-demo.mjs diff --git a/examples/options-run-alias.mjs b/js/examples/options-run-alias.mjs similarity index 100% rename from examples/options-run-alias.mjs rename to js/examples/options-run-alias.mjs diff --git a/examples/options-silent-execution.mjs b/js/examples/options-silent-execution.mjs similarity index 100% rename from examples/options-silent-execution.mjs rename to js/examples/options-silent-execution.mjs diff --git a/examples/options-streaming-capture.mjs b/js/examples/options-streaming-capture.mjs similarity index 100% rename from examples/options-streaming-capture.mjs rename to js/examples/options-streaming-capture.mjs diff --git a/examples/options-streaming-multiple.mjs b/js/examples/options-streaming-multiple.mjs similarity index 100% rename from examples/options-streaming-multiple.mjs rename to js/examples/options-streaming-multiple.mjs diff --git a/examples/options-streaming-silent.mjs b/js/examples/options-streaming-silent.mjs similarity index 100% rename from examples/options-streaming-silent.mjs rename to js/examples/options-streaming-silent.mjs diff --git a/examples/options-streaming-stdin.mjs b/js/examples/options-streaming-stdin.mjs similarity index 100% rename from examples/options-streaming-stdin.mjs rename to js/examples/options-streaming-stdin.mjs diff --git a/examples/ping-streaming-filtered.mjs b/js/examples/ping-streaming-filtered.mjs similarity index 100% rename from examples/ping-streaming-filtered.mjs rename to js/examples/ping-streaming-filtered.mjs diff --git a/examples/ping-streaming-interruptible.mjs b/js/examples/ping-streaming-interruptible.mjs similarity index 100% rename from examples/ping-streaming-interruptible.mjs rename to js/examples/ping-streaming-interruptible.mjs diff --git a/examples/ping-streaming-silent.mjs b/js/examples/ping-streaming-silent.mjs similarity index 100% rename from examples/ping-streaming-silent.mjs rename to js/examples/ping-streaming-silent.mjs diff --git a/examples/ping-streaming-simple.mjs b/js/examples/ping-streaming-simple.mjs similarity index 100% rename from examples/ping-streaming-simple.mjs rename to js/examples/ping-streaming-simple.mjs diff --git a/examples/ping-streaming-statistics.mjs b/js/examples/ping-streaming-statistics.mjs similarity index 100% rename from examples/ping-streaming-statistics.mjs rename to js/examples/ping-streaming-statistics.mjs diff --git a/examples/ping-streaming-timestamps.mjs b/js/examples/ping-streaming-timestamps.mjs similarity index 100% rename from examples/ping-streaming-timestamps.mjs rename to js/examples/ping-streaming-timestamps.mjs diff --git a/examples/ping-streaming.mjs b/js/examples/ping-streaming.mjs similarity index 100% rename from examples/ping-streaming.mjs rename to js/examples/ping-streaming.mjs diff --git a/examples/prove-ping-stdin-limitation.mjs b/js/examples/prove-ping-stdin-limitation.mjs similarity index 100% rename from examples/prove-ping-stdin-limitation.mjs rename to js/examples/prove-ping-stdin-limitation.mjs diff --git a/examples/readme-example.mjs b/js/examples/readme-example.mjs similarity index 100% rename from examples/readme-example.mjs rename to js/examples/readme-example.mjs diff --git a/examples/realtime-json-stream.mjs b/js/examples/realtime-json-stream.mjs similarity index 100% rename from examples/realtime-json-stream.mjs rename to js/examples/realtime-json-stream.mjs diff --git a/examples/reliable-stdin-commands.mjs b/js/examples/reliable-stdin-commands.mjs similarity index 100% rename from examples/reliable-stdin-commands.mjs rename to js/examples/reliable-stdin-commands.mjs diff --git a/examples/reproduce-issue-135-v2.mjs b/js/examples/reproduce-issue-135-v2.mjs similarity index 100% rename from examples/reproduce-issue-135-v2.mjs rename to js/examples/reproduce-issue-135-v2.mjs diff --git a/examples/reproduce-issue-135.mjs b/js/examples/reproduce-issue-135.mjs similarity index 100% rename from examples/reproduce-issue-135.mjs rename to js/examples/reproduce-issue-135.mjs diff --git a/examples/shell-cd-behavior.mjs b/js/examples/shell-cd-behavior.mjs similarity index 100% rename from examples/shell-cd-behavior.mjs rename to js/examples/shell-cd-behavior.mjs diff --git a/examples/sigint-forwarding-test.mjs b/js/examples/sigint-forwarding-test.mjs similarity index 100% rename from examples/sigint-forwarding-test.mjs rename to js/examples/sigint-forwarding-test.mjs diff --git a/examples/sigint-handler-test.mjs b/js/examples/sigint-handler-test.mjs similarity index 100% rename from examples/sigint-handler-test.mjs rename to js/examples/sigint-handler-test.mjs diff --git a/examples/simple-async-test.mjs b/js/examples/simple-async-test.mjs similarity index 100% rename from examples/simple-async-test.mjs rename to js/examples/simple-async-test.mjs diff --git a/examples/simple-claude-test.mjs b/js/examples/simple-claude-test.mjs similarity index 100% rename from examples/simple-claude-test.mjs rename to js/examples/simple-claude-test.mjs diff --git a/examples/simple-event-test.mjs b/js/examples/simple-event-test.mjs similarity index 100% rename from examples/simple-event-test.mjs rename to js/examples/simple-event-test.mjs diff --git a/examples/simple-jq-streaming.mjs b/js/examples/simple-jq-streaming.mjs similarity index 100% rename from examples/simple-jq-streaming.mjs rename to js/examples/simple-jq-streaming.mjs diff --git a/examples/simple-stream-demo.mjs b/js/examples/simple-stream-demo.mjs similarity index 100% rename from examples/simple-stream-demo.mjs rename to js/examples/simple-stream-demo.mjs diff --git a/examples/simple-test-sleep.js b/js/examples/simple-test-sleep.js similarity index 100% rename from examples/simple-test-sleep.js rename to js/examples/simple-test-sleep.js diff --git a/examples/simple-working-stdin.mjs b/js/examples/simple-working-stdin.mjs similarity index 100% rename from examples/simple-working-stdin.mjs rename to js/examples/simple-working-stdin.mjs diff --git a/examples/streaming-behavior-test.mjs b/js/examples/streaming-behavior-test.mjs similarity index 100% rename from examples/streaming-behavior-test.mjs rename to js/examples/streaming-behavior-test.mjs diff --git a/examples/streaming-direct-command.mjs b/js/examples/streaming-direct-command.mjs similarity index 84% rename from examples/streaming-direct-command.mjs rename to js/examples/streaming-direct-command.mjs index e8c86bd..bbac30d 100755 --- a/examples/streaming-direct-command.mjs +++ b/js/examples/streaming-direct-command.mjs @@ -8,7 +8,7 @@ console.log('Direct command streaming test:'); const start = Date.now(); let chunkCount = 0; -for await (const chunk of $`bun run examples/emulate-claude-stream.mjs`.stream()) { +for await (const chunk of $`bun run js/examples/emulate-claude-stream.mjs`.stream()) { if (chunk.type === 'stdout') { chunkCount++; const elapsed = Date.now() - start; diff --git a/examples/streaming-filtered-output.mjs b/js/examples/streaming-filtered-output.mjs similarity index 100% rename from examples/streaming-filtered-output.mjs rename to js/examples/streaming-filtered-output.mjs diff --git a/examples/streaming-grep-pipeline.mjs b/js/examples/streaming-grep-pipeline.mjs similarity index 82% rename from examples/streaming-grep-pipeline.mjs rename to js/examples/streaming-grep-pipeline.mjs index bec8807..9a9fe01 100755 --- a/examples/streaming-grep-pipeline.mjs +++ b/js/examples/streaming-grep-pipeline.mjs @@ -8,7 +8,7 @@ console.log('grep pipeline streaming test:'); const start = Date.now(); let chunkCount = 0; -for await (const chunk of $`bun run examples/emulate-claude-stream.mjs | grep -E '"type"'`.stream()) { +for await (const chunk of $`bun run js/examples/emulate-claude-stream.mjs | grep -E '"type"'`.stream()) { if (chunk.type === 'stdout') { chunkCount++; const elapsed = Date.now() - start; diff --git a/examples/streaming-interactive-stdin.mjs b/js/examples/streaming-interactive-stdin.mjs similarity index 100% rename from examples/streaming-interactive-stdin.mjs rename to js/examples/streaming-interactive-stdin.mjs diff --git a/examples/streaming-jq-pipeline.mjs b/js/examples/streaming-jq-pipeline.mjs similarity index 84% rename from examples/streaming-jq-pipeline.mjs rename to js/examples/streaming-jq-pipeline.mjs index 6a0207c..d52a494 100755 --- a/examples/streaming-jq-pipeline.mjs +++ b/js/examples/streaming-jq-pipeline.mjs @@ -8,7 +8,7 @@ console.log('jq pipeline streaming test:'); const start = Date.now(); let chunkCount = 0; -for await (const chunk of $`bun run examples/emulate-claude-stream.mjs | jq .`.stream()) { +for await (const chunk of $`bun run js/examples/emulate-claude-stream.mjs | jq .`.stream()) { if (chunk.type === 'stdout') { chunkCount++; const elapsed = Date.now() - start; diff --git a/examples/streaming-multistage-pipeline.mjs b/js/examples/streaming-multistage-pipeline.mjs similarity index 84% rename from examples/streaming-multistage-pipeline.mjs rename to js/examples/streaming-multistage-pipeline.mjs index 7294fdd..52490ab 100755 --- a/examples/streaming-multistage-pipeline.mjs +++ b/js/examples/streaming-multistage-pipeline.mjs @@ -8,7 +8,7 @@ console.log('Multi-stage pipeline streaming test:'); const start = Date.now(); let chunkCount = 0; -for await (const chunk of $`bun run examples/emulate-claude-stream.mjs | cat | jq .`.stream()) { +for await (const chunk of $`bun run js/examples/emulate-claude-stream.mjs | cat | jq .`.stream()) { if (chunk.type === 'stdout') { chunkCount++; const elapsed = Date.now() - start; diff --git a/examples/streaming-pipes-event-pattern.mjs b/js/examples/streaming-pipes-event-pattern.mjs similarity index 100% rename from examples/streaming-pipes-event-pattern.mjs rename to js/examples/streaming-pipes-event-pattern.mjs diff --git a/examples/streaming-pipes-multistage.mjs b/js/examples/streaming-pipes-multistage.mjs similarity index 100% rename from examples/streaming-pipes-multistage.mjs rename to js/examples/streaming-pipes-multistage.mjs diff --git a/examples/streaming-pipes-realtime-jq.mjs b/js/examples/streaming-pipes-realtime-jq.mjs similarity index 100% rename from examples/streaming-pipes-realtime-jq.mjs rename to js/examples/streaming-pipes-realtime-jq.mjs diff --git a/examples/streaming-progress-tracking.mjs b/js/examples/streaming-progress-tracking.mjs similarity index 100% rename from examples/streaming-progress-tracking.mjs rename to js/examples/streaming-progress-tracking.mjs diff --git a/examples/streaming-reusable-configs.mjs b/js/examples/streaming-reusable-configs.mjs similarity index 100% rename from examples/streaming-reusable-configs.mjs rename to js/examples/streaming-reusable-configs.mjs diff --git a/examples/streaming-silent-capture.mjs b/js/examples/streaming-silent-capture.mjs similarity index 100% rename from examples/streaming-silent-capture.mjs rename to js/examples/streaming-silent-capture.mjs diff --git a/examples/streaming-test-simple.mjs b/js/examples/streaming-test-simple.mjs similarity index 100% rename from examples/streaming-test-simple.mjs rename to js/examples/streaming-test-simple.mjs diff --git a/examples/streaming-virtual-pipeline.mjs b/js/examples/streaming-virtual-pipeline.mjs similarity index 100% rename from examples/streaming-virtual-pipeline.mjs rename to js/examples/streaming-virtual-pipeline.mjs diff --git a/examples/syntax-basic-comparison.mjs b/js/examples/syntax-basic-comparison.mjs similarity index 100% rename from examples/syntax-basic-comparison.mjs rename to js/examples/syntax-basic-comparison.mjs diff --git a/examples/syntax-basic-options.mjs b/js/examples/syntax-basic-options.mjs similarity index 100% rename from examples/syntax-basic-options.mjs rename to js/examples/syntax-basic-options.mjs diff --git a/examples/syntax-combined-options.mjs b/js/examples/syntax-combined-options.mjs similarity index 100% rename from examples/syntax-combined-options.mjs rename to js/examples/syntax-combined-options.mjs diff --git a/examples/syntax-command-chaining.mjs b/js/examples/syntax-command-chaining.mjs similarity index 100% rename from examples/syntax-command-chaining.mjs rename to js/examples/syntax-command-chaining.mjs diff --git a/examples/syntax-custom-directory.mjs b/js/examples/syntax-custom-directory.mjs similarity index 100% rename from examples/syntax-custom-directory.mjs rename to js/examples/syntax-custom-directory.mjs diff --git a/examples/syntax-custom-environment.mjs b/js/examples/syntax-custom-environment.mjs similarity index 100% rename from examples/syntax-custom-environment.mjs rename to js/examples/syntax-custom-environment.mjs diff --git a/examples/syntax-custom-stdin.mjs b/js/examples/syntax-custom-stdin.mjs similarity index 100% rename from examples/syntax-custom-stdin.mjs rename to js/examples/syntax-custom-stdin.mjs diff --git a/examples/syntax-mixed-regular.mjs b/js/examples/syntax-mixed-regular.mjs similarity index 100% rename from examples/syntax-mixed-regular.mjs rename to js/examples/syntax-mixed-regular.mjs diff --git a/examples/syntax-mixed-usage.mjs b/js/examples/syntax-mixed-usage.mjs similarity index 100% rename from examples/syntax-mixed-usage.mjs rename to js/examples/syntax-mixed-usage.mjs diff --git a/examples/syntax-multiple-listeners.mjs b/js/examples/syntax-multiple-listeners.mjs similarity index 100% rename from examples/syntax-multiple-listeners.mjs rename to js/examples/syntax-multiple-listeners.mjs diff --git a/examples/syntax-piping-comparison.mjs b/js/examples/syntax-piping-comparison.mjs similarity index 100% rename from examples/syntax-piping-comparison.mjs rename to js/examples/syntax-piping-comparison.mjs diff --git a/examples/syntax-reusable-config.mjs b/js/examples/syntax-reusable-config.mjs similarity index 100% rename from examples/syntax-reusable-config.mjs rename to js/examples/syntax-reusable-config.mjs diff --git a/examples/syntax-reusable-configs.mjs b/js/examples/syntax-reusable-configs.mjs similarity index 100% rename from examples/syntax-reusable-configs.mjs rename to js/examples/syntax-reusable-configs.mjs diff --git a/examples/syntax-silent-operations.mjs b/js/examples/syntax-silent-operations.mjs similarity index 100% rename from examples/syntax-silent-operations.mjs rename to js/examples/syntax-silent-operations.mjs diff --git a/examples/syntax-stdin-option.mjs b/js/examples/syntax-stdin-option.mjs similarity index 100% rename from examples/syntax-stdin-option.mjs rename to js/examples/syntax-stdin-option.mjs diff --git a/examples/temp-sigint-test.mjs b/js/examples/temp-sigint-test.mjs similarity index 100% rename from examples/temp-sigint-test.mjs rename to js/examples/temp-sigint-test.mjs diff --git a/examples/test-actual-buildshell.mjs b/js/examples/test-actual-buildshell.mjs similarity index 100% rename from examples/test-actual-buildshell.mjs rename to js/examples/test-actual-buildshell.mjs diff --git a/examples/test-async-streams-working.mjs b/js/examples/test-async-streams-working.mjs similarity index 100% rename from examples/test-async-streams-working.mjs rename to js/examples/test-async-streams-working.mjs diff --git a/examples/test-async-streams.mjs b/js/examples/test-async-streams.mjs similarity index 100% rename from examples/test-async-streams.mjs rename to js/examples/test-async-streams.mjs diff --git a/examples/test-auth-parse.mjs b/js/examples/test-auth-parse.mjs similarity index 98% rename from examples/test-auth-parse.mjs rename to js/examples/test-auth-parse.mjs index e38a75c..6311309 100644 --- a/examples/test-auth-parse.mjs +++ b/js/examples/test-auth-parse.mjs @@ -1,4 +1,4 @@ -import { $ } from './src/$.mjs'; +import { $ } from '../src/$.mjs'; async function getDetailedAuthStatus() { try { diff --git a/examples/test-auto-quoting.mjs b/js/examples/test-auto-quoting.mjs similarity index 100% rename from examples/test-auto-quoting.mjs rename to js/examples/test-auto-quoting.mjs diff --git a/examples/test-auto-start-fix.mjs b/js/examples/test-auto-start-fix.mjs similarity index 100% rename from examples/test-auto-start-fix.mjs rename to js/examples/test-auto-start-fix.mjs diff --git a/examples/test-baseline-sigint.mjs b/js/examples/test-baseline-sigint.mjs similarity index 100% rename from examples/test-baseline-sigint.mjs rename to js/examples/test-baseline-sigint.mjs diff --git a/examples/test-buffer-behavior.mjs b/js/examples/test-buffer-behavior.mjs similarity index 100% rename from examples/test-buffer-behavior.mjs rename to js/examples/test-buffer-behavior.mjs diff --git a/examples/test-buffers-simple.mjs b/js/examples/test-buffers-simple.mjs similarity index 100% rename from examples/test-buffers-simple.mjs rename to js/examples/test-buffers-simple.mjs diff --git a/examples/test-bun-specific-issue.mjs b/js/examples/test-bun-specific-issue.mjs similarity index 100% rename from examples/test-bun-specific-issue.mjs rename to js/examples/test-bun-specific-issue.mjs diff --git a/examples/test-bun-streaming.mjs b/js/examples/test-bun-streaming.mjs similarity index 92% rename from examples/test-bun-streaming.mjs rename to js/examples/test-bun-streaming.mjs index c095cfd..c5db5f7 100755 --- a/examples/test-bun-streaming.mjs +++ b/js/examples/test-bun-streaming.mjs @@ -22,7 +22,7 @@ for await (const chunk of proc1.stdout) { console.log('\nTest 2: Command with jq pipeline'); const proc2 = Bun.spawn( - ['sh', '-c', './examples/emulate-claude-stream.mjs | jq .'], + ['sh', '-c', './js/examples/emulate-claude-stream.mjs | jq .'], { stdout: 'pipe', stderr: 'pipe', @@ -47,7 +47,7 @@ for await (const chunk of proc2.stdout) { console.log('\nTest 3: Read stdout using different method'); const proc3 = Bun.spawn( - ['sh', '-c', './examples/emulate-claude-stream.mjs | jq .'], + ['sh', '-c', './js/examples/emulate-claude-stream.mjs | jq .'], { stdout: 'pipe', stderr: 'pipe', diff --git a/examples/test-cat-direct.mjs b/js/examples/test-cat-direct.mjs similarity index 84% rename from examples/test-cat-direct.mjs rename to js/examples/test-cat-direct.mjs index aed2787..d92e345 100755 --- a/examples/test-cat-direct.mjs +++ b/js/examples/test-cat-direct.mjs @@ -3,10 +3,13 @@ console.log('=== Test cat | jq directly ===\n'); // Direct Bun test -const proc1 = Bun.spawn(['bun', 'run', 'examples/emulate-claude-stream.mjs'], { - stdout: 'pipe', - stderr: 'pipe', -}); +const proc1 = Bun.spawn( + ['bun', 'run', 'js/examples/emulate-claude-stream.mjs'], + { + stdout: 'pipe', + stderr: 'pipe', + } +); const proc2 = Bun.spawn(['cat'], { stdin: proc1.stdout, diff --git a/examples/test-cat-pipe.mjs b/js/examples/test-cat-pipe.mjs similarity index 90% rename from examples/test-cat-pipe.mjs rename to js/examples/test-cat-pipe.mjs index e0ea6fe..14af4ba 100755 --- a/examples/test-cat-pipe.mjs +++ b/js/examples/test-cat-pipe.mjs @@ -5,7 +5,7 @@ console.log('Test: Piping through cat (should not buffer)\n'); // First process: emulator -const proc1 = Bun.spawn(['./examples/emulate-claude-stream.mjs'], { +const proc1 = Bun.spawn(['./js/examples/emulate-claude-stream.mjs'], { stdout: 'pipe', stderr: 'pipe', }); diff --git a/examples/test-cd-behavior.mjs b/js/examples/test-cd-behavior.mjs similarity index 100% rename from examples/test-cd-behavior.mjs rename to js/examples/test-cd-behavior.mjs diff --git a/examples/test-child-process-timing.mjs b/js/examples/test-child-process-timing.mjs similarity index 100% rename from examples/test-child-process-timing.mjs rename to js/examples/test-child-process-timing.mjs diff --git a/examples/test-child-sigint-handler.mjs b/js/examples/test-child-sigint-handler.mjs similarity index 100% rename from examples/test-child-sigint-handler.mjs rename to js/examples/test-child-sigint-handler.mjs diff --git a/examples/test-cleanup-simple.mjs b/js/examples/test-cleanup-simple.mjs similarity index 100% rename from examples/test-cleanup-simple.mjs rename to js/examples/test-cleanup-simple.mjs diff --git a/examples/test-comprehensive-tracing.mjs b/js/examples/test-comprehensive-tracing.mjs similarity index 74% rename from examples/test-comprehensive-tracing.mjs rename to js/examples/test-comprehensive-tracing.mjs index 64fa046..74cb6ef 100755 --- a/examples/test-comprehensive-tracing.mjs +++ b/js/examples/test-comprehensive-tracing.mjs @@ -15,7 +15,7 @@ * - trace-error-handling.mjs - Error conditions and cleanup * * Usage for any example: - * COMMAND_STREAM_TRACE=ProcessRunner node examples/trace-[example-name].mjs + * COMMAND_STREAM_TRACE=ProcessRunner node js/examples/trace-[example-name].mjs * * For CI debugging, enable tracing in your test runs: * COMMAND_STREAM_TRACE=ProcessRunner bun test @@ -36,22 +36,22 @@ console.log(''); console.log('Run individual examples to test specific areas:'); console.log(''); console.log( - 'COMMAND_STREAM_TRACE=ProcessRunner node examples/trace-simple-command.mjs' + 'COMMAND_STREAM_TRACE=ProcessRunner node js/examples/trace-simple-command.mjs' ); console.log( - 'COMMAND_STREAM_TRACE=ProcessRunner node examples/trace-signal-handling.mjs' + 'COMMAND_STREAM_TRACE=ProcessRunner node js/examples/trace-signal-handling.mjs' ); console.log( - 'COMMAND_STREAM_TRACE=ProcessRunner node examples/trace-abort-controller.mjs' + 'COMMAND_STREAM_TRACE=ProcessRunner node js/examples/trace-abort-controller.mjs' ); console.log( - 'COMMAND_STREAM_TRACE=ProcessRunner node examples/trace-stderr-output.mjs' + 'COMMAND_STREAM_TRACE=ProcessRunner node js/examples/trace-stderr-output.mjs' ); console.log( - 'COMMAND_STREAM_TRACE=ProcessRunner node examples/trace-pipeline-command.mjs' + 'COMMAND_STREAM_TRACE=ProcessRunner node js/examples/trace-pipeline-command.mjs' ); console.log( - 'COMMAND_STREAM_TRACE=ProcessRunner node examples/trace-error-handling.mjs' + 'COMMAND_STREAM_TRACE=ProcessRunner node js/examples/trace-error-handling.mjs' ); console.log(''); console.log('💡 Use COMMAND_STREAM_TRACE=* to see all tracing categories'); diff --git a/examples/test-correct-space-handling.mjs b/js/examples/test-correct-space-handling.mjs similarity index 100% rename from examples/test-correct-space-handling.mjs rename to js/examples/test-correct-space-handling.mjs diff --git a/examples/test-ctrl-c-debug.mjs b/js/examples/test-ctrl-c-debug.mjs similarity index 100% rename from examples/test-ctrl-c-debug.mjs rename to js/examples/test-ctrl-c-debug.mjs diff --git a/examples/test-ctrl-c-inherit.mjs b/js/examples/test-ctrl-c-inherit.mjs similarity index 100% rename from examples/test-ctrl-c-inherit.mjs rename to js/examples/test-ctrl-c-inherit.mjs diff --git a/examples/test-ctrl-c-sleep.mjs b/js/examples/test-ctrl-c-sleep.mjs similarity index 100% rename from examples/test-ctrl-c-sleep.mjs rename to js/examples/test-ctrl-c-sleep.mjs diff --git a/examples/test-ctrl-c.mjs b/js/examples/test-ctrl-c.mjs similarity index 100% rename from examples/test-ctrl-c.mjs rename to js/examples/test-ctrl-c.mjs diff --git a/examples/test-debug-new-options.mjs b/js/examples/test-debug-new-options.mjs similarity index 100% rename from examples/test-debug-new-options.mjs rename to js/examples/test-debug-new-options.mjs diff --git a/examples/test-debug-pty.mjs b/js/examples/test-debug-pty.mjs similarity index 89% rename from examples/test-debug-pty.mjs rename to js/examples/test-debug-pty.mjs index 4bd2b68..be90eea 100755 --- a/examples/test-debug-pty.mjs +++ b/js/examples/test-debug-pty.mjs @@ -30,7 +30,7 @@ console.log('Testing pipeline routing:\n'); // Test with actual file to avoid shell wrapper console.log('Test 1: Using actual file command:'); const start = Date.now(); -for await (const chunk of $`./examples/emulate-claude-stream.mjs | jq .`.stream()) { +for await (const chunk of $`./js/examples/emulate-claude-stream.mjs | jq .`.stream()) { if (chunk.type === 'stdout') { const elapsed = Date.now() - start; const lines = chunk.data.toString().trim().split('\n').slice(0, 2); @@ -40,7 +40,7 @@ for await (const chunk of $`./examples/emulate-claude-stream.mjs | jq .`.stream( console.log('\nTest 2: Using bun run:'); const start2 = Date.now(); -for await (const chunk of $`bun run examples/emulate-claude-stream.mjs | jq .`.stream()) { +for await (const chunk of $`bun run js/examples/emulate-claude-stream.mjs | jq .`.stream()) { if (chunk.type === 'stdout') { const elapsed = Date.now() - start2; const lines = chunk.data.toString().trim().split('\n').slice(0, 2); diff --git a/examples/test-debug-tee.mjs b/js/examples/test-debug-tee.mjs similarity index 93% rename from examples/test-debug-tee.mjs rename to js/examples/test-debug-tee.mjs index 5ab3af8..713ff33 100755 --- a/examples/test-debug-tee.mjs +++ b/js/examples/test-debug-tee.mjs @@ -5,7 +5,7 @@ import { $ } from '../src/$.mjs'; console.log('=== Debug Tee Streaming ===\n'); // Patch the emit function to trace calls -const cmd = $`bun run examples/emulate-claude-stream.mjs | cat | jq .`; +const cmd = $`bun run js/examples/emulate-claude-stream.mjs | cat | jq .`; const originalEmit = cmd.emit.bind(cmd); let emitCount = 0; diff --git a/examples/test-debug.mjs b/js/examples/test-debug.mjs similarity index 100% rename from examples/test-debug.mjs rename to js/examples/test-debug.mjs diff --git a/examples/test-direct-jq.mjs b/js/examples/test-direct-jq.mjs similarity index 93% rename from examples/test-direct-jq.mjs rename to js/examples/test-direct-jq.mjs index ac28cf0..0ba41e4 100755 --- a/examples/test-direct-jq.mjs +++ b/js/examples/test-direct-jq.mjs @@ -5,7 +5,7 @@ console.log('Test: Spawn processes directly and pipe them\n'); // First process: emulator -const proc1 = Bun.spawn(['./examples/emulate-claude-stream.mjs'], { +const proc1 = Bun.spawn(['./js/examples/emulate-claude-stream.mjs'], { stdout: 'pipe', stderr: 'pipe', }); diff --git a/examples/test-direct-pipe-reading.mjs b/js/examples/test-direct-pipe-reading.mjs similarity index 90% rename from examples/test-direct-pipe-reading.mjs rename to js/examples/test-direct-pipe-reading.mjs index 2db1964..61fea00 100755 --- a/examples/test-direct-pipe-reading.mjs +++ b/js/examples/test-direct-pipe-reading.mjs @@ -8,7 +8,7 @@ console.log('=== Testing Direct Pipe Reading Methods ===\n'); console.log('Method 1: Manual pipe connection with for await:'); { const proc1 = Bun.spawn( - ['bun', 'run', 'examples/emulate-claude-stream.mjs'], + ['bun', 'run', 'js/examples/emulate-claude-stream.mjs'], { stdout: 'pipe', stderr: 'pipe', @@ -39,7 +39,7 @@ console.log('Method 1: Manual pipe connection with for await:'); console.log('\nMethod 2: Read from first process while piped:'); { const proc1 = Bun.spawn( - ['bun', 'run', 'examples/emulate-claude-stream.mjs'], + ['bun', 'run', 'js/examples/emulate-claude-stream.mjs'], { stdout: 'pipe', stderr: 'pipe', @@ -64,7 +64,7 @@ console.log('\nMethod 2: Read from first process while piped:'); console.log('\nMethod 3: Using tee() to split stream:'); { const proc1 = Bun.spawn( - ['bun', 'run', 'examples/emulate-claude-stream.mjs'], + ['bun', 'run', 'js/examples/emulate-claude-stream.mjs'], { stdout: 'pipe', stderr: 'pipe', @@ -99,7 +99,7 @@ console.log('\nMethod 3: Using tee() to split stream:'); console.log('\nMethod 4: Full pipeline as single command:'); { const proc = Bun.spawn( - ['sh', '-c', 'bun run examples/emulate-claude-stream.mjs | jq .'], + ['sh', '-c', 'bun run js/examples/emulate-claude-stream.mjs | jq .'], { stdout: 'pipe', stderr: 'pipe', diff --git a/examples/test-direct-pipe.sh b/js/examples/test-direct-pipe.sh similarity index 100% rename from examples/test-direct-pipe.sh rename to js/examples/test-direct-pipe.sh diff --git a/examples/test-double-quoting-prevention.mjs b/js/examples/test-double-quoting-prevention.mjs similarity index 100% rename from examples/test-double-quoting-prevention.mjs rename to js/examples/test-double-quoting-prevention.mjs diff --git a/examples/test-edge-cases-quoting.mjs b/js/examples/test-edge-cases-quoting.mjs similarity index 100% rename from examples/test-edge-cases-quoting.mjs rename to js/examples/test-edge-cases-quoting.mjs diff --git a/examples/test-events.mjs b/js/examples/test-events.mjs similarity index 92% rename from examples/test-events.mjs rename to js/examples/test-events.mjs index 508ec66..fbb8452 100755 --- a/examples/test-events.mjs +++ b/js/examples/test-events.mjs @@ -5,7 +5,7 @@ import { $ } from '../src/$.mjs'; console.log('=== Test Event Emissions ===\n'); console.log('Test: emulate | cat | jq with event listeners'); -const cmd = $`bun run examples/emulate-claude-stream.mjs | cat | jq .`; +const cmd = $`bun run js/examples/emulate-claude-stream.mjs | cat | jq .`; const start = Date.now(); let eventCount = 0; diff --git a/examples/test-explicit-stdio.mjs b/js/examples/test-explicit-stdio.mjs similarity index 100% rename from examples/test-explicit-stdio.mjs rename to js/examples/test-explicit-stdio.mjs diff --git a/examples/test-final-streaming.mjs b/js/examples/test-final-streaming.mjs similarity index 85% rename from examples/test-final-streaming.mjs rename to js/examples/test-final-streaming.mjs index 6dbe956..88c36b1 100755 --- a/examples/test-final-streaming.mjs +++ b/js/examples/test-final-streaming.mjs @@ -7,7 +7,7 @@ console.log('=== Final Streaming Test with PTY Workaround ===\n'); console.log('Test 1: Direct emulator execution (baseline):'); { const start = Date.now(); - for await (const chunk of $`bun run examples/emulate-claude-stream.mjs`.stream()) { + for await (const chunk of $`bun run js/examples/emulate-claude-stream.mjs`.stream()) { if (chunk.type === 'stdout') { const elapsed = Date.now() - start; const lines = chunk.data @@ -29,7 +29,7 @@ console.log( const start = Date.now(); let chunkCount = 0; - for await (const chunk of $`bun run examples/emulate-claude-stream.mjs | jq .`.stream()) { + for await (const chunk of $`bun run js/examples/emulate-claude-stream.mjs | jq .`.stream()) { if (chunk.type === 'stdout') { chunkCount++; const elapsed = Date.now() - start; @@ -53,7 +53,7 @@ console.log('\nTest 3: Complex pipeline with jq -c:'); { const start = Date.now(); - for await (const chunk of $`bun run examples/emulate-claude-stream.mjs | jq -c .`.stream()) { + for await (const chunk of $`bun run js/examples/emulate-claude-stream.mjs | jq -c .`.stream()) { if (chunk.type === 'stdout') { const elapsed = Date.now() - start; const lines = chunk.data.toString().trim().split('\n'); diff --git a/examples/test-fix.mjs b/js/examples/test-fix.mjs similarity index 100% rename from examples/test-fix.mjs rename to js/examples/test-fix.mjs diff --git a/examples/test-incremental-streaming.mjs b/js/examples/test-incremental-streaming.mjs similarity index 100% rename from examples/test-incremental-streaming.mjs rename to js/examples/test-incremental-streaming.mjs diff --git a/examples/test-individual-spawn.mjs b/js/examples/test-individual-spawn.mjs similarity index 82% rename from examples/test-individual-spawn.mjs rename to js/examples/test-individual-spawn.mjs index e497f67..bc38dce 100755 --- a/examples/test-individual-spawn.mjs +++ b/js/examples/test-individual-spawn.mjs @@ -2,10 +2,13 @@ // Test spawning processes individually and connecting them -const proc1 = Bun.spawn(['bun', 'run', 'examples/emulate-claude-stream.mjs'], { - stdout: 'pipe', - stderr: 'pipe', -}); +const proc1 = Bun.spawn( + ['bun', 'run', 'js/examples/emulate-claude-stream.mjs'], + { + stdout: 'pipe', + stderr: 'pipe', + } +); const proc2 = Bun.spawn(['jq', '.'], { stdin: proc1.stdout, diff --git a/examples/test-inherit-stdout-not-stdin.mjs b/js/examples/test-inherit-stdout-not-stdin.mjs similarity index 100% rename from examples/test-inherit-stdout-not-stdin.mjs rename to js/examples/test-inherit-stdout-not-stdin.mjs diff --git a/examples/test-injection-protection.mjs b/js/examples/test-injection-protection.mjs similarity index 100% rename from examples/test-injection-protection.mjs rename to js/examples/test-injection-protection.mjs diff --git a/examples/test-interactive-streaming.mjs b/js/examples/test-interactive-streaming.mjs similarity index 100% rename from examples/test-interactive-streaming.mjs rename to js/examples/test-interactive-streaming.mjs diff --git a/examples/test-interactive-top.md b/js/examples/test-interactive-top.md similarity index 95% rename from examples/test-interactive-top.md rename to js/examples/test-interactive-top.md index 3a4f145..eb84b03 100644 --- a/examples/test-interactive-top.md +++ b/js/examples/test-interactive-top.md @@ -3,7 +3,7 @@ To test the interactive top example: ```bash -node examples/interactive-top.mjs +node js/examples/interactive-top.mjs ``` **Expected behavior:** diff --git a/examples/test-interactive.mjs b/js/examples/test-interactive.mjs similarity index 100% rename from examples/test-interactive.mjs rename to js/examples/test-interactive.mjs diff --git a/examples/test-interpolation.mjs b/js/examples/test-interpolation.mjs similarity index 100% rename from examples/test-interpolation.mjs rename to js/examples/test-interpolation.mjs diff --git a/examples/test-interrupt.mjs b/js/examples/test-interrupt.mjs similarity index 100% rename from examples/test-interrupt.mjs rename to js/examples/test-interrupt.mjs diff --git a/examples/test-issue-135-comprehensive.mjs b/js/examples/test-issue-135-comprehensive.mjs similarity index 100% rename from examples/test-issue-135-comprehensive.mjs rename to js/examples/test-issue-135-comprehensive.mjs diff --git a/examples/test-issue12-detailed.mjs b/js/examples/test-issue12-detailed.mjs similarity index 100% rename from examples/test-issue12-detailed.mjs rename to js/examples/test-issue12-detailed.mjs diff --git a/examples/test-issue12-exact.mjs b/js/examples/test-issue12-exact.mjs similarity index 100% rename from examples/test-issue12-exact.mjs rename to js/examples/test-issue12-exact.mjs diff --git a/examples/test-jq-color.mjs b/js/examples/test-jq-color.mjs similarity index 100% rename from examples/test-jq-color.mjs rename to js/examples/test-jq-color.mjs diff --git a/examples/test-jq-colors.mjs b/js/examples/test-jq-colors.mjs similarity index 100% rename from examples/test-jq-colors.mjs rename to js/examples/test-jq-colors.mjs diff --git a/examples/test-jq-compact.mjs b/js/examples/test-jq-compact.mjs similarity index 90% rename from examples/test-jq-compact.mjs rename to js/examples/test-jq-compact.mjs index 7ea3afb..c975254 100755 --- a/examples/test-jq-compact.mjs +++ b/js/examples/test-jq-compact.mjs @@ -4,7 +4,7 @@ console.log('Test: jq with -c (compact) flag\n'); -const proc1 = Bun.spawn(['./examples/emulate-claude-stream.mjs'], { +const proc1 = Bun.spawn(['./js/examples/emulate-claude-stream.mjs'], { stdout: 'pipe', stderr: 'pipe', }); diff --git a/examples/test-jq-native.sh b/js/examples/test-jq-native.sh similarity index 100% rename from examples/test-jq-native.sh rename to js/examples/test-jq-native.sh diff --git a/examples/test-jq-pipeline-behavior.mjs b/js/examples/test-jq-pipeline-behavior.mjs similarity index 100% rename from examples/test-jq-pipeline-behavior.mjs rename to js/examples/test-jq-pipeline-behavior.mjs diff --git a/examples/test-jq-realtime.mjs b/js/examples/test-jq-realtime.mjs similarity index 91% rename from examples/test-jq-realtime.mjs rename to js/examples/test-jq-realtime.mjs index cc06ab2..67e4ee8 100755 --- a/examples/test-jq-realtime.mjs +++ b/js/examples/test-jq-realtime.mjs @@ -9,7 +9,7 @@ const start = Date.now(); let lastTime = start; let chunkCount = 0; -for await (const chunk of $`bun run examples/emulate-claude-stream.mjs | jq .`.stream()) { +for await (const chunk of $`bun run js/examples/emulate-claude-stream.mjs | jq .`.stream()) { if (chunk.type === 'stdout') { chunkCount++; const now = Date.now(); diff --git a/examples/test-manual-start.mjs b/js/examples/test-manual-start.mjs similarity index 100% rename from examples/test-manual-start.mjs rename to js/examples/test-manual-start.mjs diff --git a/examples/test-mixed-quoting.mjs b/js/examples/test-mixed-quoting.mjs similarity index 100% rename from examples/test-mixed-quoting.mjs rename to js/examples/test-mixed-quoting.mjs diff --git a/examples/test-multi-stream.mjs b/js/examples/test-multi-stream.mjs similarity index 89% rename from examples/test-multi-stream.mjs rename to js/examples/test-multi-stream.mjs index 19f7cdf..5d9b8bf 100755 --- a/examples/test-multi-stream.mjs +++ b/js/examples/test-multi-stream.mjs @@ -4,10 +4,13 @@ console.log('=== Testing Multi-Stream Pipeline Reading ===\n'); -const proc1 = Bun.spawn(['bun', 'run', 'examples/emulate-claude-stream.mjs'], { - stdout: 'pipe', - stderr: 'pipe', -}); +const proc1 = Bun.spawn( + ['bun', 'run', 'js/examples/emulate-claude-stream.mjs'], + { + stdout: 'pipe', + stderr: 'pipe', + } +); // Use tee to split the stream so we can both pipe it and read it const [readStream, pipeStream] = proc1.stdout.tee(); diff --git a/examples/test-multistage-debug.mjs b/js/examples/test-multistage-debug.mjs similarity index 92% rename from examples/test-multistage-debug.mjs rename to js/examples/test-multistage-debug.mjs index ddba6d1..cb0d10c 100755 --- a/examples/test-multistage-debug.mjs +++ b/js/examples/test-multistage-debug.mjs @@ -10,7 +10,7 @@ let chunkCount = 0; let firstChunkTime = null; let lastChunkTime = null; -for await (const chunk of $`bun run examples/emulate-claude-stream.mjs | cat | jq .`.stream()) { +for await (const chunk of $`bun run js/examples/emulate-claude-stream.mjs | cat | jq .`.stream()) { if (chunk.type === 'stdout') { chunkCount++; const elapsed = Date.now() - start; diff --git a/examples/test-native-spawn-vs-command-stream.mjs b/js/examples/test-native-spawn-vs-command-stream.mjs similarity index 100% rename from examples/test-native-spawn-vs-command-stream.mjs rename to js/examples/test-native-spawn-vs-command-stream.mjs diff --git a/examples/test-no-parse-pipeline.mjs b/js/examples/test-no-parse-pipeline.mjs similarity index 91% rename from examples/test-no-parse-pipeline.mjs rename to js/examples/test-no-parse-pipeline.mjs index c54f1d0..69978af 100755 --- a/examples/test-no-parse-pipeline.mjs +++ b/js/examples/test-no-parse-pipeline.mjs @@ -14,7 +14,7 @@ $.prototype._parseCommand = function (command) { const start = Date.now(); // This will now be executed as a single sh command -const cmd = $`bun run examples/emulate-claude-stream.mjs | jq .`; +const cmd = $`bun run js/examples/emulate-claude-stream.mjs | jq .`; for await (const chunk of cmd.stream()) { if (chunk.type === 'stdout') { diff --git a/examples/test-non-virtual.mjs b/js/examples/test-non-virtual.mjs similarity index 100% rename from examples/test-non-virtual.mjs rename to js/examples/test-non-virtual.mjs diff --git a/examples/test-operators.mjs b/js/examples/test-operators.mjs similarity index 100% rename from examples/test-operators.mjs rename to js/examples/test-operators.mjs diff --git a/examples/test-parent-continues.mjs b/js/examples/test-parent-continues.mjs similarity index 100% rename from examples/test-parent-continues.mjs rename to js/examples/test-parent-continues.mjs diff --git a/examples/test-path-interpolation.mjs b/js/examples/test-path-interpolation.mjs similarity index 100% rename from examples/test-path-interpolation.mjs rename to js/examples/test-path-interpolation.mjs diff --git a/examples/test-ping-kill-and-stdin.mjs b/js/examples/test-ping-kill-and-stdin.mjs similarity index 100% rename from examples/test-ping-kill-and-stdin.mjs rename to js/examples/test-ping-kill-and-stdin.mjs diff --git a/examples/test-ping.mjs b/js/examples/test-ping.mjs similarity index 100% rename from examples/test-ping.mjs rename to js/examples/test-ping.mjs diff --git a/examples/test-pty-spawn.mjs b/js/examples/test-pty-spawn.mjs similarity index 94% rename from examples/test-pty-spawn.mjs rename to js/examples/test-pty-spawn.mjs index 83eaf42..5552bcc 100755 --- a/examples/test-pty-spawn.mjs +++ b/js/examples/test-pty-spawn.mjs @@ -27,7 +27,7 @@ console.log('\nUsing script command as PTY wrapper:'); '/dev/null', 'sh', '-c', - 'bun run examples/emulate-claude-stream.mjs | jq .', + 'bun run js/examples/emulate-claude-stream.mjs | jq .', ], { stdout: 'pipe', @@ -70,7 +70,7 @@ console.log('\nTrying with TTY environment variables:'); let chunkCount = 0; const proc = Bun.spawn( - ['sh', '-c', 'bun run examples/emulate-claude-stream.mjs | jq .'], + ['sh', '-c', 'bun run js/examples/emulate-claude-stream.mjs | jq .'], { stdout: 'pipe', stderr: 'pipe', diff --git a/examples/test-pty.mjs b/js/examples/test-pty.mjs similarity index 92% rename from examples/test-pty.mjs rename to js/examples/test-pty.mjs index ffaa99e..59af499 100755 --- a/examples/test-pty.mjs +++ b/js/examples/test-pty.mjs @@ -4,7 +4,7 @@ console.log('Test: Using unbuffer to force line buffering\n'); -const proc1 = Bun.spawn(['./examples/emulate-claude-stream.mjs'], { +const proc1 = Bun.spawn(['./js/examples/emulate-claude-stream.mjs'], { stdout: 'pipe', stderr: 'pipe', }); diff --git a/examples/test-quote-behavior-summary.mjs b/js/examples/test-quote-behavior-summary.mjs similarity index 100% rename from examples/test-quote-behavior-summary.mjs rename to js/examples/test-quote-behavior-summary.mjs diff --git a/examples/test-quote-edge-cases.mjs b/js/examples/test-quote-edge-cases.mjs similarity index 100% rename from examples/test-quote-edge-cases.mjs rename to js/examples/test-quote-edge-cases.mjs diff --git a/examples/test-quote-parsing.mjs b/js/examples/test-quote-parsing.mjs similarity index 100% rename from examples/test-quote-parsing.mjs rename to js/examples/test-quote-parsing.mjs diff --git a/examples/test-raw-function.mjs b/js/examples/test-raw-function.mjs similarity index 100% rename from examples/test-raw-function.mjs rename to js/examples/test-raw-function.mjs diff --git a/examples/test-raw-streaming.mjs b/js/examples/test-raw-streaming.mjs similarity index 100% rename from examples/test-raw-streaming.mjs rename to js/examples/test-raw-streaming.mjs diff --git a/examples/test-readme-examples.mjs b/js/examples/test-readme-examples.mjs similarity index 100% rename from examples/test-readme-examples.mjs rename to js/examples/test-readme-examples.mjs diff --git a/examples/test-real-cat.mjs b/js/examples/test-real-cat.mjs similarity index 100% rename from examples/test-real-cat.mjs rename to js/examples/test-real-cat.mjs diff --git a/examples/test-real-commands.mjs b/js/examples/test-real-commands.mjs similarity index 85% rename from examples/test-real-commands.mjs rename to js/examples/test-real-commands.mjs index f24c40f..6cab5d0 100755 --- a/examples/test-real-commands.mjs +++ b/js/examples/test-real-commands.mjs @@ -10,7 +10,7 @@ console.log('Result:', result.stdout); console.log('\nTest streaming:'); const start = Date.now(); -for await (const chunk of $`bun run examples/emulate-claude-stream.mjs | jq .`.stream()) { +for await (const chunk of $`bun run js/examples/emulate-claude-stream.mjs | jq .`.stream()) { if (chunk.type === 'stdout') { const elapsed = Date.now() - start; const lines = chunk.data.toString().trim().split('\n').slice(0, 2); diff --git a/examples/test-real-shell.mjs b/js/examples/test-real-shell.mjs similarity index 100% rename from examples/test-real-shell.mjs rename to js/examples/test-real-shell.mjs diff --git a/examples/test-real-stdin-commands.mjs b/js/examples/test-real-stdin-commands.mjs similarity index 100% rename from examples/test-real-stdin-commands.mjs rename to js/examples/test-real-stdin-commands.mjs diff --git a/examples/test-runner-batched.mjs b/js/examples/test-runner-batched.mjs similarity index 100% rename from examples/test-runner-batched.mjs rename to js/examples/test-runner-batched.mjs diff --git a/examples/test-runner-simple.mjs b/js/examples/test-runner-simple.mjs similarity index 100% rename from examples/test-runner-simple.mjs rename to js/examples/test-runner-simple.mjs diff --git a/examples/test-runner.mjs b/js/examples/test-runner.mjs similarity index 100% rename from examples/test-runner.mjs rename to js/examples/test-runner.mjs diff --git a/examples/test-scope-parse.mjs b/js/examples/test-scope-parse.mjs similarity index 100% rename from examples/test-scope-parse.mjs rename to js/examples/test-scope-parse.mjs diff --git a/examples/test-sh-pipeline.mjs b/js/examples/test-sh-pipeline.mjs similarity index 87% rename from examples/test-sh-pipeline.mjs rename to js/examples/test-sh-pipeline.mjs index 277be76..220132e 100755 --- a/examples/test-sh-pipeline.mjs +++ b/js/examples/test-sh-pipeline.mjs @@ -8,7 +8,7 @@ console.log('This should stream in real-time\n'); const start = Date.now(); // Execute the entire pipeline as a single shell command -const cmd = $`sh -c 'bun run examples/emulate-claude-stream.mjs | jq .'`; +const cmd = $`sh -c 'bun run js/examples/emulate-claude-stream.mjs | jq .'`; for await (const chunk of cmd.stream()) { if (chunk.type === 'stdout') { diff --git a/examples/test-shell-detection.mjs b/js/examples/test-shell-detection.mjs similarity index 100% rename from examples/test-shell-detection.mjs rename to js/examples/test-shell-detection.mjs diff --git a/examples/test-shell-parser.mjs b/js/examples/test-shell-parser.mjs similarity index 100% rename from examples/test-shell-parser.mjs rename to js/examples/test-shell-parser.mjs diff --git a/examples/test-sigint-behavior.mjs b/js/examples/test-sigint-behavior.mjs similarity index 100% rename from examples/test-sigint-behavior.mjs rename to js/examples/test-sigint-behavior.mjs diff --git a/examples/test-sigint-handling.sh b/js/examples/test-sigint-handling.sh similarity index 100% rename from examples/test-sigint-handling.sh rename to js/examples/test-sigint-handling.sh diff --git a/examples/test-simple-pipe.mjs b/js/examples/test-simple-pipe.mjs similarity index 100% rename from examples/test-simple-pipe.mjs rename to js/examples/test-simple-pipe.mjs diff --git a/examples/test-simple-streaming.mjs b/js/examples/test-simple-streaming.mjs similarity index 100% rename from examples/test-simple-streaming.mjs rename to js/examples/test-simple-streaming.mjs diff --git a/examples/test-sleep-stdin.js b/js/examples/test-sleep-stdin.js similarity index 100% rename from examples/test-sleep-stdin.js rename to js/examples/test-sleep-stdin.js diff --git a/examples/test-sleep.mjs b/js/examples/test-sleep.mjs similarity index 100% rename from examples/test-sleep.mjs rename to js/examples/test-sleep.mjs diff --git a/examples/test-smart-quoting.mjs b/js/examples/test-smart-quoting.mjs similarity index 100% rename from examples/test-smart-quoting.mjs rename to js/examples/test-smart-quoting.mjs diff --git a/examples/test-spaces-in-path.mjs b/js/examples/test-spaces-in-path.mjs similarity index 100% rename from examples/test-spaces-in-path.mjs rename to js/examples/test-spaces-in-path.mjs diff --git a/examples/test-special-chars-quoting.mjs b/js/examples/test-special-chars-quoting.mjs similarity index 100% rename from examples/test-special-chars-quoting.mjs rename to js/examples/test-special-chars-quoting.mjs diff --git a/examples/test-stdin-after-start.mjs b/js/examples/test-stdin-after-start.mjs similarity index 100% rename from examples/test-stdin-after-start.mjs rename to js/examples/test-stdin-after-start.mjs diff --git a/examples/test-stdin-simple.mjs b/js/examples/test-stdin-simple.mjs similarity index 100% rename from examples/test-stdin-simple.mjs rename to js/examples/test-stdin-simple.mjs diff --git a/examples/test-stdin-timing.mjs b/js/examples/test-stdin-timing.mjs similarity index 100% rename from examples/test-stdin-timing.mjs rename to js/examples/test-stdin-timing.mjs diff --git a/examples/test-stdio-combinations.mjs b/js/examples/test-stdio-combinations.mjs similarity index 100% rename from examples/test-stdio-combinations.mjs rename to js/examples/test-stdio-combinations.mjs diff --git a/examples/test-stream-access.mjs b/js/examples/test-stream-access.mjs similarity index 100% rename from examples/test-stream-access.mjs rename to js/examples/test-stream-access.mjs diff --git a/examples/test-stream-cleanup.mjs b/js/examples/test-stream-cleanup.mjs similarity index 100% rename from examples/test-stream-cleanup.mjs rename to js/examples/test-stream-cleanup.mjs diff --git a/examples/test-stream-readers.mjs b/js/examples/test-stream-readers.mjs similarity index 93% rename from examples/test-stream-readers.mjs rename to js/examples/test-stream-readers.mjs index ae78ef0..c9acdb2 100755 --- a/examples/test-stream-readers.mjs +++ b/js/examples/test-stream-readers.mjs @@ -8,7 +8,7 @@ console.log('=== Testing Different Stream Reading Methods ===\n'); console.log('Method 1: ReadableStream getReader():'); { const proc = Bun.spawn( - ['sh', '-c', 'bun run examples/emulate-claude-stream.mjs | jq .'], + ['sh', '-c', 'bun run js/examples/emulate-claude-stream.mjs | jq .'], { stdout: 'pipe', stderr: 'pipe', @@ -55,7 +55,7 @@ console.log('\nMethod 2: Check if readable() method exists:'); console.log('\nMethod 3: Using Response API:'); { const proc = Bun.spawn( - ['sh', '-c', 'bun run examples/emulate-claude-stream.mjs | jq .'], + ['sh', '-c', 'bun run js/examples/emulate-claude-stream.mjs | jq .'], { stdout: 'pipe', stderr: 'pipe', @@ -91,7 +91,7 @@ console.log('\nMethod 3: Using Response API:'); console.log('\nMethod 4: Polling with small timeout:'); { const proc = Bun.spawn( - ['sh', '-c', 'bun run examples/emulate-claude-stream.mjs | jq .'], + ['sh', '-c', 'bun run js/examples/emulate-claude-stream.mjs | jq .'], { stdout: 'pipe', stderr: 'pipe', diff --git a/examples/test-streaming-final.mjs b/js/examples/test-streaming-final.mjs similarity index 89% rename from examples/test-streaming-final.mjs rename to js/examples/test-streaming-final.mjs index cf2f89e..e6a8947 100755 --- a/examples/test-streaming-final.mjs +++ b/js/examples/test-streaming-final.mjs @@ -7,7 +7,7 @@ console.log('=== Final Real-Time Streaming Test ===\n'); console.log('Test 1: Simple command streaming (baseline):'); { const start = Date.now(); - for await (const chunk of $`bun run examples/emulate-claude-stream.mjs`.stream()) { + for await (const chunk of $`bun run js/examples/emulate-claude-stream.mjs`.stream()) { if (chunk.type === 'stdout') { const elapsed = Date.now() - start; const lines = chunk.data.toString().trim().split('\n'); @@ -26,7 +26,7 @@ console.log('\nTest 2: With jq pipeline (using PTY for real-time streaming):'); let chunkCount = 0; // Use a real command instead of virtual echo - for await (const chunk of $`bun run examples/emulate-claude-stream.mjs | jq .`.stream()) { + for await (const chunk of $`bun run js/examples/emulate-claude-stream.mjs | jq .`.stream()) { if (chunk.type === 'stdout') { chunkCount++; const elapsed = Date.now() - start; diff --git a/examples/test-streaming-interfaces.mjs b/js/examples/test-streaming-interfaces.mjs similarity index 98% rename from examples/test-streaming-interfaces.mjs rename to js/examples/test-streaming-interfaces.mjs index a2565ed..906e73e 100755 --- a/examples/test-streaming-interfaces.mjs +++ b/js/examples/test-streaming-interfaces.mjs @@ -9,7 +9,7 @@ * - command.strings.stdin/stdout/stderr (text data) * * Usage: - * node examples/test-streaming-interfaces.mjs + * node js/examples/test-streaming-interfaces.mjs */ import { $ } from '../src/$.mjs'; diff --git a/examples/test-streaming-timing.mjs b/js/examples/test-streaming-timing.mjs similarity index 100% rename from examples/test-streaming-timing.mjs rename to js/examples/test-streaming-timing.mjs diff --git a/examples/test-streaming.mjs b/js/examples/test-streaming.mjs similarity index 100% rename from examples/test-streaming.mjs rename to js/examples/test-streaming.mjs diff --git a/examples/test-streams-stdin-comprehensive.mjs b/js/examples/test-streams-stdin-comprehensive.mjs similarity index 100% rename from examples/test-streams-stdin-comprehensive.mjs rename to js/examples/test-streams-stdin-comprehensive.mjs diff --git a/examples/test-streams-stdin-ctrl-c.mjs b/js/examples/test-streams-stdin-ctrl-c.mjs similarity index 100% rename from examples/test-streams-stdin-ctrl-c.mjs rename to js/examples/test-streams-stdin-ctrl-c.mjs diff --git a/examples/test-template-literal.mjs b/js/examples/test-template-literal.mjs similarity index 100% rename from examples/test-template-literal.mjs rename to js/examples/test-template-literal.mjs diff --git a/examples/test-template-vs-interpolation.mjs b/js/examples/test-template-vs-interpolation.mjs similarity index 100% rename from examples/test-template-vs-interpolation.mjs rename to js/examples/test-template-vs-interpolation.mjs diff --git a/examples/test-timing.mjs b/js/examples/test-timing.mjs similarity index 100% rename from examples/test-timing.mjs rename to js/examples/test-timing.mjs diff --git a/examples/test-top-inherit-stdout-stdin-control.mjs b/js/examples/test-top-inherit-stdout-stdin-control.mjs similarity index 100% rename from examples/test-top-inherit-stdout-stdin-control.mjs rename to js/examples/test-top-inherit-stdout-stdin-control.mjs diff --git a/examples/test-top-quit-stdin.mjs b/js/examples/test-top-quit-stdin.mjs similarity index 100% rename from examples/test-top-quit-stdin.mjs rename to js/examples/test-top-quit-stdin.mjs diff --git a/examples/test-trace-option.mjs b/js/examples/test-trace-option.mjs similarity index 100% rename from examples/test-trace-option.mjs rename to js/examples/test-trace-option.mjs diff --git a/examples/test-user-double-quotes.mjs b/js/examples/test-user-double-quotes.mjs similarity index 100% rename from examples/test-user-double-quotes.mjs rename to js/examples/test-user-double-quotes.mjs diff --git a/examples/test-user-single-quotes.mjs b/js/examples/test-user-single-quotes.mjs similarity index 100% rename from examples/test-user-single-quotes.mjs rename to js/examples/test-user-single-quotes.mjs diff --git a/examples/test-verbose.mjs b/js/examples/test-verbose.mjs similarity index 100% rename from examples/test-verbose.mjs rename to js/examples/test-verbose.mjs diff --git a/examples/test-verbose2.mjs b/js/examples/test-verbose2.mjs similarity index 100% rename from examples/test-verbose2.mjs rename to js/examples/test-verbose2.mjs diff --git a/examples/test-virtual-streaming.mjs b/js/examples/test-virtual-streaming.mjs similarity index 100% rename from examples/test-virtual-streaming.mjs rename to js/examples/test-virtual-streaming.mjs diff --git a/examples/test-waiting-command.mjs b/js/examples/test-waiting-command.mjs similarity index 100% rename from examples/test-waiting-command.mjs rename to js/examples/test-waiting-command.mjs diff --git a/examples/test-waiting-commands.mjs b/js/examples/test-waiting-commands.mjs similarity index 100% rename from examples/test-waiting-commands.mjs rename to js/examples/test-waiting-commands.mjs diff --git a/examples/test-watch-mode.mjs b/js/examples/test-watch-mode.mjs similarity index 100% rename from examples/test-watch-mode.mjs rename to js/examples/test-watch-mode.mjs diff --git a/examples/test-yes-cancellation.mjs b/js/examples/test-yes-cancellation.mjs similarity index 100% rename from examples/test-yes-cancellation.mjs rename to js/examples/test-yes-cancellation.mjs diff --git a/examples/test-yes-detailed.mjs b/js/examples/test-yes-detailed.mjs similarity index 100% rename from examples/test-yes-detailed.mjs rename to js/examples/test-yes-detailed.mjs diff --git a/examples/test-yes-trace.mjs b/js/examples/test-yes-trace.mjs similarity index 100% rename from examples/test-yes-trace.mjs rename to js/examples/test-yes-trace.mjs diff --git a/examples/trace-abort-controller.mjs b/js/examples/trace-abort-controller.mjs similarity index 89% rename from examples/trace-abort-controller.mjs rename to js/examples/trace-abort-controller.mjs index f56ac74..3e17a0d 100755 --- a/examples/trace-abort-controller.mjs +++ b/js/examples/trace-abort-controller.mjs @@ -7,7 +7,7 @@ * signal handling and virtual command cancellation. * * Usage: - * COMMAND_STREAM_TRACE=ProcessRunner node examples/trace-abort-controller.mjs + * COMMAND_STREAM_TRACE=ProcessRunner node js/examples/trace-abort-controller.mjs */ import { $ } from '../src/$.mjs'; diff --git a/examples/trace-error-handling.mjs b/js/examples/trace-error-handling.mjs similarity index 84% rename from examples/trace-error-handling.mjs rename to js/examples/trace-error-handling.mjs index a1060dc..abc1752 100755 --- a/examples/trace-error-handling.mjs +++ b/js/examples/trace-error-handling.mjs @@ -7,7 +7,7 @@ * cleanup on failure, and exception handling. * * Usage: - * COMMAND_STREAM_TRACE=ProcessRunner node examples/trace-error-handling.mjs + * COMMAND_STREAM_TRACE=ProcessRunner node js/examples/trace-error-handling.mjs */ import { $ } from '../src/$.mjs'; diff --git a/examples/trace-pipeline-command.mjs b/js/examples/trace-pipeline-command.mjs similarity index 84% rename from examples/trace-pipeline-command.mjs rename to js/examples/trace-pipeline-command.mjs index b1e144c..b56c8ea 100755 --- a/examples/trace-pipeline-command.mjs +++ b/js/examples/trace-pipeline-command.mjs @@ -7,7 +7,7 @@ * parsing, pipeline creation, and multi-process coordination. * * Usage: - * COMMAND_STREAM_TRACE=ProcessRunner node examples/trace-pipeline-command.mjs + * COMMAND_STREAM_TRACE=ProcessRunner node js/examples/trace-pipeline-command.mjs */ import { $ } from '../src/$.mjs'; diff --git a/examples/trace-signal-handling.mjs b/js/examples/trace-signal-handling.mjs similarity index 89% rename from examples/trace-signal-handling.mjs rename to js/examples/trace-signal-handling.mjs index 4370cb3..a633333 100755 --- a/examples/trace-signal-handling.mjs +++ b/js/examples/trace-signal-handling.mjs @@ -7,7 +7,7 @@ * SIGINT forwarding and cleanup operations. * * Usage: - * COMMAND_STREAM_TRACE=ProcessRunner node examples/trace-signal-handling.mjs + * COMMAND_STREAM_TRACE=ProcessRunner node js/examples/trace-signal-handling.mjs */ import { $ } from '../src/$.mjs'; diff --git a/examples/trace-simple-command.mjs b/js/examples/trace-simple-command.mjs similarity index 83% rename from examples/trace-simple-command.mjs rename to js/examples/trace-simple-command.mjs index 4d8cca4..c303f5e 100755 --- a/examples/trace-simple-command.mjs +++ b/js/examples/trace-simple-command.mjs @@ -7,7 +7,7 @@ * stdout/stderr handling, and completion. * * Usage: - * COMMAND_STREAM_TRACE=ProcessRunner node examples/trace-simple-command.mjs + * COMMAND_STREAM_TRACE=ProcessRunner node js/examples/trace-simple-command.mjs */ import { $ } from '../src/$.mjs'; diff --git a/examples/trace-stderr-output.mjs b/js/examples/trace-stderr-output.mjs similarity index 86% rename from examples/trace-stderr-output.mjs rename to js/examples/trace-stderr-output.mjs index 182c97e..1678a89 100755 --- a/examples/trace-stderr-output.mjs +++ b/js/examples/trace-stderr-output.mjs @@ -7,7 +7,7 @@ * data capture, and I/O operations. * * Usage: - * COMMAND_STREAM_TRACE=ProcessRunner node examples/trace-stderr-output.mjs + * COMMAND_STREAM_TRACE=ProcessRunner node js/examples/trace-stderr-output.mjs */ import { $ } from '../src/$.mjs'; diff --git a/examples/verify-fix-both-runtimes.mjs b/js/examples/verify-fix-both-runtimes.mjs similarity index 100% rename from examples/verify-fix-both-runtimes.mjs rename to js/examples/verify-fix-both-runtimes.mjs diff --git a/examples/verify-issue12-fixed.mjs b/js/examples/verify-issue12-fixed.mjs similarity index 100% rename from examples/verify-issue12-fixed.mjs rename to js/examples/verify-issue12-fixed.mjs diff --git a/examples/which-command-common-commands.mjs b/js/examples/which-command-common-commands.mjs similarity index 100% rename from examples/which-command-common-commands.mjs rename to js/examples/which-command-common-commands.mjs diff --git a/examples/which-command-gh-test.mjs b/js/examples/which-command-gh-test.mjs similarity index 100% rename from examples/which-command-gh-test.mjs rename to js/examples/which-command-gh-test.mjs diff --git a/examples/which-command-nonexistent.mjs b/js/examples/which-command-nonexistent.mjs similarity index 100% rename from examples/which-command-nonexistent.mjs rename to js/examples/which-command-nonexistent.mjs diff --git a/examples/which-command-system-comparison.mjs b/js/examples/which-command-system-comparison.mjs similarity index 100% rename from examples/which-command-system-comparison.mjs rename to js/examples/which-command-system-comparison.mjs diff --git a/examples/working-example.mjs b/js/examples/working-example.mjs similarity index 100% rename from examples/working-example.mjs rename to js/examples/working-example.mjs diff --git a/examples/working-stdin-examples.mjs b/js/examples/working-stdin-examples.mjs similarity index 100% rename from examples/working-stdin-examples.mjs rename to js/examples/working-stdin-examples.mjs diff --git a/examples/working-streaming-demo.mjs b/js/examples/working-streaming-demo.mjs similarity index 100% rename from examples/working-streaming-demo.mjs rename to js/examples/working-streaming-demo.mjs diff --git a/src/$.mjs b/js/src/$.mjs similarity index 100% rename from src/$.mjs rename to js/src/$.mjs diff --git a/src/$.utils.mjs b/js/src/$.utils.mjs similarity index 100% rename from src/$.utils.mjs rename to js/src/$.utils.mjs diff --git a/src/commands/$.basename.mjs b/js/src/commands/$.basename.mjs similarity index 100% rename from src/commands/$.basename.mjs rename to js/src/commands/$.basename.mjs diff --git a/src/commands/$.cat.mjs b/js/src/commands/$.cat.mjs similarity index 100% rename from src/commands/$.cat.mjs rename to js/src/commands/$.cat.mjs diff --git a/src/commands/$.cd.mjs b/js/src/commands/$.cd.mjs similarity index 100% rename from src/commands/$.cd.mjs rename to js/src/commands/$.cd.mjs diff --git a/src/commands/$.cp.mjs b/js/src/commands/$.cp.mjs similarity index 100% rename from src/commands/$.cp.mjs rename to js/src/commands/$.cp.mjs diff --git a/src/commands/$.dirname.mjs b/js/src/commands/$.dirname.mjs similarity index 100% rename from src/commands/$.dirname.mjs rename to js/src/commands/$.dirname.mjs diff --git a/src/commands/$.echo.mjs b/js/src/commands/$.echo.mjs similarity index 100% rename from src/commands/$.echo.mjs rename to js/src/commands/$.echo.mjs diff --git a/src/commands/$.env.mjs b/js/src/commands/$.env.mjs similarity index 100% rename from src/commands/$.env.mjs rename to js/src/commands/$.env.mjs diff --git a/src/commands/$.exit.mjs b/js/src/commands/$.exit.mjs similarity index 100% rename from src/commands/$.exit.mjs rename to js/src/commands/$.exit.mjs diff --git a/src/commands/$.false.mjs b/js/src/commands/$.false.mjs similarity index 100% rename from src/commands/$.false.mjs rename to js/src/commands/$.false.mjs diff --git a/src/commands/$.ls.mjs b/js/src/commands/$.ls.mjs similarity index 100% rename from src/commands/$.ls.mjs rename to js/src/commands/$.ls.mjs diff --git a/src/commands/$.mkdir.mjs b/js/src/commands/$.mkdir.mjs similarity index 100% rename from src/commands/$.mkdir.mjs rename to js/src/commands/$.mkdir.mjs diff --git a/src/commands/$.mv.mjs b/js/src/commands/$.mv.mjs similarity index 100% rename from src/commands/$.mv.mjs rename to js/src/commands/$.mv.mjs diff --git a/src/commands/$.pwd.mjs b/js/src/commands/$.pwd.mjs similarity index 100% rename from src/commands/$.pwd.mjs rename to js/src/commands/$.pwd.mjs diff --git a/src/commands/$.rm.mjs b/js/src/commands/$.rm.mjs similarity index 100% rename from src/commands/$.rm.mjs rename to js/src/commands/$.rm.mjs diff --git a/src/commands/$.seq.mjs b/js/src/commands/$.seq.mjs similarity index 100% rename from src/commands/$.seq.mjs rename to js/src/commands/$.seq.mjs diff --git a/src/commands/$.sleep.mjs b/js/src/commands/$.sleep.mjs similarity index 100% rename from src/commands/$.sleep.mjs rename to js/src/commands/$.sleep.mjs diff --git a/src/commands/$.test.mjs b/js/src/commands/$.test.mjs similarity index 100% rename from src/commands/$.test.mjs rename to js/src/commands/$.test.mjs diff --git a/src/commands/$.touch.mjs b/js/src/commands/$.touch.mjs similarity index 100% rename from src/commands/$.touch.mjs rename to js/src/commands/$.touch.mjs diff --git a/src/commands/$.true.mjs b/js/src/commands/$.true.mjs similarity index 100% rename from src/commands/$.true.mjs rename to js/src/commands/$.true.mjs diff --git a/src/commands/$.which.mjs b/js/src/commands/$.which.mjs similarity index 100% rename from src/commands/$.which.mjs rename to js/src/commands/$.which.mjs diff --git a/src/commands/$.yes.mjs b/js/src/commands/$.yes.mjs similarity index 100% rename from src/commands/$.yes.mjs rename to js/src/commands/$.yes.mjs diff --git a/src/shell-parser.mjs b/js/src/shell-parser.mjs similarity index 100% rename from src/shell-parser.mjs rename to js/src/shell-parser.mjs diff --git a/tests/$.features.test.mjs b/js/tests/$.features.test.mjs similarity index 100% rename from tests/$.features.test.mjs rename to js/tests/$.features.test.mjs diff --git a/tests/$.test.mjs b/js/tests/$.test.mjs similarity index 100% rename from tests/$.test.mjs rename to js/tests/$.test.mjs diff --git a/tests/builtin-commands.test.mjs b/js/tests/builtin-commands.test.mjs similarity index 100% rename from tests/builtin-commands.test.mjs rename to js/tests/builtin-commands.test.mjs diff --git a/tests/bun-shell-path-fix.test.mjs b/js/tests/bun-shell-path-fix.test.mjs similarity index 100% rename from tests/bun-shell-path-fix.test.mjs rename to js/tests/bun-shell-path-fix.test.mjs diff --git a/tests/bun.features.test.mjs b/js/tests/bun.features.test.mjs similarity index 100% rename from tests/bun.features.test.mjs rename to js/tests/bun.features.test.mjs diff --git a/tests/cd-virtual-command.test.mjs b/js/tests/cd-virtual-command.test.mjs similarity index 100% rename from tests/cd-virtual-command.test.mjs rename to js/tests/cd-virtual-command.test.mjs diff --git a/tests/cleanup-verification.test.mjs b/js/tests/cleanup-verification.test.mjs similarity index 100% rename from tests/cleanup-verification.test.mjs rename to js/tests/cleanup-verification.test.mjs diff --git a/tests/ctrl-c-baseline.test.mjs b/js/tests/ctrl-c-baseline.test.mjs similarity index 98% rename from tests/ctrl-c-baseline.test.mjs rename to js/tests/ctrl-c-baseline.test.mjs index a5cabe0..8bc4cc8 100644 --- a/tests/ctrl-c-baseline.test.mjs +++ b/js/tests/ctrl-c-baseline.test.mjs @@ -156,7 +156,7 @@ describe.skipIf(isWindows)('CTRL+C Baseline Tests (Native Spawn)', () => { trace('BaselineTest', 'Testing Node.js script file'); // Use the simple-test-sleep.js which doesn't have ES module dependencies - const child = spawn('node', ['examples/simple-test-sleep.js'], { + const child = spawn('node', ['js/examples/simple-test-sleep.js'], { stdio: ['pipe', 'pipe', 'pipe'], detached: true, cwd: process.cwd(), diff --git a/tests/ctrl-c-basic.test.mjs b/js/tests/ctrl-c-basic.test.mjs similarity index 100% rename from tests/ctrl-c-basic.test.mjs rename to js/tests/ctrl-c-basic.test.mjs diff --git a/tests/ctrl-c-library.test.mjs b/js/tests/ctrl-c-library.test.mjs similarity index 98% rename from tests/ctrl-c-library.test.mjs rename to js/tests/ctrl-c-library.test.mjs index 4149caa..a6da297 100644 --- a/tests/ctrl-c-library.test.mjs +++ b/js/tests/ctrl-c-library.test.mjs @@ -58,7 +58,7 @@ describe.skipIf(isWindows)('CTRL+C Library Tests (command-stream)', () => { trace('LibraryTest', 'Testing library via external script'); // Use test-sleep.mjs which imports our library - const child = spawn('node', ['examples/test-sleep.mjs'], { + const child = spawn('node', ['js/examples/test-sleep.mjs'], { stdio: ['pipe', 'pipe', 'pipe'], detached: true, cwd: process.cwd(), diff --git a/tests/ctrl-c-signal.test.mjs b/js/tests/ctrl-c-signal.test.mjs similarity index 98% rename from tests/ctrl-c-signal.test.mjs rename to js/tests/ctrl-c-signal.test.mjs index 26a2d90..0293492 100644 --- a/tests/ctrl-c-signal.test.mjs +++ b/js/tests/ctrl-c-signal.test.mjs @@ -116,7 +116,11 @@ describe.skipIf(isWindows)('CTRL+C Signal Handling', () => { // Check if file exists first const fs = await import('fs'); const path = await import('path'); - const scriptPath = path.join(process.cwd(), 'examples', 'test-sleep.mjs'); + const scriptPath = path.join( + process.cwd(), + 'js/examples', + 'test-sleep.mjs' + ); trace('SignalTest', () => `Script path: ${scriptPath}`); trace('SignalTest', () => `Script exists: ${fs.existsSync(scriptPath)}`); @@ -664,7 +668,7 @@ describe.skipIf(isWindows)('CTRL+C with Different stdin Modes', () => { [ '-e', ` - import { $ } from './src/$.mjs'; + import { $ } from './js/src/$.mjs'; // Start a long-running command const runner = $\`sleep 5\`; @@ -720,7 +724,7 @@ describe.skipIf(isWindows)('CTRL+C with Different stdin Modes', () => { [ '-e', ` - import { $ } from './src/$.mjs'; + import { $ } from './js/src/$.mjs'; console.log('STARTING_SLEEP_WITH_CUSTOM_STDIN'); @@ -772,7 +776,7 @@ describe.skipIf(isWindows)('CTRL+C with Different stdin Modes', () => { [ '-e', ` - import { $ } from './src/$.mjs'; + import { $ } from './js/src/$.mjs'; const isBun = typeof globalThis.Bun !== 'undefined'; console.log('RUNTIME: ' + (isBun ? 'BUN' : 'NODE')); @@ -826,7 +830,7 @@ describe.skipIf(isWindows)('CTRL+C with Different stdin Modes', () => { // Test 1: Virtual command cancellation with proper exit codes trace('SignalTest', 'Testing virtual command SIGINT cancellation...'); - const child1 = spawn('node', ['examples/test-sleep.mjs'], { + const child1 = spawn('node', ['js/examples/test-sleep.mjs'], { stdio: ['pipe', 'pipe', 'pipe'], detached: true, }); @@ -857,7 +861,7 @@ describe.skipIf(isWindows)('CTRL+C with Different stdin Modes', () => { [ '-e', ` - import { $ } from './src/$.mjs'; + import { $ } from './js/src/$.mjs'; // Set up user's SIGINT handler AFTER importing our library process.on('SIGINT', () => { diff --git a/tests/examples.test.mjs b/js/tests/examples.test.mjs similarity index 96% rename from tests/examples.test.mjs rename to js/tests/examples.test.mjs index 9c7cd42..12d648d 100644 --- a/tests/examples.test.mjs +++ b/js/tests/examples.test.mjs @@ -6,7 +6,7 @@ import { readdirSync, statSync, readFileSync } from 'fs'; import { join } from 'path'; // Get all .mjs examples -const examplesDir = join(process.cwd(), 'examples'); +const examplesDir = join(process.cwd(), 'js/examples'); const allExamples = readdirSync(examplesDir) .filter( (file) => @@ -24,7 +24,7 @@ describe('Examples Execution Tests', () => { // Core functionality test - our main example should work // SKIP: May hang when run with full suite test.skip('readme-example.mjs should execute and demonstrate new API signature', async () => { - const result = await $`node examples/readme-example.mjs`; + const result = await $`node js/examples/readme-example.mjs`; expect(result.code).toBe(0); expect(result.stdout).toContain('Hello, World!'); expect(result.stdout).toContain('Hello, Mr. Smith!'); @@ -36,7 +36,7 @@ describe('Examples Execution Tests', () => { // JSON streaming test - key feature // SKIP: This test hangs when run with full test suite due to sleep commands test.skip('simple-jq-streaming.mjs should complete successfully', async () => { - const result = await $`node examples/simple-jq-streaming.mjs`; + const result = await $`node js/examples/simple-jq-streaming.mjs`; expect(result.code).toBe(0); expect(result.stdout).toContain('✅ Streaming completed successfully!'); expect(result.stdout).toContain('🎉 All tests passed!'); @@ -67,7 +67,7 @@ describe('Examples Execution Tests', () => { if (manualTestExamples.length > 0) { trace('ExampleTest', 'Recommended for manual testing:'); manualTestExamples.forEach((ex) => - trace('ExampleTest', () => `node examples/${ex}`) + trace('ExampleTest', () => `node js/examples/${ex}`) ); } @@ -168,7 +168,7 @@ describe('Examples Execution Tests', () => { const { spawn } = await import('child_process'); // Start our debug script that imports $ but doesn't run commands - const child = spawn('node', ['examples/debug-user-sigint.mjs'], { + const child = spawn('node', ['js/examples/debug-user-sigint.mjs'], { stdio: ['pipe', 'pipe', 'pipe'], detached: true, }); diff --git a/tests/execa.features.test.mjs b/js/tests/execa.features.test.mjs similarity index 100% rename from tests/execa.features.test.mjs rename to js/tests/execa.features.test.mjs diff --git a/tests/gh-commands.test.mjs b/js/tests/gh-commands.test.mjs similarity index 100% rename from tests/gh-commands.test.mjs rename to js/tests/gh-commands.test.mjs diff --git a/tests/gh-gist-operations.test.mjs b/js/tests/gh-gist-operations.test.mjs similarity index 100% rename from tests/gh-gist-operations.test.mjs rename to js/tests/gh-gist-operations.test.mjs diff --git a/tests/git-gh-cd.test.mjs b/js/tests/git-gh-cd.test.mjs similarity index 100% rename from tests/git-gh-cd.test.mjs rename to js/tests/git-gh-cd.test.mjs diff --git a/tests/interactive-option.test.mjs b/js/tests/interactive-option.test.mjs similarity index 100% rename from tests/interactive-option.test.mjs rename to js/tests/interactive-option.test.mjs diff --git a/tests/interactive-streaming.test.mjs b/js/tests/interactive-streaming.test.mjs similarity index 100% rename from tests/interactive-streaming.test.mjs rename to js/tests/interactive-streaming.test.mjs diff --git a/tests/issue-135-final.test.mjs b/js/tests/issue-135-final.test.mjs similarity index 100% rename from tests/issue-135-final.test.mjs rename to js/tests/issue-135-final.test.mjs diff --git a/tests/jq-color-behavior.test.mjs b/js/tests/jq-color-behavior.test.mjs similarity index 100% rename from tests/jq-color-behavior.test.mjs rename to js/tests/jq-color-behavior.test.mjs diff --git a/tests/jq.test.mjs b/js/tests/jq.test.mjs similarity index 100% rename from tests/jq.test.mjs rename to js/tests/jq.test.mjs diff --git a/tests/options-examples.test.mjs b/js/tests/options-examples.test.mjs similarity index 100% rename from tests/options-examples.test.mjs rename to js/tests/options-examples.test.mjs diff --git a/tests/options-syntax.test.mjs b/js/tests/options-syntax.test.mjs similarity index 100% rename from tests/options-syntax.test.mjs rename to js/tests/options-syntax.test.mjs diff --git a/tests/path-interpolation.test.mjs b/js/tests/path-interpolation.test.mjs similarity index 100% rename from tests/path-interpolation.test.mjs rename to js/tests/path-interpolation.test.mjs diff --git a/tests/pipe.test.mjs b/js/tests/pipe.test.mjs similarity index 100% rename from tests/pipe.test.mjs rename to js/tests/pipe.test.mjs diff --git a/tests/raw-function.test.mjs b/js/tests/raw-function.test.mjs similarity index 100% rename from tests/raw-function.test.mjs rename to js/tests/raw-function.test.mjs diff --git a/tests/readme-examples.test.mjs b/js/tests/readme-examples.test.mjs similarity index 100% rename from tests/readme-examples.test.mjs rename to js/tests/readme-examples.test.mjs diff --git a/tests/resource-cleanup-internals.test.mjs b/js/tests/resource-cleanup-internals.test.mjs similarity index 100% rename from tests/resource-cleanup-internals.test.mjs rename to js/tests/resource-cleanup-internals.test.mjs diff --git a/tests/shell-settings.test.mjs b/js/tests/shell-settings.test.mjs similarity index 100% rename from tests/shell-settings.test.mjs rename to js/tests/shell-settings.test.mjs diff --git a/tests/sigint-cleanup-isolated.test.mjs b/js/tests/sigint-cleanup-isolated.test.mjs similarity index 100% rename from tests/sigint-cleanup-isolated.test.mjs rename to js/tests/sigint-cleanup-isolated.test.mjs diff --git a/tests/sigint-cleanup.test.mjs b/js/tests/sigint-cleanup.test.mjs similarity index 100% rename from tests/sigint-cleanup.test.mjs rename to js/tests/sigint-cleanup.test.mjs diff --git a/tests/start-run-edge-cases.test.mjs b/js/tests/start-run-edge-cases.test.mjs similarity index 100% rename from tests/start-run-edge-cases.test.mjs rename to js/tests/start-run-edge-cases.test.mjs diff --git a/tests/start-run-options.test.mjs b/js/tests/start-run-options.test.mjs similarity index 100% rename from tests/start-run-options.test.mjs rename to js/tests/start-run-options.test.mjs diff --git a/tests/stderr-output-handling.test.mjs b/js/tests/stderr-output-handling.test.mjs similarity index 100% rename from tests/stderr-output-handling.test.mjs rename to js/tests/stderr-output-handling.test.mjs diff --git a/tests/streaming-interfaces.test.mjs b/js/tests/streaming-interfaces.test.mjs similarity index 100% rename from tests/streaming-interfaces.test.mjs rename to js/tests/streaming-interfaces.test.mjs diff --git a/tests/sync.test.mjs b/js/tests/sync.test.mjs similarity index 100% rename from tests/sync.test.mjs rename to js/tests/sync.test.mjs diff --git a/tests/system-pipe.test.mjs b/js/tests/system-pipe.test.mjs similarity index 100% rename from tests/system-pipe.test.mjs rename to js/tests/system-pipe.test.mjs diff --git a/tests/test-cleanup.mjs b/js/tests/test-cleanup.mjs similarity index 97% rename from tests/test-cleanup.mjs rename to js/tests/test-cleanup.mjs index 6bfe6b6..2e0eca4 100644 --- a/tests/test-cleanup.mjs +++ b/js/tests/test-cleanup.mjs @@ -47,7 +47,7 @@ export async function beforeTestCleanup() { try { // Force restoration regardless of current state process.chdir(originalCwd); - } catch (e) { + } catch (_e) { // Original directory might be gone, try fallbacks try { if (existsSync(originalCwd)) { @@ -59,7 +59,7 @@ export async function beforeTestCleanup() { } else { process.chdir('/'); } - } catch (e2) { + } catch (_e2) { console.error( '[test-cleanup] FATAL: Cannot set working directory in beforeTestCleanup' ); @@ -74,7 +74,7 @@ export async function beforeTestCleanup() { // Extra safety: ensure we're in a valid directory after reset try { process.cwd(); // This will throw if we're in a bad state - } catch (e) { + } catch (_e) { // Force to a known good directory process.chdir(originalCwd); } @@ -96,7 +96,7 @@ export async function beforeTestCleanup() { `[test-cleanup] CRITICAL: Cannot restore to original directory ${originalCwd}, stuck in ${verifiedCwd}` ); } - } catch (e) { + } catch (_e) { throw new Error( `[test-cleanup] CRITICAL: Cannot restore to original directory ${originalCwd}, stuck in ${finalCwd}` ); @@ -121,7 +121,7 @@ export async function afterTestCleanup() { try { // Force restoration regardless of current state process.chdir(originalCwd); - } catch (e) { + } catch (_e) { // Original directory might be gone, try fallbacks try { if (existsSync(originalCwd)) { @@ -133,7 +133,7 @@ export async function afterTestCleanup() { } else { process.chdir('/'); } - } catch (e2) { + } catch (_e2) { console.error( '[test-cleanup] FATAL: Cannot set working directory in afterTestCleanup' ); @@ -148,7 +148,7 @@ export async function afterTestCleanup() { // Extra safety: ensure we're in a valid directory after reset try { process.cwd(); // This will throw if we're in a bad state - } catch (e) { + } catch (_e) { // Force to a known good directory process.chdir(originalCwd); } @@ -170,7 +170,7 @@ export async function afterTestCleanup() { `[test-cleanup] CRITICAL: Cannot restore to original directory ${originalCwd} in afterEach, stuck in ${verifiedCwd}` ); } - } catch (e) { + } catch (_e) { throw new Error( `[test-cleanup] CRITICAL: Cannot restore to original directory ${originalCwd} in afterEach, stuck in ${finalCwd}` ); @@ -188,7 +188,7 @@ process.on('beforeExit', () => { if (process.cwd() !== originalCwd) { process.chdir(originalCwd); } - } catch (e) { + } catch (_e) { // Ignore } }); diff --git a/tests/test-helper-fixed.mjs b/js/tests/test-helper-fixed.mjs similarity index 96% rename from tests/test-helper-fixed.mjs rename to js/tests/test-helper-fixed.mjs index 6bf9b0b..f82f8ca 100644 --- a/tests/test-helper-fixed.mjs +++ b/js/tests/test-helper-fixed.mjs @@ -50,7 +50,7 @@ export function setupTestHooks() { try { // Force restoration regardless of current state process.chdir(originalCwd); - } catch (e) { + } catch (_e) { // Original directory might be gone, try fallbacks try { if (existsSync(originalCwd)) { @@ -62,7 +62,7 @@ export function setupTestHooks() { } else { process.chdir('/'); } - } catch (e2) { + } catch (_e2) { console.error( '[test-helper] FATAL: Cannot set working directory in beforeEach' ); @@ -77,7 +77,7 @@ export function setupTestHooks() { // Extra safety: ensure we're in a valid directory after reset try { process.cwd(); // This will throw if we're in a bad state - } catch (e) { + } catch (_e) { // Force to a known good directory process.chdir(originalCwd); } @@ -96,7 +96,7 @@ export function setupTestHooks() { try { // Force restoration regardless of current state process.chdir(originalCwd); - } catch (e) { + } catch (_e) { // Original directory might be gone, try fallbacks try { if (existsSync(originalCwd)) { @@ -108,7 +108,7 @@ export function setupTestHooks() { } else { process.chdir('/'); } - } catch (e2) { + } catch (_e2) { console.error( '[test-helper] FATAL: Cannot set working directory in afterEach' ); @@ -123,7 +123,7 @@ export function setupTestHooks() { // Extra safety: ensure we're in a valid directory after reset try { process.cwd(); // This will throw if we're in a bad state - } catch (e) { + } catch (_e) { // Force to a known good directory process.chdir(originalCwd); } @@ -140,7 +140,7 @@ process.on('beforeExit', () => { if (process.cwd() !== originalCwd) { process.chdir(originalCwd); } - } catch (e) { + } catch (_e) { // Ignore } }); diff --git a/tests/test-helper-v2.mjs b/js/tests/test-helper-v2.mjs similarity index 96% rename from tests/test-helper-v2.mjs rename to js/tests/test-helper-v2.mjs index c2aa4cb..3a42e80 100644 --- a/tests/test-helper-v2.mjs +++ b/js/tests/test-helper-v2.mjs @@ -28,7 +28,7 @@ export function setupTestHooks() { try { // Force restoration regardless of current state process.chdir(originalCwd); - } catch (e) { + } catch (_e) { // Original directory might be gone, try fallbacks try { if (existsSync(originalCwd)) { @@ -40,7 +40,7 @@ export function setupTestHooks() { } else { process.chdir('/'); } - } catch (e2) { + } catch (_e2) { console.error( '[test-helper-v2] FATAL: Cannot set working directory in beforeEach' ); @@ -55,7 +55,7 @@ export function setupTestHooks() { // Extra safety: ensure we're in a valid directory after reset try { process.cwd(); // This will throw if we're in a bad state - } catch (e) { + } catch (_e) { // Force to a known good directory process.chdir(originalCwd); } @@ -74,7 +74,7 @@ export function setupTestHooks() { try { // Force restoration regardless of current state process.chdir(originalCwd); - } catch (e) { + } catch (_e) { // Original directory might be gone, try fallbacks try { if (existsSync(originalCwd)) { @@ -86,7 +86,7 @@ export function setupTestHooks() { } else { process.chdir('/'); } - } catch (e2) { + } catch (_e2) { console.error( '[test-helper-v2] FATAL: Cannot set working directory in afterEach' ); @@ -101,7 +101,7 @@ export function setupTestHooks() { // Extra safety: ensure we're in a valid directory after reset try { process.cwd(); // This will throw if we're in a bad state - } catch (e) { + } catch (_e) { // Force to a known good directory process.chdir(originalCwd); } diff --git a/tests/test-helper.mjs b/js/tests/test-helper.mjs similarity index 97% rename from tests/test-helper.mjs rename to js/tests/test-helper.mjs index 9320a2f..3ddb896 100644 --- a/tests/test-helper.mjs +++ b/js/tests/test-helper.mjs @@ -25,7 +25,7 @@ process.on('beforeExit', () => { if (process.cwd() !== originalCwd) { process.chdir(originalCwd); } - } catch (e) { + } catch (_e) { // Ignore } }); @@ -41,7 +41,7 @@ beforeEach(async () => { try { // Force restoration regardless of current state process.chdir(originalCwd); - } catch (e) { + } catch (_e) { // Original directory might be gone, try fallbacks try { if (existsSync(originalCwd)) { @@ -53,7 +53,7 @@ beforeEach(async () => { } else { process.chdir('/'); } - } catch (e2) { + } catch (_e2) { console.error( '[test-helper] FATAL: Cannot set working directory in beforeEach' ); @@ -67,7 +67,7 @@ beforeEach(async () => { // Extra safety: ensure we're in a valid directory after reset try { process.cwd(); // This will throw if we're in a bad state - } catch (e) { + } catch (_e) { // Force to a known good directory process.chdir(originalCwd); } @@ -89,7 +89,7 @@ beforeEach(async () => { `[test-helper] CRITICAL: Cannot restore to original directory ${originalCwd}, stuck in ${verifiedCwd}` ); } - } catch (e) { + } catch (_e) { throw new Error( `[test-helper] CRITICAL: Cannot restore to original directory ${originalCwd}, stuck in ${finalCwd}` ); @@ -109,7 +109,7 @@ afterEach(async () => { try { // Force restoration regardless of current state process.chdir(originalCwd); - } catch (e) { + } catch (_e) { // Original directory might be gone, try fallbacks try { if (existsSync(originalCwd)) { @@ -121,7 +121,7 @@ afterEach(async () => { } else { process.chdir('/'); } - } catch (e2) { + } catch (_e2) { console.error( '[test-helper] FATAL: Cannot set working directory in afterEach' ); @@ -135,7 +135,7 @@ afterEach(async () => { // Extra safety: ensure we're in a valid directory after reset try { process.cwd(); // This will throw if we're in a bad state - } catch (e) { + } catch (_e) { // Force to a known good directory process.chdir(originalCwd); } @@ -157,7 +157,7 @@ afterEach(async () => { `[test-helper] CRITICAL: Cannot restore to original directory ${originalCwd}, stuck in ${verifiedCwd}` ); } - } catch (e) { + } catch (_e) { throw new Error( `[test-helper] CRITICAL: Cannot restore to original directory ${originalCwd}, stuck in ${finalCwd}` ); diff --git a/tests/test-sigint-child.js b/js/tests/test-sigint-child.js similarity index 100% rename from tests/test-sigint-child.js rename to js/tests/test-sigint-child.js diff --git a/tests/text-method.test.mjs b/js/tests/text-method.test.mjs similarity index 99% rename from tests/text-method.test.mjs rename to js/tests/text-method.test.mjs index ab0b70b..ce7227e 100644 --- a/tests/text-method.test.mjs +++ b/js/tests/text-method.test.mjs @@ -71,7 +71,7 @@ describe('.text() method for Bun.$ compatibility', () => { }); test('.text() should work with ls built-in command', async () => { - const result = await $`ls -1 tests/`; + const result = await $`ls -1 js/tests/`; const text = await result.text(); // Should contain at least this test file diff --git a/tests/virtual.test.mjs b/js/tests/virtual.test.mjs similarity index 100% rename from tests/virtual.test.mjs rename to js/tests/virtual.test.mjs diff --git a/tests/yes-command-cleanup.test.mjs b/js/tests/yes-command-cleanup.test.mjs similarity index 99% rename from tests/yes-command-cleanup.test.mjs rename to js/tests/yes-command-cleanup.test.mjs index 34ac9ea..f0ff5dd 100644 --- a/tests/yes-command-cleanup.test.mjs +++ b/js/tests/yes-command-cleanup.test.mjs @@ -152,7 +152,7 @@ describe('Yes Command Cleanup Tests', () => { test('should cleanup yes command in subprocess', async () => { // Create a test script that runs yes and should exit cleanly const script = ` - import { $ } from './src/$.mjs'; + import { $ } from './js/src/$.mjs'; const runner = $({ mirror: false })\`yes "subprocess test"\`; let count = 0; diff --git a/tests/zx.features.test.mjs b/js/tests/zx.features.test.mjs similarity index 100% rename from tests/zx.features.test.mjs rename to js/tests/zx.features.test.mjs diff --git a/package.json b/package.json index 63c0274..c47aed9 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,9 @@ "version": "0.8.3", "description": "Modern $ shell utility library with streaming, async iteration, and EventEmitter support, optimized for Bun runtime", "type": "module", - "main": "src/$.mjs", + "main": "js/src/$.mjs", "exports": { - ".": "./src/$.mjs" + ".": "./js/src/$.mjs" }, "repository": { "type": "git", @@ -16,14 +16,14 @@ "url": "https://github.com/link-foundation/command-stream/issues" }, "scripts": { - "test": "bun test tests/", - "test:coverage": "bun test tests/ --coverage", - "test:features": "bun test tests/*.features.test.mjs", - "test:comparison": "bun test tests/*.features.test.mjs --reporter=verbose", - "test:readme": "bun test tests/readme-examples.test.mjs", - "test:sync": "bun test tests/sync.test.mjs", - "test:builtin": "bun test tests/builtin-commands.test.mjs", - "test:pipe": "bun test tests/pipe.test.mjs", + "test": "bun test js/tests/", + "test:coverage": "bun test js/tests/ --coverage", + "test:features": "bun test js/tests/*.features.test.mjs", + "test:comparison": "bun test js/tests/*.features.test.mjs --reporter=verbose", + "test:readme": "bun test js/tests/readme-examples.test.mjs", + "test:sync": "bun test js/tests/sync.test.mjs", + "test:builtin": "bun test js/tests/builtin-commands.test.mjs", + "test:pipe": "bun test js/tests/pipe.test.mjs", "lint": "eslint .", "lint:fix": "eslint . --fix", "format": "prettier --write .", @@ -54,7 +54,8 @@ "node": ">=20.0.0" }, "files": [ - "src/", + "js/", + "rust/", "README.md", "LICENSE" ], diff --git a/rust/Cargo.lock b/rust/Cargo.lock new file mode 100644 index 0000000..a2b4183 --- /dev/null +++ b/rust/Cargo.lock @@ -0,0 +1,947 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "assert_cmd" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcbb6924530aa9e0432442af08bbcafdad182db80d2e560da42a6d442535bf85" +dependencies = [ + "anstyle", + "bstr", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "cc" +version = "1.2.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "command-stream" +version = "0.8.3" +dependencies = [ + "assert_cmd", + "async-trait", + "chrono", + "filetime", + "glob", + "libc", + "nix", + "once_cell", + "regex", + "serde", + "serde_json", + "tempfile", + "thiserror", + "tokio", + "tokio-test", + "which", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "env_home" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "filetime" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.60.2", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.178" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" + +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags", + "libc", + "redox_syscall 0.7.0", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "predicates" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +dependencies = [ + "anstyle", + "difflib", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" + +[[package]] +name = "predicates-tree" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "proc-macro2" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_syscall" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.148" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-test" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" +dependencies = [ + "async-stream", + "bytes", + "futures-core", + "tokio", + "tokio-stream", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "which" +version = "7.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762" +dependencies = [ + "either", + "env_home", + "rustix", + "winsafe", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "zmij" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f4a4e8e9dc5c62d159f04fcdbe07f4c3fb710415aab4754bf11505501e3251d" diff --git a/rust/Cargo.toml b/rust/Cargo.toml new file mode 100644 index 0000000..e283685 --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "command-stream" +version = "0.8.3" +edition = "2021" +authors = ["link-foundation"] +description = "Modern shell command execution library with streaming, async iteration, and event support" +license = "Unlicense" +repository = "https://github.com/link-foundation/command-stream" +keywords = ["shell", "command", "streaming", "async", "process"] +categories = ["command-line-utilities", "asynchronous"] +readme = "../README.md" + +[lib] +name = "command_stream" +path = "src/lib.rs" + +[[bin]] +name = "command-stream" +path = "src/main.rs" + +[dependencies] +tokio = { version = "1.43", features = ["full", "process", "signal"] } +async-trait = "0.1" +thiserror = "2.0" +once_cell = "1.20" +regex = "1.11" +serde = { version = "1.0", features = ["derive"], optional = true } +serde_json = { version = "1.0", optional = true } +nix = { version = "0.29", features = ["signal", "process"] } +libc = "0.2" +which = "7.0" +glob = "0.3" +chrono = "0.4" +filetime = "0.2" + +[dev-dependencies] +tokio-test = "0.4" +tempfile = "3.14" +assert_cmd = "2.0" + +[features] +default = [] +json = ["serde", "serde_json"] + +[profile.release] +opt-level = 3 +lto = true diff --git a/rust/src/commands/basename.rs b/rust/src/commands/basename.rs new file mode 100644 index 0000000..704d7d9 --- /dev/null +++ b/rust/src/commands/basename.rs @@ -0,0 +1,69 @@ +//! Virtual `basename` command implementation + +use crate::commands::CommandContext; +use crate::utils::{CommandResult, VirtualUtils}; +use std::path::Path; + +/// Execute the basename command +/// +/// Strips directory and suffix from filenames. +pub async fn basename(ctx: CommandContext) -> CommandResult { + if ctx.args.is_empty() { + return VirtualUtils::missing_operand_error("basename"); + } + + let path = &ctx.args[0]; + let suffix = ctx.args.get(1); + + let base = Path::new(path) + .file_name() + .map(|n| n.to_string_lossy().to_string()) + .unwrap_or_default(); + + let result = if let Some(suf) = suffix { + if base.ends_with(suf.as_str()) { + base[..base.len() - suf.len()].to_string() + } else { + base + } + } else { + base + }; + + CommandResult::success(format!("{}\n", result)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_basename_simple() { + let ctx = CommandContext::new(vec!["/path/to/file.txt".to_string()]); + let result = basename(ctx).await; + + assert!(result.is_success()); + assert_eq!(result.stdout, "file.txt\n"); + } + + #[tokio::test] + async fn test_basename_with_suffix() { + let ctx = CommandContext::new(vec![ + "/path/to/file.txt".to_string(), + ".txt".to_string(), + ]); + let result = basename(ctx).await; + + assert!(result.is_success()); + assert_eq!(result.stdout, "file\n"); + } + + #[tokio::test] + async fn test_basename_no_path() { + let ctx = CommandContext::new(vec!["file.txt".to_string()]); + let result = basename(ctx).await; + + assert!(result.is_success()); + assert_eq!(result.stdout, "file.txt\n"); + } +} diff --git a/rust/src/commands/cat.rs b/rust/src/commands/cat.rs new file mode 100644 index 0000000..70447c8 --- /dev/null +++ b/rust/src/commands/cat.rs @@ -0,0 +1,123 @@ +//! Virtual `cat` command implementation + +use crate::commands::CommandContext; +use crate::utils::{trace_lazy, CommandResult, VirtualUtils}; +use std::fs; + +/// Execute the cat command +/// +/// Concatenates and displays file contents. +pub async fn cat(ctx: CommandContext) -> CommandResult { + if ctx.args.is_empty() { + // Read from stdin if no files specified + if let Some(ref stdin) = ctx.stdin { + if !stdin.is_empty() { + return CommandResult::success(stdin.clone()); + } + } + return CommandResult::success_empty(); + } + + let cwd = ctx.get_cwd(); + let mut outputs = Vec::new(); + + for file in &ctx.args { + // Check for cancellation before processing each file + if ctx.is_cancelled() { + trace_lazy("VirtualCommand", || { + "cat: cancelled while processing files".to_string() + }); + return CommandResult::error_with_code("", 130); // SIGINT exit code + } + + trace_lazy("VirtualCommand", || { + format!("cat: reading file {:?}", file) + }); + + let resolved_path = VirtualUtils::resolve_path(file, Some(&cwd)); + + match fs::read_to_string(&resolved_path) { + Ok(content) => { + outputs.push(content); + } + Err(e) => { + let error_msg = if e.kind() == std::io::ErrorKind::NotFound { + format!("cat: {}: No such file or directory\n", file) + } else if e.kind() == std::io::ErrorKind::IsADirectory + || (e.kind() == std::io::ErrorKind::Other && e.to_string().contains("directory")) + { + format!("cat: {}: Is a directory\n", file) + } else { + format!("cat: {}: {}\n", file, e) + }; + return CommandResult::error(error_msg); + } + } + } + + let output = outputs.join(""); + trace_lazy("VirtualCommand", || { + format!("cat: success, bytes read: {}", output.len()) + }); + + CommandResult::success(output) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + use tempfile::NamedTempFile; + + #[tokio::test] + async fn test_cat_file() { + let mut temp = NamedTempFile::new().unwrap(); + writeln!(temp, "Hello, World!").unwrap(); + + let ctx = CommandContext::new(vec![ + temp.path().to_string_lossy().to_string() + ]); + let result = cat(ctx).await; + + assert!(result.is_success()); + assert_eq!(result.stdout, "Hello, World!\n"); + } + + #[tokio::test] + async fn test_cat_stdin() { + let mut ctx = CommandContext::new(vec![]); + ctx.stdin = Some("stdin content".to_string()); + + let result = cat(ctx).await; + assert!(result.is_success()); + assert_eq!(result.stdout, "stdin content"); + } + + #[tokio::test] + async fn test_cat_nonexistent() { + let ctx = CommandContext::new(vec![ + "/nonexistent/file/12345".to_string() + ]); + let result = cat(ctx).await; + + assert!(!result.is_success()); + assert!(result.stderr.contains("No such file or directory")); + } + + #[tokio::test] + async fn test_cat_multiple_files() { + let mut temp1 = NamedTempFile::new().unwrap(); + let mut temp2 = NamedTempFile::new().unwrap(); + write!(temp1, "file1").unwrap(); + write!(temp2, "file2").unwrap(); + + let ctx = CommandContext::new(vec![ + temp1.path().to_string_lossy().to_string(), + temp2.path().to_string_lossy().to_string(), + ]); + let result = cat(ctx).await; + + assert!(result.is_success()); + assert_eq!(result.stdout, "file1file2"); + } +} diff --git a/rust/src/commands/cd.rs b/rust/src/commands/cd.rs new file mode 100644 index 0000000..a0e9f88 --- /dev/null +++ b/rust/src/commands/cd.rs @@ -0,0 +1,67 @@ +//! Virtual `cd` command implementation + +use crate::commands::CommandContext; +use crate::utils::{trace, CommandResult}; +use std::env; +use std::path::PathBuf; + +/// Execute the cd command +/// +/// Changes the current working directory. +pub async fn cd(ctx: CommandContext) -> CommandResult { + let target = if ctx.args.is_empty() { + // No argument - go to home directory + env::var("HOME") + .or_else(|_| env::var("USERPROFILE")) + .unwrap_or_else(|_| "/".to_string()) + } else { + ctx.args[0].clone() + }; + + trace("VirtualCommand", &format!("cd: changing directory to {:?}", target)); + + let path = PathBuf::from(&target); + + match env::set_current_dir(&path) { + Ok(()) => { + let new_dir = env::current_dir() + .map(|p| p.display().to_string()) + .unwrap_or_default(); + trace("VirtualCommand", &format!("cd: success, new dir: {}", new_dir)); + // cd command should not output anything on success + CommandResult::success_empty() + } + Err(e) => { + trace("VirtualCommand", &format!("cd: failed: {}", e)); + CommandResult::error(format!("cd: {}\n", e)) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::tempdir; + + #[tokio::test] + async fn test_cd_to_temp() { + let temp = tempdir().unwrap(); + let temp_path = temp.path().to_string_lossy().to_string(); + let original_dir = env::current_dir().unwrap(); + + let ctx = CommandContext::new(vec![temp_path.clone()]); + let result = cd(ctx).await; + assert!(result.is_success()); + assert_eq!(result.stdout, ""); + + // Restore original directory + env::set_current_dir(original_dir).unwrap(); + } + + #[tokio::test] + async fn test_cd_to_nonexistent() { + let ctx = CommandContext::new(vec!["/nonexistent/path/12345".to_string()]); + let result = cd(ctx).await; + assert!(!result.is_success()); + } +} diff --git a/rust/src/commands/cp.rs b/rust/src/commands/cp.rs new file mode 100644 index 0000000..adee339 --- /dev/null +++ b/rust/src/commands/cp.rs @@ -0,0 +1,187 @@ +//! Virtual `cp` command implementation + +use crate::commands::CommandContext; +use crate::utils::{trace_lazy, CommandResult, VirtualUtils}; +use std::fs; +use std::path::Path; + +/// Execute the cp command +/// +/// Copies files and directories. +pub async fn cp(ctx: CommandContext) -> CommandResult { + if ctx.args.len() < 2 { + return VirtualUtils::invalid_argument_error("cp", "missing file operand"); + } + + // Parse flags + let mut recursive = false; + let mut paths = Vec::new(); + + for arg in &ctx.args { + if arg == "-r" || arg == "-R" || arg == "--recursive" { + recursive = true; + } else if arg.starts_with('-') { + if arg.contains('r') || arg.contains('R') { + recursive = true; + } + } else { + paths.push(arg.clone()); + } + } + + if paths.len() < 2 { + return VirtualUtils::invalid_argument_error("cp", "missing destination file operand"); + } + + let cwd = ctx.get_cwd(); + let dest = paths.pop().unwrap(); + let dest_path = VirtualUtils::resolve_path(&dest, Some(&cwd)); + + // If multiple sources or dest is a directory, copy into the directory + let dest_is_dir = dest_path.is_dir(); + let multiple_sources = paths.len() > 1; + + if multiple_sources && !dest_is_dir { + return CommandResult::error(format!( + "cp: target '{}' is not a directory\n", + dest + )); + } + + for source in paths { + let source_path = VirtualUtils::resolve_path(&source, Some(&cwd)); + + trace_lazy("VirtualCommand", || { + format!("cp: copying {:?} to {:?}", source_path, dest_path) + }); + + if !source_path.exists() { + return CommandResult::error(format!( + "cp: cannot stat '{}': No such file or directory\n", + source + )); + } + + let final_dest = if dest_is_dir { + dest_path.join(source_path.file_name().unwrap_or_default()) + } else { + dest_path.clone() + }; + + if source_path.is_dir() { + if !recursive { + return CommandResult::error(format!( + "cp: -r not specified; omitting directory '{}'\n", + source + )); + } + + if let Err(e) = copy_dir_recursive(&source_path, &final_dest) { + return CommandResult::error(format!( + "cp: cannot copy '{}': {}\n", + source, e + )); + } + } else { + if let Some(parent) = final_dest.parent() { + if !parent.exists() { + if let Err(e) = fs::create_dir_all(parent) { + return CommandResult::error(format!( + "cp: cannot create directory '{}': {}\n", + parent.display(), e + )); + } + } + } + + if let Err(e) = fs::copy(&source_path, &final_dest) { + return CommandResult::error(format!( + "cp: cannot copy '{}': {}\n", + source, e + )); + } + } + } + + CommandResult::success_empty() +} + +fn copy_dir_recursive(src: &Path, dst: &Path) -> std::io::Result<()> { + fs::create_dir_all(dst)?; + + for entry in fs::read_dir(src)? { + let entry = entry?; + let entry_path = entry.path(); + let dest_path = dst.join(entry.file_name()); + + if entry_path.is_dir() { + copy_dir_recursive(&entry_path, &dest_path)?; + } else { + fs::copy(&entry_path, &dest_path)?; + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::tempdir; + + #[tokio::test] + async fn test_cp_file() { + let temp = tempdir().unwrap(); + let src = temp.path().join("source.txt"); + let dst = temp.path().join("dest.txt"); + fs::write(&src, "test content").unwrap(); + + let ctx = CommandContext::new(vec![ + src.to_string_lossy().to_string(), + dst.to_string_lossy().to_string(), + ]); + let result = cp(ctx).await; + + assert!(result.is_success()); + assert!(dst.exists()); + assert_eq!(fs::read_to_string(&dst).unwrap(), "test content"); + } + + #[tokio::test] + async fn test_cp_directory_recursive() { + let temp = tempdir().unwrap(); + let src_dir = temp.path().join("src_dir"); + let dst_dir = temp.path().join("dst_dir"); + + fs::create_dir(&src_dir).unwrap(); + fs::write(src_dir.join("file.txt"), "test").unwrap(); + + let ctx = CommandContext::new(vec![ + "-r".to_string(), + src_dir.to_string_lossy().to_string(), + dst_dir.to_string_lossy().to_string(), + ]); + let result = cp(ctx).await; + + assert!(result.is_success()); + assert!(dst_dir.join("file.txt").exists()); + } + + #[tokio::test] + async fn test_cp_directory_without_recursive() { + let temp = tempdir().unwrap(); + let src_dir = temp.path().join("src_dir"); + let dst_dir = temp.path().join("dst_dir"); + + fs::create_dir(&src_dir).unwrap(); + + let ctx = CommandContext::new(vec![ + src_dir.to_string_lossy().to_string(), + dst_dir.to_string_lossy().to_string(), + ]); + let result = cp(ctx).await; + + assert!(!result.is_success()); + assert!(result.stderr.contains("-r not specified")); + } +} diff --git a/rust/src/commands/dirname.rs b/rust/src/commands/dirname.rs new file mode 100644 index 0000000..b8668b5 --- /dev/null +++ b/rust/src/commands/dirname.rs @@ -0,0 +1,57 @@ +//! Virtual `dirname` command implementation + +use crate::commands::CommandContext; +use crate::utils::{CommandResult, VirtualUtils}; +use std::path::Path; + +/// Execute the dirname command +/// +/// Strips the last component from filenames. +pub async fn dirname(ctx: CommandContext) -> CommandResult { + if ctx.args.is_empty() { + return VirtualUtils::missing_operand_error("dirname"); + } + + let path = &ctx.args[0]; + let parent = Path::new(path) + .parent() + .map(|p| p.to_string_lossy().to_string()) + .unwrap_or_else(|| ".".to_string()); + + // Handle empty parent (file in current directory) + let result = if parent.is_empty() { "." } else { &parent }; + + CommandResult::success(format!("{}\n", result)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_dirname_simple() { + let ctx = CommandContext::new(vec!["/path/to/file.txt".to_string()]); + let result = dirname(ctx).await; + + assert!(result.is_success()); + assert_eq!(result.stdout, "/path/to\n"); + } + + #[tokio::test] + async fn test_dirname_just_file() { + let ctx = CommandContext::new(vec!["file.txt".to_string()]); + let result = dirname(ctx).await; + + assert!(result.is_success()); + assert_eq!(result.stdout, ".\n"); + } + + #[tokio::test] + async fn test_dirname_root() { + let ctx = CommandContext::new(vec!["/file.txt".to_string()]); + let result = dirname(ctx).await; + + assert!(result.is_success()); + assert_eq!(result.stdout, "/\n"); + } +} diff --git a/rust/src/commands/echo.rs b/rust/src/commands/echo.rs new file mode 100644 index 0000000..7c3e74c --- /dev/null +++ b/rust/src/commands/echo.rs @@ -0,0 +1,73 @@ +//! Virtual `echo` command implementation + +use crate::commands::CommandContext; +use crate::utils::CommandResult; + +/// Execute the echo command +/// +/// Outputs the arguments separated by spaces, followed by a newline. +/// Supports -n (no newline), -e (interpret escape sequences), and -E (no escape sequences) +pub async fn echo(ctx: CommandContext) -> CommandResult { + let mut no_newline = false; + let mut interpret_escapes = false; + let mut args = ctx.args.iter().peekable(); + + // Parse flags + while let Some(arg) = args.peek() { + if arg.starts_with('-') && arg.len() > 1 && arg.chars().skip(1).all(|c| c == 'n' || c == 'e' || c == 'E') { + let arg = args.next().unwrap(); + for c in arg.chars().skip(1) { + match c { + 'n' => no_newline = true, + 'e' => interpret_escapes = true, + 'E' => interpret_escapes = false, + _ => {} + } + } + } else { + break; + } + } + + // Collect remaining args + let remaining: Vec<_> = args.collect(); + let output = remaining.iter().map(|s| s.as_str()).collect::>().join(" "); + + // Process escape sequences if -e flag is set + let output = if interpret_escapes { + output + .replace("\\n", "\n") + .replace("\\t", "\t") + .replace("\\r", "\r") + .replace("\\\\", "\\") + } else { + output + }; + + if no_newline { + CommandResult::success(output) + } else { + CommandResult::success(format!("{}\n", output)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_echo_simple() { + let ctx = CommandContext::new(vec!["hello".to_string(), "world".to_string()]); + let result = echo(ctx).await; + assert!(result.is_success()); + assert_eq!(result.stdout, "hello world\n"); + } + + #[tokio::test] + async fn test_echo_empty() { + let ctx = CommandContext::new(vec![]); + let result = echo(ctx).await; + assert!(result.is_success()); + assert_eq!(result.stdout, "\n"); + } +} diff --git a/rust/src/commands/env.rs b/rust/src/commands/env.rs new file mode 100644 index 0000000..6491fe3 --- /dev/null +++ b/rust/src/commands/env.rs @@ -0,0 +1,33 @@ +//! Virtual `env` command implementation + +use crate::commands::CommandContext; +use crate::utils::CommandResult; +use std::env; + +/// Execute the env command +/// +/// Displays environment variables. +pub async fn env(_ctx: CommandContext) -> CommandResult { + let mut output = String::new(); + + for (key, value) in env::vars() { + output.push_str(&format!("{}={}\n", key, value)); + } + + CommandResult::success(output) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_env() { + let ctx = CommandContext::new(vec![]); + let result = env(ctx).await; + + assert!(result.is_success()); + // Should contain at least PATH or HOME + assert!(result.stdout.contains("PATH=") || result.stdout.contains("HOME=")); + } +} diff --git a/rust/src/commands/exit.rs b/rust/src/commands/exit.rs new file mode 100644 index 0000000..857186c --- /dev/null +++ b/rust/src/commands/exit.rs @@ -0,0 +1,36 @@ +//! Virtual `exit` command implementation + +use crate::commands::CommandContext; +use crate::utils::CommandResult; + +/// Execute the exit command +/// +/// Exits with the specified code (default 0). +/// Note: This doesn't actually exit the process, it returns the exit code. +pub async fn exit(ctx: CommandContext) -> CommandResult { + let code: i32 = ctx.args + .first() + .and_then(|s| s.parse().ok()) + .unwrap_or(0); + + CommandResult::error_with_code("", code) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_exit_default() { + let ctx = CommandContext::new(vec![]); + let result = exit(ctx).await; + assert_eq!(result.code, 0); + } + + #[tokio::test] + async fn test_exit_with_code() { + let ctx = CommandContext::new(vec!["42".to_string()]); + let result = exit(ctx).await; + assert_eq!(result.code, 42); + } +} diff --git a/rust/src/commands/false.rs b/rust/src/commands/false.rs new file mode 100644 index 0000000..046cbeb --- /dev/null +++ b/rust/src/commands/false.rs @@ -0,0 +1,24 @@ +//! Virtual `false` command implementation + +use crate::commands::CommandContext; +use crate::utils::CommandResult; + +/// Execute the false command +/// +/// Always returns failure (exit code 1). +pub async fn r#false(_ctx: CommandContext) -> CommandResult { + CommandResult::error_with_code("", 1) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_false() { + let ctx = CommandContext::new(vec![]); + let result = r#false(ctx).await; + assert!(!result.is_success()); + assert_eq!(result.code, 1); + } +} diff --git a/rust/src/commands/ls.rs b/rust/src/commands/ls.rs new file mode 100644 index 0000000..9faec6e --- /dev/null +++ b/rust/src/commands/ls.rs @@ -0,0 +1,182 @@ +//! Virtual `ls` command implementation + +use crate::commands::CommandContext; +use crate::utils::{trace_lazy, CommandResult}; +use std::fs; +use std::path::Path; + +/// Execute the ls command +/// +/// Lists directory contents. +pub async fn ls(ctx: CommandContext) -> CommandResult { + // Parse flags + let mut show_all = false; + let mut long_format = false; + let mut paths = Vec::new(); + + for arg in &ctx.args { + if arg == "-a" || arg == "--all" { + show_all = true; + } else if arg == "-l" { + long_format = true; + } else if arg == "-la" || arg == "-al" { + show_all = true; + long_format = true; + } else if arg.starts_with('-') { + if arg.contains('a') { + show_all = true; + } + if arg.contains('l') { + long_format = true; + } + } else { + paths.push(arg.clone()); + } + } + + // Default to current directory + if paths.is_empty() { + paths.push(".".to_string()); + } + + let cwd = ctx.get_cwd(); + let mut outputs = Vec::new(); + + for path_str in paths { + let resolved_path = if Path::new(&path_str).is_absolute() { + Path::new(&path_str).to_path_buf() + } else { + cwd.join(&path_str) + }; + + trace_lazy("VirtualCommand", || { + format!("ls: listing {:?}", resolved_path) + }); + + if !resolved_path.exists() { + return CommandResult::error(format!( + "ls: cannot access '{}': No such file or directory\n", + path_str + )); + } + + if resolved_path.is_file() { + outputs.push(format_entry(&resolved_path, long_format)); + } else { + match fs::read_dir(&resolved_path) { + Ok(entries) => { + let mut entry_strs = Vec::new(); + + for entry in entries { + if let Ok(entry) = entry { + let name = entry.file_name().to_string_lossy().to_string(); + + // Skip hidden files unless -a is specified + if !show_all && name.starts_with('.') { + continue; + } + + if long_format { + entry_strs.push(format_entry(&entry.path(), true)); + } else { + entry_strs.push(name); + } + } + } + + entry_strs.sort(); + outputs.push(entry_strs.join("\n")); + } + Err(e) => { + return CommandResult::error(format!("ls: cannot open '{}': {}\n", path_str, e)); + } + } + } + } + + let output = outputs.join("\n"); + if output.is_empty() { + CommandResult::success_empty() + } else { + CommandResult::success(format!("{}\n", output)) + } +} + +fn format_entry(path: &Path, long_format: bool) -> String { + let name = path.file_name() + .map(|n| n.to_string_lossy().to_string()) + .unwrap_or_else(|| path.display().to_string()); + + if !long_format { + return name; + } + + // Long format: permissions, links, owner, group, size, date, name + let metadata = match fs::metadata(path) { + Ok(m) => m, + Err(_) => return name, + }; + + let file_type = if metadata.is_dir() { "d" } else { "-" }; + let size = metadata.len(); + + // Simplified permissions + let perms = if metadata.is_dir() { + "drwxr-xr-x" + } else { + "-rw-r--r--" + }; + + format!("{} {:>8} {}", perms, size, name) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::tempdir; + + #[tokio::test] + async fn test_ls_current_dir() { + let ctx = CommandContext::new(vec![]); + let result = ls(ctx).await; + assert!(result.is_success()); + } + + #[tokio::test] + async fn test_ls_with_path() { + let temp = tempdir().unwrap(); + fs::write(temp.path().join("file.txt"), "test").unwrap(); + + let ctx = CommandContext::new(vec![ + temp.path().to_string_lossy().to_string() + ]); + let result = ls(ctx).await; + + assert!(result.is_success()); + assert!(result.stdout.contains("file.txt")); + } + + #[tokio::test] + async fn test_ls_hidden_files() { + let temp = tempdir().unwrap(); + fs::write(temp.path().join(".hidden"), "test").unwrap(); + fs::write(temp.path().join("visible"), "test").unwrap(); + + // Without -a + let ctx = CommandContext::new(vec![ + temp.path().to_string_lossy().to_string() + ]); + let result = ls(ctx).await; + assert!(!result.stdout.contains(".hidden")); + assert!(result.stdout.contains("visible")); + + // With -a + let ctx = CommandContext::new(vec![ + "-a".to_string(), + temp.path().to_string_lossy().to_string(), + ]); + let result = ls(ctx).await; + assert!(result.stdout.contains(".hidden")); + assert!(result.stdout.contains("visible")); + } +} diff --git a/rust/src/commands/mkdir.rs b/rust/src/commands/mkdir.rs new file mode 100644 index 0000000..4605222 --- /dev/null +++ b/rust/src/commands/mkdir.rs @@ -0,0 +1,98 @@ +//! Virtual `mkdir` command implementation + +use crate::commands::CommandContext; +use crate::utils::{trace_lazy, CommandResult, VirtualUtils}; +use std::fs; + +/// Execute the mkdir command +/// +/// Creates directories. +pub async fn mkdir(ctx: CommandContext) -> CommandResult { + if ctx.args.is_empty() { + return VirtualUtils::missing_operand_error("mkdir"); + } + + // Check for -p flag + let mut create_parents = false; + let mut dirs = Vec::new(); + + for arg in &ctx.args { + if arg == "-p" { + create_parents = true; + } else if arg.starts_with('-') { + // Skip other flags + } else { + dirs.push(arg.clone()); + } + } + + if dirs.is_empty() { + return VirtualUtils::missing_operand_error("mkdir"); + } + + let cwd = ctx.get_cwd(); + + for dir in dirs { + let resolved_path = VirtualUtils::resolve_path(&dir, Some(&cwd)); + + trace_lazy("VirtualCommand", || { + format!("mkdir: creating directory {:?}, parents: {}", resolved_path, create_parents) + }); + + let result = if create_parents { + fs::create_dir_all(&resolved_path) + } else { + fs::create_dir(&resolved_path) + }; + + if let Err(e) = result { + return CommandResult::error(format!("mkdir: cannot create directory '{}': {}\n", dir, e)); + } + } + + CommandResult::success_empty() +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::tempdir; + + #[tokio::test] + async fn test_mkdir_simple() { + let temp = tempdir().unwrap(); + let new_dir = temp.path().join("new_directory"); + + let ctx = CommandContext::new(vec![ + new_dir.to_string_lossy().to_string() + ]); + let result = mkdir(ctx).await; + + assert!(result.is_success()); + assert!(new_dir.exists()); + } + + #[tokio::test] + async fn test_mkdir_with_parents() { + let temp = tempdir().unwrap(); + let nested_dir = temp.path().join("a/b/c/d"); + + let ctx = CommandContext::new(vec![ + "-p".to_string(), + nested_dir.to_string_lossy().to_string(), + ]); + let result = mkdir(ctx).await; + + assert!(result.is_success()); + assert!(nested_dir.exists()); + } + + #[tokio::test] + async fn test_mkdir_missing_operand() { + let ctx = CommandContext::new(vec![]); + let result = mkdir(ctx).await; + + assert!(!result.is_success()); + assert!(result.stderr.contains("missing operand")); + } +} diff --git a/rust/src/commands/mod.rs b/rust/src/commands/mod.rs new file mode 100644 index 0000000..5f2ba42 --- /dev/null +++ b/rust/src/commands/mod.rs @@ -0,0 +1,200 @@ +//! Virtual command implementations +//! +//! This module contains implementations of shell commands that run in-process +//! without spawning external processes. These provide faster execution and +//! consistent behavior across platforms. + +mod cat; +mod cd; +mod echo; +mod pwd; +mod sleep; +mod r#true; +mod r#false; +mod mkdir; +mod rm; +mod touch; +mod ls; +mod cp; +mod mv; +mod basename; +mod dirname; +mod env; +mod exit; +mod which; +mod yes; +mod seq; +mod test; + +pub use cat::cat; +pub use cd::cd; +pub use echo::echo; +pub use pwd::pwd; +pub use sleep::sleep; +pub use r#true::r#true; +pub use r#false::r#false; +pub use mkdir::mkdir; +pub use rm::rm; +pub use touch::touch; +pub use ls::ls; +pub use cp::cp; +pub use mv::mv; +pub use basename::basename; +pub use dirname::dirname; +pub use env::env; +pub use exit::exit; +pub use which::which; +pub use yes::yes; +pub use seq::seq; +pub use test::test; + +use crate::utils::CommandResult; +use std::collections::HashMap; +use std::path::Path; +use tokio::sync::mpsc; + +/// Context for virtual command execution +pub struct CommandContext { + /// Command arguments (excluding the command name) + pub args: Vec, + /// Standard input content + pub stdin: Option, + /// Current working directory + pub cwd: Option, + /// Environment variables + pub env: Option>, + /// Channel to send streaming output + pub output_tx: Option>, + /// Cancellation check function + pub is_cancelled: Option bool + Send + Sync>>, +} + +impl std::fmt::Debug for CommandContext { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CommandContext") + .field("args", &self.args) + .field("stdin", &self.stdin) + .field("cwd", &self.cwd) + .field("env", &self.env) + .field("output_tx", &self.output_tx.is_some()) + .field("is_cancelled", &self.is_cancelled.is_some()) + .finish() + } +} + +/// A chunk of streaming output +#[derive(Debug, Clone)] +pub enum StreamChunk { + Stdout(String), + Stderr(String), +} + +impl CommandContext { + /// Create a new command context with arguments + pub fn new(args: Vec) -> Self { + CommandContext { + args, + stdin: None, + cwd: None, + env: None, + output_tx: None, + is_cancelled: None, + } + } + + /// Check if the command has been cancelled + pub fn is_cancelled(&self) -> bool { + self.is_cancelled + .as_ref() + .map(|f| f()) + .unwrap_or(false) + } + + /// Get the current working directory + pub fn get_cwd(&self) -> std::path::PathBuf { + self.cwd.clone().unwrap_or_else(|| { + std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("/")) + }) + } +} + +/// Type for virtual command handler functions +pub type VirtualCommandHandler = fn(CommandContext) -> std::pin::Pin + Send>>; + +/// Registry of virtual commands +pub struct VirtualCommandRegistry { + commands: HashMap, +} + +impl Default for VirtualCommandRegistry { + fn default() -> Self { + Self::new() + } +} + +impl VirtualCommandRegistry { + /// Create a new empty registry + pub fn new() -> Self { + VirtualCommandRegistry { + commands: HashMap::new(), + } + } + + /// Create a registry with all built-in commands registered + pub fn with_builtins() -> Self { + let mut registry = Self::new(); + registry.register_builtins(); + registry + } + + /// Register a virtual command + pub fn register(&mut self, name: &str, handler: VirtualCommandHandler) { + self.commands.insert(name.to_string(), handler); + } + + /// Unregister a virtual command + pub fn unregister(&mut self, name: &str) -> bool { + self.commands.remove(name).is_some() + } + + /// Get a virtual command handler + pub fn get(&self, name: &str) -> Option<&VirtualCommandHandler> { + self.commands.get(name) + } + + /// Check if a command is registered + pub fn contains(&self, name: &str) -> bool { + self.commands.contains_key(name) + } + + /// List all registered command names + pub fn list(&self) -> Vec<&str> { + self.commands.keys().map(|s| s.as_str()).collect() + } + + /// Register all built-in commands + pub fn register_builtins(&mut self) { + // Note: These are placeholder registrations - actual async handlers + // would need proper wrapper functions + // The actual commands are available as standalone functions + } +} + +/// Global virtual commands enabled flag +static VIRTUAL_COMMANDS_ENABLED: std::sync::atomic::AtomicBool = + std::sync::atomic::AtomicBool::new(true); + +/// Enable virtual commands +pub fn enable_virtual_commands() { + VIRTUAL_COMMANDS_ENABLED.store(true, std::sync::atomic::Ordering::SeqCst); +} + +/// Disable virtual commands +pub fn disable_virtual_commands() { + VIRTUAL_COMMANDS_ENABLED.store(false, std::sync::atomic::Ordering::SeqCst); +} + +/// Check if virtual commands are enabled +pub fn are_virtual_commands_enabled() -> bool { + VIRTUAL_COMMANDS_ENABLED.load(std::sync::atomic::Ordering::SeqCst) +} diff --git a/rust/src/commands/mv.rs b/rust/src/commands/mv.rs new file mode 100644 index 0000000..8485f6c --- /dev/null +++ b/rust/src/commands/mv.rs @@ -0,0 +1,180 @@ +//! Virtual `mv` command implementation + +use crate::commands::CommandContext; +use crate::utils::{trace_lazy, CommandResult, VirtualUtils}; +use std::fs; + +/// Execute the mv command +/// +/// Moves (renames) files and directories. +pub async fn mv(ctx: CommandContext) -> CommandResult { + if ctx.args.len() < 2 { + return VirtualUtils::invalid_argument_error("mv", "missing file operand"); + } + + // Parse flags (currently just skip them) + let mut paths = Vec::new(); + + for arg in &ctx.args { + if !arg.starts_with('-') { + paths.push(arg.clone()); + } + } + + if paths.len() < 2 { + return VirtualUtils::invalid_argument_error("mv", "missing destination file operand"); + } + + let cwd = ctx.get_cwd(); + let dest = paths.pop().unwrap(); + let dest_path = VirtualUtils::resolve_path(&dest, Some(&cwd)); + + // If multiple sources or dest is a directory, move into the directory + let dest_is_dir = dest_path.is_dir(); + let multiple_sources = paths.len() > 1; + + if multiple_sources && !dest_is_dir { + return CommandResult::error(format!( + "mv: target '{}' is not a directory\n", + dest + )); + } + + for source in paths { + let source_path = VirtualUtils::resolve_path(&source, Some(&cwd)); + + trace_lazy("VirtualCommand", || { + format!("mv: moving {:?} to {:?}", source_path, dest_path) + }); + + if !source_path.exists() { + return CommandResult::error(format!( + "mv: cannot stat '{}': No such file or directory\n", + source + )); + } + + let final_dest = if dest_is_dir { + dest_path.join(source_path.file_name().unwrap_or_default()) + } else { + dest_path.clone() + }; + + // Try rename first (fastest if on same filesystem) + match fs::rename(&source_path, &final_dest) { + Ok(()) => continue, + Err(e) => { + // If rename fails (e.g., cross-filesystem), try copy + delete + if e.kind() == std::io::ErrorKind::CrossesDevices + || e.kind() == std::io::ErrorKind::Other + { + if source_path.is_dir() { + if let Err(e) = copy_and_remove_dir(&source_path, &final_dest) { + return CommandResult::error(format!( + "mv: cannot move '{}': {}\n", + source, e + )); + } + } else { + if let Err(e) = fs::copy(&source_path, &final_dest) { + return CommandResult::error(format!( + "mv: cannot move '{}': {}\n", + source, e + )); + } + if let Err(e) = fs::remove_file(&source_path) { + return CommandResult::error(format!( + "mv: cannot remove '{}': {}\n", + source, e + )); + } + } + } else { + return CommandResult::error(format!( + "mv: cannot move '{}': {}\n", + source, e + )); + } + } + } + } + + CommandResult::success_empty() +} + +fn copy_and_remove_dir(src: &std::path::Path, dst: &std::path::Path) -> std::io::Result<()> { + fs::create_dir_all(dst)?; + + for entry in fs::read_dir(src)? { + let entry = entry?; + let entry_path = entry.path(); + let dest_path = dst.join(entry.file_name()); + + if entry_path.is_dir() { + copy_and_remove_dir(&entry_path, &dest_path)?; + } else { + fs::copy(&entry_path, &dest_path)?; + } + } + + fs::remove_dir_all(src)?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::tempdir; + + #[tokio::test] + async fn test_mv_file() { + let temp = tempdir().unwrap(); + let src = temp.path().join("source.txt"); + let dst = temp.path().join("dest.txt"); + fs::write(&src, "test content").unwrap(); + + let ctx = CommandContext::new(vec![ + src.to_string_lossy().to_string(), + dst.to_string_lossy().to_string(), + ]); + let result = mv(ctx).await; + + assert!(result.is_success()); + assert!(!src.exists()); + assert!(dst.exists()); + assert_eq!(fs::read_to_string(&dst).unwrap(), "test content"); + } + + #[tokio::test] + async fn test_mv_directory() { + let temp = tempdir().unwrap(); + let src_dir = temp.path().join("src_dir"); + let dst_dir = temp.path().join("dst_dir"); + + fs::create_dir(&src_dir).unwrap(); + fs::write(src_dir.join("file.txt"), "test").unwrap(); + + let ctx = CommandContext::new(vec![ + src_dir.to_string_lossy().to_string(), + dst_dir.to_string_lossy().to_string(), + ]); + let result = mv(ctx).await; + + assert!(result.is_success()); + assert!(!src_dir.exists()); + assert!(dst_dir.join("file.txt").exists()); + } + + #[tokio::test] + async fn test_mv_nonexistent() { + let temp = tempdir().unwrap(); + + let ctx = CommandContext::new(vec![ + "/nonexistent/file".to_string(), + temp.path().join("dest").to_string_lossy().to_string(), + ]); + let result = mv(ctx).await; + + assert!(!result.is_success()); + } +} diff --git a/rust/src/commands/pwd.rs b/rust/src/commands/pwd.rs new file mode 100644 index 0000000..acf0fee --- /dev/null +++ b/rust/src/commands/pwd.rs @@ -0,0 +1,28 @@ +//! Virtual `pwd` command implementation + +use crate::commands::CommandContext; +use crate::utils::CommandResult; +use std::env; + +/// Execute the pwd command +/// +/// Prints the current working directory. +pub async fn pwd(_ctx: CommandContext) -> CommandResult { + match env::current_dir() { + Ok(path) => CommandResult::success(format!("{}\n", path.display())), + Err(e) => CommandResult::error(format!("pwd: {}\n", e)), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_pwd() { + let ctx = CommandContext::new(vec![]); + let result = pwd(ctx).await; + assert!(result.is_success()); + assert!(!result.stdout.is_empty()); + } +} diff --git a/rust/src/commands/rm.rs b/rust/src/commands/rm.rs new file mode 100644 index 0000000..42b10ff --- /dev/null +++ b/rust/src/commands/rm.rs @@ -0,0 +1,150 @@ +//! Virtual `rm` command implementation + +use crate::commands::CommandContext; +use crate::utils::{trace_lazy, CommandResult, VirtualUtils}; +use std::fs; + +/// Execute the rm command +/// +/// Removes files and directories. +pub async fn rm(ctx: CommandContext) -> CommandResult { + if ctx.args.is_empty() { + return VirtualUtils::missing_operand_error("rm"); + } + + // Parse flags + let mut recursive = false; + let mut force = false; + let mut paths = Vec::new(); + + for arg in &ctx.args { + if arg == "-r" || arg == "-R" || arg == "--recursive" { + recursive = true; + } else if arg == "-f" || arg == "--force" { + force = true; + } else if arg == "-rf" || arg == "-fr" { + recursive = true; + force = true; + } else if arg.starts_with('-') { + // Check for combined flags like -rf + if arg.contains('r') || arg.contains('R') { + recursive = true; + } + if arg.contains('f') { + force = true; + } + } else { + paths.push(arg.clone()); + } + } + + if paths.is_empty() { + return VirtualUtils::missing_operand_error("rm"); + } + + let cwd = ctx.get_cwd(); + + for path_str in paths { + let resolved_path = VirtualUtils::resolve_path(&path_str, Some(&cwd)); + + trace_lazy("VirtualCommand", || { + format!("rm: removing {:?}, recursive: {}, force: {}", resolved_path, recursive, force) + }); + + if !resolved_path.exists() { + if !force { + return CommandResult::error(format!( + "rm: cannot remove '{}': No such file or directory\n", + path_str + )); + } + continue; + } + + let result = if resolved_path.is_dir() { + if recursive { + fs::remove_dir_all(&resolved_path) + } else { + return CommandResult::error(format!( + "rm: cannot remove '{}': Is a directory\n", + path_str + )); + } + } else { + fs::remove_file(&resolved_path) + }; + + if let Err(e) = result { + if !force { + return CommandResult::error(format!("rm: cannot remove '{}': {}\n", path_str, e)); + } + } + } + + CommandResult::success_empty() +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + use tempfile::{tempdir, NamedTempFile}; + + #[tokio::test] + async fn test_rm_file() { + let mut temp = NamedTempFile::new().unwrap(); + writeln!(temp, "test").unwrap(); + let path = temp.path().to_path_buf(); + + // Keep the file but get the path + let path_str = path.to_string_lossy().to_string(); + drop(temp); + + // Create file again + fs::write(&path, "test").unwrap(); + + let ctx = CommandContext::new(vec![path_str.clone()]); + let result = rm(ctx).await; + + assert!(result.is_success()); + assert!(!path.exists()); + } + + #[tokio::test] + async fn test_rm_directory_recursive() { + let temp = tempdir().unwrap(); + let dir = temp.path().join("subdir"); + fs::create_dir(&dir).unwrap(); + fs::write(dir.join("file.txt"), "test").unwrap(); + + let ctx = CommandContext::new(vec![ + "-r".to_string(), + dir.to_string_lossy().to_string(), + ]); + let result = rm(ctx).await; + + assert!(result.is_success()); + assert!(!dir.exists()); + } + + #[tokio::test] + async fn test_rm_nonexistent_force() { + let ctx = CommandContext::new(vec![ + "-f".to_string(), + "/nonexistent/file/12345".to_string(), + ]); + let result = rm(ctx).await; + + assert!(result.is_success()); + } + + #[tokio::test] + async fn test_rm_nonexistent_no_force() { + let ctx = CommandContext::new(vec![ + "/nonexistent/file/12345".to_string() + ]); + let result = rm(ctx).await; + + assert!(!result.is_success()); + } +} diff --git a/rust/src/commands/seq.rs b/rust/src/commands/seq.rs new file mode 100644 index 0000000..a88af7c --- /dev/null +++ b/rust/src/commands/seq.rs @@ -0,0 +1,179 @@ +//! Virtual `seq` command implementation + +use crate::commands::CommandContext; +use crate::utils::{CommandResult, VirtualUtils}; + +/// Execute the seq command +/// +/// Prints a sequence of numbers. +pub async fn seq(ctx: CommandContext) -> CommandResult { + if ctx.args.is_empty() { + return VirtualUtils::missing_operand_error("seq"); + } + + let (first, increment, last) = match ctx.args.len() { + 1 => { + let last: f64 = match ctx.args[0].parse() { + Ok(n) => n, + Err(_) => { + return CommandResult::error(format!( + "seq: invalid floating point argument: '{}'\n", + ctx.args[0] + )); + } + }; + (1.0, 1.0, last) + } + 2 => { + let first: f64 = match ctx.args[0].parse() { + Ok(n) => n, + Err(_) => { + return CommandResult::error(format!( + "seq: invalid floating point argument: '{}'\n", + ctx.args[0] + )); + } + }; + let last: f64 = match ctx.args[1].parse() { + Ok(n) => n, + Err(_) => { + return CommandResult::error(format!( + "seq: invalid floating point argument: '{}'\n", + ctx.args[1] + )); + } + }; + (first, 1.0, last) + } + _ => { + let first: f64 = match ctx.args[0].parse() { + Ok(n) => n, + Err(_) => { + return CommandResult::error(format!( + "seq: invalid floating point argument: '{}'\n", + ctx.args[0] + )); + } + }; + let increment: f64 = match ctx.args[1].parse() { + Ok(n) => n, + Err(_) => { + return CommandResult::error(format!( + "seq: invalid floating point argument: '{}'\n", + ctx.args[1] + )); + } + }; + let last: f64 = match ctx.args[2].parse() { + Ok(n) => n, + Err(_) => { + return CommandResult::error(format!( + "seq: invalid floating point argument: '{}'\n", + ctx.args[2] + )); + } + }; + (first, increment, last) + } + }; + + if increment == 0.0 { + return CommandResult::error("seq: zero increment\n"); + } + + let mut output = String::new(); + let mut current = first; + + if increment > 0.0 { + while current <= last { + if ctx.is_cancelled() { + return CommandResult::error_with_code("", 130); + } + + // Format as integer if possible + if current.fract() == 0.0 { + output.push_str(&format!("{}\n", current as i64)); + } else { + output.push_str(&format!("{}\n", current)); + } + current += increment; + } + } else { + while current >= last { + if ctx.is_cancelled() { + return CommandResult::error_with_code("", 130); + } + + if current.fract() == 0.0 { + output.push_str(&format!("{}\n", current as i64)); + } else { + output.push_str(&format!("{}\n", current)); + } + current += increment; + } + } + + CommandResult::success(output) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_seq_single_arg() { + let ctx = CommandContext::new(vec!["5".to_string()]); + let result = seq(ctx).await; + + assert!(result.is_success()); + assert_eq!(result.stdout, "1\n2\n3\n4\n5\n"); + } + + #[tokio::test] + async fn test_seq_two_args() { + let ctx = CommandContext::new(vec!["3".to_string(), "7".to_string()]); + let result = seq(ctx).await; + + assert!(result.is_success()); + assert_eq!(result.stdout, "3\n4\n5\n6\n7\n"); + } + + #[tokio::test] + async fn test_seq_three_args() { + let ctx = CommandContext::new(vec![ + "2".to_string(), + "2".to_string(), + "8".to_string(), + ]); + let result = seq(ctx).await; + + assert!(result.is_success()); + assert_eq!(result.stdout, "2\n4\n6\n8\n"); + } + + #[tokio::test] + async fn test_seq_descending() { + let ctx = CommandContext::new(vec![ + "5".to_string(), + "-1".to_string(), + "1".to_string(), + ]); + let result = seq(ctx).await; + + assert!(result.is_success()); + assert_eq!(result.stdout, "5\n4\n3\n2\n1\n"); + } + + #[tokio::test] + async fn test_seq_zero_increment() { + let ctx = CommandContext::new(vec![ + "1".to_string(), + "0".to_string(), + "5".to_string(), + ]); + let result = seq(ctx).await; + + assert!(!result.is_success()); + assert!(result.stderr.contains("zero increment")); + } +} diff --git a/rust/src/commands/sleep.rs b/rust/src/commands/sleep.rs new file mode 100644 index 0000000..60adfd6 --- /dev/null +++ b/rust/src/commands/sleep.rs @@ -0,0 +1,97 @@ +//! Virtual `sleep` command implementation + +use crate::commands::CommandContext; +use crate::utils::{trace_lazy, CommandResult}; +use tokio::time::{sleep as tokio_sleep, Duration}; + +/// Execute the sleep command +/// +/// Pauses for the specified number of seconds. +pub async fn sleep(ctx: CommandContext) -> CommandResult { + let seconds_str = ctx.args.first().map(|s| s.as_str()).unwrap_or("0"); + + let seconds: f64 = match seconds_str.parse() { + Ok(s) => s, + Err(_) => { + return CommandResult::error(format!( + "sleep: invalid time interval '{}'\n", + seconds_str + )); + } + }; + + if seconds < 0.0 { + return CommandResult::error(format!( + "sleep: invalid time interval '{}'\n", + seconds_str + )); + } + + trace_lazy("VirtualCommand", || { + format!("sleep: starting {} seconds", seconds) + }); + + let duration = Duration::from_secs_f64(seconds); + + // Check for cancellation during sleep + tokio::select! { + _ = tokio_sleep(duration) => { + trace_lazy("VirtualCommand", || { + format!("sleep: completed naturally after {} seconds", seconds) + }); + CommandResult::success_empty() + } + _ = async { + loop { + tokio::time::sleep(Duration::from_millis(100)).await; + if ctx.is_cancelled() { + break; + } + } + } => { + trace_lazy("VirtualCommand", || { + format!("sleep: cancelled after partial sleep") + }); + CommandResult::error_with_code("", 130) // SIGINT exit code + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::Instant; + + #[tokio::test] + async fn test_sleep_short() { + let ctx = CommandContext::new(vec!["0.1".to_string()]); + let start = Instant::now(); + let result = sleep(ctx).await; + let elapsed = start.elapsed(); + + assert!(result.is_success()); + assert!(elapsed >= Duration::from_millis(100)); + assert!(elapsed < Duration::from_millis(200)); + } + + #[tokio::test] + async fn test_sleep_zero() { + let ctx = CommandContext::new(vec!["0".to_string()]); + let result = sleep(ctx).await; + assert!(result.is_success()); + } + + #[tokio::test] + async fn test_sleep_invalid() { + let ctx = CommandContext::new(vec!["invalid".to_string()]); + let result = sleep(ctx).await; + assert!(!result.is_success()); + } + + #[tokio::test] + async fn test_sleep_negative() { + let ctx = CommandContext::new(vec!["-1".to_string()]); + let result = sleep(ctx).await; + assert!(!result.is_success()); + } +} diff --git a/rust/src/commands/test.rs b/rust/src/commands/test.rs new file mode 100644 index 0000000..b6c273c --- /dev/null +++ b/rust/src/commands/test.rs @@ -0,0 +1,204 @@ +//! Virtual `test` command implementation + +use crate::commands::CommandContext; +use crate::utils::CommandResult; +use std::fs; +use std::path::Path; + +/// Execute the test command +/// +/// Evaluates conditional expressions. +pub async fn test(ctx: CommandContext) -> CommandResult { + if ctx.args.is_empty() { + return CommandResult::error_with_code("", 1); + } + + let result = evaluate_expression(&ctx.args); + + if result { + CommandResult::success_empty() + } else { + CommandResult::error_with_code("", 1) + } +} + +fn evaluate_expression(args: &[String]) -> bool { + if args.is_empty() { + return false; + } + + // Handle unary operators + if args.len() == 2 { + let op = &args[0]; + let arg = &args[1]; + + return match op.as_str() { + "-e" => Path::new(arg).exists(), + "-f" => Path::new(arg).is_file(), + "-d" => Path::new(arg).is_dir(), + "-r" => { + // Check if readable (simplified) + fs::metadata(arg).is_ok() + } + "-w" => { + // Check if writable (simplified) + fs::metadata(arg).map(|m| !m.permissions().readonly()).unwrap_or(false) + } + "-x" => { + // Check if executable (simplified - Unix only) + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + fs::metadata(arg) + .map(|m| m.permissions().mode() & 0o111 != 0) + .unwrap_or(false) + } + #[cfg(not(unix))] + { + Path::new(arg).exists() + } + } + "-s" => { + // Check if file has size > 0 + fs::metadata(arg).map(|m| m.len() > 0).unwrap_or(false) + } + "-z" => arg.is_empty(), + "-n" => !arg.is_empty(), + "!" => !evaluate_expression(&args[1..]), + _ => false, + }; + } + + // Handle binary operators + if args.len() == 3 { + let left = &args[0]; + let op = &args[1]; + let right = &args[2]; + + return match op.as_str() { + "=" | "==" => left == right, + "!=" => left != right, + "-eq" => { + let l: i64 = left.parse().unwrap_or(0); + let r: i64 = right.parse().unwrap_or(0); + l == r + } + "-ne" => { + let l: i64 = left.parse().unwrap_or(0); + let r: i64 = right.parse().unwrap_or(0); + l != r + } + "-lt" => { + let l: i64 = left.parse().unwrap_or(0); + let r: i64 = right.parse().unwrap_or(0); + l < r + } + "-le" => { + let l: i64 = left.parse().unwrap_or(0); + let r: i64 = right.parse().unwrap_or(0); + l <= r + } + "-gt" => { + let l: i64 = left.parse().unwrap_or(0); + let r: i64 = right.parse().unwrap_or(0); + l > r + } + "-ge" => { + let l: i64 = left.parse().unwrap_or(0); + let r: i64 = right.parse().unwrap_or(0); + l >= r + } + _ => false, + }; + } + + // Single argument: true if non-empty + if args.len() == 1 { + return !args[0].is_empty(); + } + + false +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::tempdir; + + #[tokio::test] + async fn test_file_exists() { + let temp = tempdir().unwrap(); + let file = temp.path().join("test.txt"); + fs::write(&file, "test").unwrap(); + + let ctx = CommandContext::new(vec![ + "-e".to_string(), + file.to_string_lossy().to_string(), + ]); + let result = test(ctx).await; + assert!(result.is_success()); + } + + #[tokio::test] + async fn test_file_not_exists() { + let ctx = CommandContext::new(vec![ + "-e".to_string(), + "/nonexistent/file/12345".to_string(), + ]); + let result = test(ctx).await; + assert!(!result.is_success()); + } + + #[tokio::test] + async fn test_string_equality() { + let ctx = CommandContext::new(vec![ + "hello".to_string(), + "=".to_string(), + "hello".to_string(), + ]); + let result = test(ctx).await; + assert!(result.is_success()); + } + + #[tokio::test] + async fn test_string_inequality() { + let ctx = CommandContext::new(vec![ + "hello".to_string(), + "!=".to_string(), + "world".to_string(), + ]); + let result = test(ctx).await; + assert!(result.is_success()); + } + + #[tokio::test] + async fn test_numeric_comparison() { + let ctx = CommandContext::new(vec![ + "5".to_string(), + "-gt".to_string(), + "3".to_string(), + ]); + let result = test(ctx).await; + assert!(result.is_success()); + } + + #[tokio::test] + async fn test_empty_string() { + let ctx = CommandContext::new(vec![ + "-z".to_string(), + "".to_string(), + ]); + let result = test(ctx).await; + assert!(result.is_success()); + } + + #[tokio::test] + async fn test_non_empty_string() { + let ctx = CommandContext::new(vec![ + "-n".to_string(), + "hello".to_string(), + ]); + let result = test(ctx).await; + assert!(result.is_success()); + } +} diff --git a/rust/src/commands/touch.rs b/rust/src/commands/touch.rs new file mode 100644 index 0000000..5f15bf5 --- /dev/null +++ b/rust/src/commands/touch.rs @@ -0,0 +1,99 @@ +//! Virtual `touch` command implementation + +use crate::commands::CommandContext; +use crate::utils::{trace_lazy, CommandResult, VirtualUtils}; +use std::fs::{self, OpenOptions}; +use std::time::SystemTime; + +/// Execute the touch command +/// +/// Updates file timestamps or creates empty files. +pub async fn touch(ctx: CommandContext) -> CommandResult { + if ctx.args.is_empty() { + return VirtualUtils::missing_operand_error("touch"); + } + + let cwd = ctx.get_cwd(); + + for file in &ctx.args { + if file.starts_with('-') { + // Skip flags for now + continue; + } + + let resolved_path = VirtualUtils::resolve_path(file, Some(&cwd)); + + trace_lazy("VirtualCommand", || { + format!("touch: touching {:?}", resolved_path) + }); + + if resolved_path.exists() { + // Update modification time + let now = SystemTime::now(); + if let Err(e) = filetime::set_file_mtime(&resolved_path, filetime::FileTime::from_system_time(now)) { + // Fallback: try to just open and close the file + if let Err(e2) = OpenOptions::new().write(true).open(&resolved_path) { + return CommandResult::error(format!("touch: cannot touch '{}': {}\n", file, e2)); + } + } + } else { + // Create the file + if let Some(parent) = resolved_path.parent() { + if !parent.exists() { + if let Err(e) = fs::create_dir_all(parent) { + return CommandResult::error(format!("touch: cannot touch '{}': {}\n", file, e)); + } + } + } + + if let Err(e) = fs::File::create(&resolved_path) { + return CommandResult::error(format!("touch: cannot touch '{}': {}\n", file, e)); + } + } + } + + CommandResult::success_empty() +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::tempdir; + + #[tokio::test] + async fn test_touch_new_file() { + let temp = tempdir().unwrap(); + let new_file = temp.path().join("new_file.txt"); + + let ctx = CommandContext::new(vec![ + new_file.to_string_lossy().to_string() + ]); + let result = touch(ctx).await; + + assert!(result.is_success()); + assert!(new_file.exists()); + } + + #[tokio::test] + async fn test_touch_existing_file() { + let temp = tempdir().unwrap(); + let file = temp.path().join("existing.txt"); + fs::write(&file, "test").unwrap(); + + let ctx = CommandContext::new(vec![ + file.to_string_lossy().to_string() + ]); + let result = touch(ctx).await; + + assert!(result.is_success()); + } + + #[tokio::test] + async fn test_touch_missing_operand() { + let ctx = CommandContext::new(vec![]); + let result = touch(ctx).await; + + assert!(!result.is_success()); + assert!(result.stderr.contains("missing operand")); + } +} diff --git a/rust/src/commands/true.rs b/rust/src/commands/true.rs new file mode 100644 index 0000000..5441029 --- /dev/null +++ b/rust/src/commands/true.rs @@ -0,0 +1,24 @@ +//! Virtual `true` command implementation + +use crate::commands::CommandContext; +use crate::utils::CommandResult; + +/// Execute the true command +/// +/// Always returns success (exit code 0). +pub async fn r#true(_ctx: CommandContext) -> CommandResult { + CommandResult::success_empty() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_true() { + let ctx = CommandContext::new(vec![]); + let result = r#true(ctx).await; + assert!(result.is_success()); + assert_eq!(result.code, 0); + } +} diff --git a/rust/src/commands/which.rs b/rust/src/commands/which.rs new file mode 100644 index 0000000..2cbc0ff --- /dev/null +++ b/rust/src/commands/which.rs @@ -0,0 +1,87 @@ +//! Virtual `which` command implementation + +use crate::commands::CommandContext; +use crate::utils::{CommandResult, VirtualUtils}; + +/// List of virtual (shell builtin) commands +const VIRTUAL_COMMANDS: &[&str] = &[ + "echo", "pwd", "cd", "true", "false", "sleep", "cat", "ls", "mkdir", "rm", + "touch", "cp", "mv", "basename", "dirname", "env", "exit", "which", "yes", + "seq", "test", +]; + +/// Execute the which command +/// +/// Locates commands in the PATH or identifies shell builtins. +pub async fn which(ctx: CommandContext) -> CommandResult { + if ctx.args.is_empty() { + return VirtualUtils::missing_operand_error("which"); + } + + let mut output = String::new(); + let mut errors = String::new(); + let mut found_all = true; + + for cmd in &ctx.args { + if cmd.starts_with('-') { + continue; + } + + // Check if it's a virtual/builtin command + if VIRTUAL_COMMANDS.contains(&cmd.as_str()) { + output.push_str(&format!("{}: shell builtin\n", cmd)); + } else { + // Try to find in PATH + match which::which(cmd) { + Ok(path) => { + output.push_str(&format!("{}\n", path.display())); + } + Err(_) => { + found_all = false; + errors.push_str(&format!("which: no {} in PATH\n", cmd)); + } + } + } + } + + if output.is_empty() { + CommandResult { + stdout: String::new(), + stderr: errors, + code: 1, + } + } else if !found_all { + // Some commands found, some not + CommandResult { + stdout: output, + stderr: errors, + code: 1, + } + } else { + CommandResult::success(output) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_which_existing_command() { + // 'sh' should exist on most Unix systems + let ctx = CommandContext::new(vec!["sh".to_string()]); + let result = which(ctx).await; + + // May or may not find sh depending on PATH + // Just check it doesn't panic + assert!(result.code == 0 || result.code == 1); + } + + #[tokio::test] + async fn test_which_nonexistent_command() { + let ctx = CommandContext::new(vec!["nonexistent_command_12345".to_string()]); + let result = which(ctx).await; + + assert_eq!(result.code, 1); + } +} diff --git a/rust/src/commands/yes.rs b/rust/src/commands/yes.rs new file mode 100644 index 0000000..9e53f87 --- /dev/null +++ b/rust/src/commands/yes.rs @@ -0,0 +1,99 @@ +//! Virtual `yes` command implementation + +use crate::commands::{CommandContext, StreamChunk}; +use crate::utils::{trace_lazy, CommandResult}; +use tokio::time::Duration; + +/// Execute the yes command +/// +/// Outputs a string repeatedly until cancelled. +pub async fn yes(ctx: CommandContext) -> CommandResult { + let output_str = if ctx.args.is_empty() { + "y".to_string() + } else { + ctx.args.join(" ") + }; + + let line = format!("{}\n", output_str); + + trace_lazy("VirtualCommand", || { + format!("yes: starting with output '{}'", output_str) + }); + + // If we have a streaming output channel, use it + if let Some(ref tx) = ctx.output_tx { + loop { + if ctx.is_cancelled() { + trace_lazy("VirtualCommand", || "yes: cancelled".to_string()); + return CommandResult::error_with_code("", 130); + } + + if tx.send(StreamChunk::Stdout(line.clone())).await.is_err() { + // Channel closed + break; + } + + // Small delay to prevent overwhelming + tokio::time::sleep(Duration::from_micros(100)).await; + } + } else { + // Without streaming, just output a few lines and return + // This is a safety measure to prevent infinite output + let mut output = String::new(); + let max_iterations = 1000; + + for _ in 0..max_iterations { + if ctx.is_cancelled() { + trace_lazy("VirtualCommand", || "yes: cancelled".to_string()); + return CommandResult::error_with_code("", 130); + } + output.push_str(&line); + } + + return CommandResult::success(output); + } + + CommandResult::error_with_code("", 130) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::{AtomicBool, Ordering}; + use std::sync::Arc; + + #[tokio::test] + async fn test_yes_with_cancellation() { + let cancelled = Arc::new(AtomicBool::new(false)); + let cancelled_clone = cancelled.clone(); + + let mut ctx = CommandContext::new(vec![]); + ctx.is_cancelled = Some(Box::new(move || cancelled_clone.load(Ordering::SeqCst))); + + // Cancel after a short delay + tokio::spawn(async move { + tokio::time::sleep(Duration::from_millis(10)).await; + cancelled.store(true, Ordering::SeqCst); + }); + + let result = yes(ctx).await; + + // Should have produced some output before being cancelled + assert!(!result.stdout.is_empty() || result.code == 130); + } + + #[tokio::test] + async fn test_yes_custom_string() { + let cancelled = Arc::new(AtomicBool::new(false)); + let cancelled_clone = cancelled.clone(); + + let mut ctx = CommandContext::new(vec!["hello".to_string()]); + ctx.is_cancelled = Some(Box::new(move || cancelled_clone.load(Ordering::SeqCst))); + + // Cancel immediately + cancelled.store(true, Ordering::SeqCst); + + let result = yes(ctx).await; + assert_eq!(result.code, 130); + } +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs new file mode 100644 index 0000000..311230f --- /dev/null +++ b/rust/src/lib.rs @@ -0,0 +1,492 @@ +//! # command-stream +//! +//! Modern shell command execution library with streaming, async iteration, and event support. +//! +//! This library provides a Rust equivalent to the JavaScript command-stream library, +//! offering powerful shell command execution with streaming capabilities. +//! +//! ## Features +//! +//! - Async command execution with tokio +//! - Streaming output via async iterators +//! - Virtual commands for common operations (cat, ls, mkdir, etc.) +//! - Shell operator support (&&, ||, ;, |) +//! - Cross-platform support +//! +//! ## Quick Start +//! +//! ```rust,no_run +//! use command_stream::run; +//! +//! #[tokio::main] +//! async fn main() -> Result<(), Box> { +//! // Execute a simple command +//! let result = run("echo hello world").await?; +//! println!("{}", result.stdout); +//! Ok(()) +//! } +//! ``` + +pub mod commands; +pub mod shell_parser; +pub mod utils; + +use std::collections::HashMap; +use std::env; +use std::path::PathBuf; +use std::process::Stdio; +use std::sync::Arc; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; +use tokio::process::{Child, Command}; +use tokio::sync::{mpsc, Mutex}; + +pub use commands::{CommandContext, StreamChunk}; +pub use shell_parser::{parse_shell_command, needs_real_shell, ParsedCommand}; +pub use utils::{AnsiConfig, AnsiUtils, CommandResult, VirtualUtils, quote, trace}; + +/// Error type for command-stream operations +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + #[error("Command failed with exit code {code}: {message}")] + CommandFailed { code: i32, message: String }, + + #[error("Command not found: {0}")] + CommandNotFound(String), + + #[error("Parse error: {0}")] + ParseError(String), + + #[error("Cancelled")] + Cancelled, +} + +/// Result type for command-stream operations +pub type Result = std::result::Result; + +/// Shell settings for controlling execution behavior +#[derive(Debug, Clone, Default)] +pub struct ShellSettings { + /// Exit immediately if a command exits with non-zero status (set -e) + pub errexit: bool, + /// Print commands as they are executed (set -v) + pub verbose: bool, + /// Print trace of commands (set -x) + pub xtrace: bool, + /// Return value of a pipeline is the status of the last command to exit with non-zero (set -o pipefail) + pub pipefail: bool, + /// Treat unset variables as an error (set -u) + pub nounset: bool, +} + +/// Options for command execution +#[derive(Debug, Clone)] +pub struct RunOptions { + /// Mirror output to parent stdout/stderr + pub mirror: bool, + /// Capture output in result + pub capture: bool, + /// Standard input handling + pub stdin: StdinOption, + /// Working directory + pub cwd: Option, + /// Environment variables + pub env: Option>, + /// Interactive mode (TTY forwarding) + pub interactive: bool, + /// Enable shell operator parsing + pub shell_operators: bool, + /// Enable tracing for this command + pub trace: bool, +} + +impl Default for RunOptions { + fn default() -> Self { + RunOptions { + mirror: true, + capture: true, + stdin: StdinOption::Inherit, + cwd: None, + env: None, + interactive: false, + shell_operators: true, + trace: true, + } + } +} + +/// Standard input options +#[derive(Debug, Clone)] +pub enum StdinOption { + /// Inherit from parent process + Inherit, + /// Pipe (allow writing to stdin) + Pipe, + /// Provide string content + Content(String), + /// Null device + Null, +} + +/// A running or completed process +pub struct ProcessRunner { + command: String, + options: RunOptions, + child: Option, + result: Option, + started: bool, + finished: bool, + cancelled: bool, + output_tx: Option>, + output_rx: Option>, +} + +impl ProcessRunner { + /// Create a new process runner + pub fn new(command: impl Into, options: RunOptions) -> Self { + let (tx, rx) = mpsc::channel(1024); + ProcessRunner { + command: command.into(), + options, + child: None, + result: None, + started: false, + finished: false, + cancelled: false, + output_tx: Some(tx), + output_rx: Some(rx), + } + } + + /// Start the process + pub async fn start(&mut self) -> Result<()> { + if self.started { + return Ok(()); + } + self.started = true; + + utils::trace_lazy("ProcessRunner", || { + format!("Starting command: {}", self.command) + }); + + // Check if this is a virtual command + let first_word = self.command.split_whitespace().next().unwrap_or(""); + if let Some(result) = self.try_virtual_command(first_word).await { + self.result = Some(result); + self.finished = true; + return Ok(()); + } + + // Parse command for shell operators + let parsed = if self.options.shell_operators && !needs_real_shell(&self.command) { + parse_shell_command(&self.command) + } else { + None + }; + + // Execute via real shell if needed + let shell = find_available_shell(); + + let mut cmd = Command::new(&shell.cmd); + for arg in &shell.args { + cmd.arg(arg); + } + cmd.arg(&self.command); + + // Configure stdin + match &self.options.stdin { + StdinOption::Inherit => { cmd.stdin(Stdio::inherit()); } + StdinOption::Pipe => { cmd.stdin(Stdio::piped()); } + StdinOption::Content(_) => { cmd.stdin(Stdio::piped()); } + StdinOption::Null => { cmd.stdin(Stdio::null()); } + } + + // Configure stdout/stderr + if self.options.capture || self.options.mirror { + cmd.stdout(Stdio::piped()); + cmd.stderr(Stdio::piped()); + } else { + cmd.stdout(Stdio::inherit()); + cmd.stderr(Stdio::inherit()); + } + + // Set working directory + if let Some(ref cwd) = self.options.cwd { + cmd.current_dir(cwd); + } + + // Set environment + if let Some(ref env_vars) = self.options.env { + for (key, value) in env_vars { + cmd.env(key, value); + } + } + + // Spawn the process + let child = cmd.spawn()?; + self.child = Some(child); + + Ok(()) + } + + /// Run the process to completion + pub async fn run(&mut self) -> Result { + self.start().await?; + + if let Some(result) = &self.result { + return Ok(result.clone()); + } + + let mut child = self.child.take().ok_or_else(|| { + Error::Io(std::io::Error::new( + std::io::ErrorKind::Other, + "Process not started", + )) + })?; + + // Handle stdin content if provided + if let StdinOption::Content(ref content) = self.options.stdin { + if let Some(mut stdin) = child.stdin.take() { + let content = content.clone(); + tokio::spawn(async move { + let _ = stdin.write_all(content.as_bytes()).await; + let _ = stdin.shutdown().await; + }); + } + } + + // Collect output + let mut stdout_content = String::new(); + let mut stderr_content = String::new(); + + if let Some(stdout) = child.stdout.take() { + let mut reader = BufReader::new(stdout).lines(); + while let Ok(Some(line)) = reader.next_line().await { + if self.options.mirror { + println!("{}", line); + } + stdout_content.push_str(&line); + stdout_content.push('\n'); + } + } + + if let Some(stderr) = child.stderr.take() { + let mut reader = BufReader::new(stderr).lines(); + while let Ok(Some(line)) = reader.next_line().await { + if self.options.mirror { + eprintln!("{}", line); + } + stderr_content.push_str(&line); + stderr_content.push('\n'); + } + } + + let status = child.wait().await?; + let code = status.code().unwrap_or(-1); + + let result = CommandResult { + stdout: stdout_content, + stderr: stderr_content, + code, + }; + + self.result = Some(result.clone()); + self.finished = true; + + Ok(result) + } + + /// Try to execute as a virtual command + async fn try_virtual_command(&self, cmd_name: &str) -> Option { + if !commands::are_virtual_commands_enabled() { + return None; + } + + // Parse args from command string + let parts: Vec<&str> = self.command.split_whitespace().collect(); + let args: Vec = parts.iter().skip(1).map(|s| s.to_string()).collect(); + + let ctx = CommandContext { + args, + stdin: match &self.options.stdin { + StdinOption::Content(s) => Some(s.clone()), + _ => None, + }, + cwd: self.options.cwd.clone(), + env: self.options.env.clone(), + output_tx: self.output_tx.clone(), + is_cancelled: None, + }; + + match cmd_name { + "echo" => Some(commands::echo(ctx).await), + "pwd" => Some(commands::pwd(ctx).await), + "cd" => Some(commands::cd(ctx).await), + "true" => Some(commands::r#true(ctx).await), + "false" => Some(commands::r#false(ctx).await), + "sleep" => Some(commands::sleep(ctx).await), + "cat" => Some(commands::cat(ctx).await), + "ls" => Some(commands::ls(ctx).await), + "mkdir" => Some(commands::mkdir(ctx).await), + "rm" => Some(commands::rm(ctx).await), + "touch" => Some(commands::touch(ctx).await), + "cp" => Some(commands::cp(ctx).await), + "mv" => Some(commands::mv(ctx).await), + "basename" => Some(commands::basename(ctx).await), + "dirname" => Some(commands::dirname(ctx).await), + "env" => Some(commands::env(ctx).await), + "exit" => Some(commands::exit(ctx).await), + "which" => Some(commands::which(ctx).await), + "yes" => Some(commands::yes(ctx).await), + "seq" => Some(commands::seq(ctx).await), + "test" => Some(commands::test(ctx).await), + _ => None, + } + } + + /// Kill the process + pub fn kill(&mut self) -> Result<()> { + self.cancelled = true; + if let Some(ref mut child) = self.child { + child.start_kill()?; + } + Ok(()) + } + + /// Check if the process is finished + pub fn is_finished(&self) -> bool { + self.finished + } + + /// Get the result if available + pub fn result(&self) -> Option<&CommandResult> { + self.result.as_ref() + } +} + +/// Shell configuration +#[derive(Debug, Clone)] +struct ShellConfig { + cmd: String, + args: Vec, +} + +/// Find an available shell +fn find_available_shell() -> ShellConfig { + let is_windows = cfg!(windows); + + if is_windows { + // Windows shells + let shells = [ + ("cmd.exe", vec!["/c"]), + ("powershell.exe", vec!["-Command"]), + ]; + + for (cmd, args) in shells { + if which::which(cmd).is_ok() { + return ShellConfig { + cmd: cmd.to_string(), + args: args.into_iter().map(String::from).collect(), + }; + } + } + + ShellConfig { + cmd: "cmd.exe".to_string(), + args: vec!["/c".to_string()], + } + } else { + // Unix shells + let shells = [ + ("/bin/sh", vec!["-c"]), + ("/usr/bin/sh", vec!["-c"]), + ("/bin/bash", vec!["-c"]), + ("sh", vec!["-c"]), + ]; + + for (cmd, args) in shells { + if std::path::Path::new(cmd).exists() || which::which(cmd).is_ok() { + return ShellConfig { + cmd: cmd.to_string(), + args: args.into_iter().map(String::from).collect(), + }; + } + } + + ShellConfig { + cmd: "/bin/sh".to_string(), + args: vec!["-c".to_string()], + } + } +} + +/// Execute a command and return the result +/// +/// This is the main entry point for simple command execution. +/// Named `run` instead of `$` since `$` is not a valid Rust identifier. +pub async fn run(command: impl Into) -> Result { + let mut runner = ProcessRunner::new(command, RunOptions::default()); + runner.run().await +} + +/// Alias for `run` function - for JavaScript-like API feel +/// Since `$` is not valid in Rust, this provides a similar short name +pub use run as execute; + +/// Execute a command with custom options +pub async fn exec(command: impl Into, options: RunOptions) -> Result { + let mut runner = ProcessRunner::new(command, options); + runner.run().await +} + +/// Create a new process runner without starting it +pub fn create(command: impl Into, options: RunOptions) -> ProcessRunner { + ProcessRunner::new(command, options) +} + +/// Execute a command synchronously (blocking) +pub fn run_sync(command: impl Into) -> Result { + let rt = tokio::runtime::Runtime::new()?; + rt.block_on(run(command)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_simple_echo() { + let result = run("echo hello").await.unwrap(); + assert!(result.is_success()); + assert!(result.stdout.contains("hello")); + } + + #[tokio::test] + async fn test_virtual_echo() { + let mut runner = ProcessRunner::new("echo test virtual", RunOptions::default()); + let result = runner.run().await.unwrap(); + assert!(result.is_success()); + assert!(result.stdout.contains("test virtual")); + } + + #[tokio::test] + async fn test_process_runner() { + let mut runner = ProcessRunner::new("echo hello world", RunOptions { + mirror: false, + ..Default::default() + }); + + let result = runner.run().await.unwrap(); + assert!(result.is_success()); + } + + #[tokio::test] + async fn test_virtual_pwd() { + let mut runner = ProcessRunner::new("pwd", RunOptions::default()); + let result = runner.run().await.unwrap(); + assert!(result.is_success()); + assert!(!result.stdout.is_empty()); + } +} diff --git a/rust/src/main.rs b/rust/src/main.rs new file mode 100644 index 0000000..b6e8edd --- /dev/null +++ b/rust/src/main.rs @@ -0,0 +1,37 @@ +//! command-stream CLI +//! +//! A simple CLI wrapper for the command-stream library. + +use command_stream::{run, RunOptions, ProcessRunner}; +use std::env; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let args: Vec = env::args().skip(1).collect(); + + if args.is_empty() { + eprintln!("Usage: command-stream [args...]"); + eprintln!(); + eprintln!("Execute shell commands with streaming support."); + eprintln!(); + eprintln!("Examples:"); + eprintln!(" command-stream echo hello world"); + eprintln!(" command-stream ls -la"); + eprintln!(" command-stream 'echo hello && echo world'"); + std::process::exit(1); + } + + let command = args.join(" "); + + let result = run(command).await?; + + // Print any output that wasn't mirrored + if !result.stdout.is_empty() && !result.stdout.ends_with('\n') { + println!("{}", result.stdout); + } + if !result.stderr.is_empty() { + eprint!("{}", result.stderr); + } + + std::process::exit(result.code); +} diff --git a/rust/src/shell_parser.rs b/rust/src/shell_parser.rs new file mode 100644 index 0000000..6d65454 --- /dev/null +++ b/rust/src/shell_parser.rs @@ -0,0 +1,565 @@ +//! Enhanced shell command parser that handles &&, ||, ;, and () operators +//! This allows virtual commands to work properly with shell operators + +use std::fmt; + +/// Token types for the parser +#[derive(Debug, Clone, PartialEq)] +pub enum TokenType { + Word(String), + And, // && + Or, // || + Semicolon, // ; + Pipe, // | + LParen, // ( + RParen, // ) + RedirectOut, // > + RedirectAppend, // >> + RedirectIn, // < + Eof, +} + +impl fmt::Display for TokenType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TokenType::Word(s) => write!(f, "Word({})", s), + TokenType::And => write!(f, "&&"), + TokenType::Or => write!(f, "||"), + TokenType::Semicolon => write!(f, ";"), + TokenType::Pipe => write!(f, "|"), + TokenType::LParen => write!(f, "("), + TokenType::RParen => write!(f, ")"), + TokenType::RedirectOut => write!(f, ">"), + TokenType::RedirectAppend => write!(f, ">>"), + TokenType::RedirectIn => write!(f, "<"), + TokenType::Eof => write!(f, "EOF"), + } + } +} + +/// A token with its type and original value +#[derive(Debug, Clone)] +pub struct Token { + pub token_type: TokenType, + pub value: String, +} + +/// Redirect information +#[derive(Debug, Clone)] +pub struct Redirect { + pub redirect_type: TokenType, + pub target: String, +} + +/// Parsed argument with quote information +#[derive(Debug, Clone)] +pub struct ParsedArg { + pub value: String, + pub quoted: bool, + pub quote_char: Option, +} + +/// Types of parsed commands +#[derive(Debug, Clone)] +pub enum ParsedCommand { + /// A simple command with command name, arguments, and optional redirects + Simple { + cmd: String, + args: Vec, + redirects: Vec, + }, + /// A sequence of commands connected by &&, ||, or ; + Sequence { + commands: Vec, + operators: Vec, + }, + /// A pipeline of commands connected by | + Pipeline { + commands: Vec, + }, + /// A subshell (commands in parentheses) + Subshell { + command: Box, + }, +} + +/// Tokenize a shell command string +pub fn tokenize(command: &str) -> Vec { + let mut tokens = Vec::new(); + let chars: Vec = command.chars().collect(); + let mut i = 0; + + while i < chars.len() { + // Skip whitespace + while i < chars.len() && chars[i].is_whitespace() { + i += 1; + } + + if i >= chars.len() { + break; + } + + // Check for operators + if chars[i] == '&' && i + 1 < chars.len() && chars[i + 1] == '&' { + tokens.push(Token { + token_type: TokenType::And, + value: "&&".to_string(), + }); + i += 2; + } else if chars[i] == '|' && i + 1 < chars.len() && chars[i + 1] == '|' { + tokens.push(Token { + token_type: TokenType::Or, + value: "||".to_string(), + }); + i += 2; + } else if chars[i] == '|' { + tokens.push(Token { + token_type: TokenType::Pipe, + value: "|".to_string(), + }); + i += 1; + } else if chars[i] == ';' { + tokens.push(Token { + token_type: TokenType::Semicolon, + value: ";".to_string(), + }); + i += 1; + } else if chars[i] == '(' { + tokens.push(Token { + token_type: TokenType::LParen, + value: "(".to_string(), + }); + i += 1; + } else if chars[i] == ')' { + tokens.push(Token { + token_type: TokenType::RParen, + value: ")".to_string(), + }); + i += 1; + } else if chars[i] == '>' && i + 1 < chars.len() && chars[i + 1] == '>' { + tokens.push(Token { + token_type: TokenType::RedirectAppend, + value: ">>".to_string(), + }); + i += 2; + } else if chars[i] == '>' { + tokens.push(Token { + token_type: TokenType::RedirectOut, + value: ">".to_string(), + }); + i += 1; + } else if chars[i] == '<' { + tokens.push(Token { + token_type: TokenType::RedirectIn, + value: "<".to_string(), + }); + i += 1; + } else { + // Parse word (respecting quotes) + let mut word = String::new(); + let mut in_quote = false; + let mut quote_char = ' '; + + while i < chars.len() { + let c = chars[i]; + + if !in_quote { + if c == '"' || c == '\'' { + in_quote = true; + quote_char = c; + word.push(c); + i += 1; + } else if c.is_whitespace() || "&|;()<>".contains(c) { + break; + } else if c == '\\' && i + 1 < chars.len() { + // Handle escape sequences + word.push(c); + i += 1; + if i < chars.len() { + word.push(chars[i]); + i += 1; + } + } else { + word.push(c); + i += 1; + } + } else { + let prev_char = if i > 0 { Some(chars[i - 1]) } else { None }; + if c == quote_char && prev_char != Some('\\') { + in_quote = false; + word.push(c); + i += 1; + } else if c == '\\' && i + 1 < chars.len() { + let next_char = chars[i + 1]; + if next_char == quote_char || next_char == '\\' { + // Handle escaped quotes and backslashes inside quotes + word.push(c); + i += 1; + if i < chars.len() { + word.push(chars[i]); + i += 1; + } + } else { + word.push(c); + i += 1; + } + } else { + word.push(c); + i += 1; + } + } + } + + if !word.is_empty() { + tokens.push(Token { + token_type: TokenType::Word(word.clone()), + value: word, + }); + } + } + } + + tokens.push(Token { + token_type: TokenType::Eof, + value: String::new(), + }); + + tokens +} + +/// Shell command parser +pub struct ShellParser { + tokens: Vec, + pos: usize, +} + +impl ShellParser { + /// Create a new parser for the given command + pub fn new(command: &str) -> Self { + ShellParser { + tokens: tokenize(command), + pos: 0, + } + } + + fn current(&self) -> Token { + self.tokens.get(self.pos).cloned().unwrap_or(Token { + token_type: TokenType::Eof, + value: String::new(), + }) + } + + fn consume(&mut self) -> Token { + let token = self.current().clone(); + self.pos += 1; + token + } + + /// Parse the main command sequence + pub fn parse(&mut self) -> Option { + self.parse_sequence() + } + + /// Parse a sequence of commands connected by &&, ||, ; + fn parse_sequence(&mut self) -> Option { + let mut commands = Vec::new(); + let mut operators = Vec::new(); + + // Parse first command + if let Some(cmd) = self.parse_pipeline() { + commands.push(cmd); + } + + // Parse additional commands with operators + loop { + match &self.current().token_type { + TokenType::Eof | TokenType::RParen => break, + TokenType::And | TokenType::Or | TokenType::Semicolon => { + let op = self.consume().token_type; + operators.push(op); + + if let Some(cmd) = self.parse_pipeline() { + commands.push(cmd); + } + } + _ => break, + } + } + + if commands.len() == 1 && operators.is_empty() { + return commands.into_iter().next(); + } + + if commands.is_empty() { + return None; + } + + Some(ParsedCommand::Sequence { + commands, + operators, + }) + } + + /// Parse a pipeline (commands connected by |) + fn parse_pipeline(&mut self) -> Option { + let mut commands = Vec::new(); + + if let Some(cmd) = self.parse_command() { + commands.push(cmd); + } + + while matches!(self.current().token_type, TokenType::Pipe) { + self.consume(); + if let Some(cmd) = self.parse_command() { + commands.push(cmd); + } + } + + if commands.len() == 1 { + return commands.into_iter().next(); + } + + if commands.is_empty() { + return None; + } + + Some(ParsedCommand::Pipeline { commands }) + } + + /// Parse a single command or subshell + fn parse_command(&mut self) -> Option { + // Check for subshell + if matches!(self.current().token_type, TokenType::LParen) { + self.consume(); // consume ( + let subshell = self.parse_sequence(); + + if matches!(self.current().token_type, TokenType::RParen) { + self.consume(); // consume ) + } + + return subshell.map(|cmd| ParsedCommand::Subshell { + command: Box::new(cmd), + }); + } + + // Parse simple command + self.parse_simple_command() + } + + /// Parse a simple command (command + args + redirections) + fn parse_simple_command(&mut self) -> Option { + let mut words = Vec::new(); + let mut redirects = Vec::new(); + + loop { + match &self.current().token_type { + TokenType::Eof => break, + TokenType::Word(w) => { + words.push(w.clone()); + self.consume(); + } + TokenType::RedirectOut | TokenType::RedirectAppend | TokenType::RedirectIn => { + let redirect_type = self.consume().token_type; + if let TokenType::Word(target) = &self.current().token_type { + redirects.push(Redirect { + redirect_type, + target: target.clone(), + }); + self.consume(); + } + } + _ => break, + } + } + + if words.is_empty() { + return None; + } + + let cmd = words.remove(0); + let args: Vec = words + .into_iter() + .map(|word| { + // Remove quotes if present + if (word.starts_with('"') && word.ends_with('"')) + || (word.starts_with('\'') && word.ends_with('\'')) + { + ParsedArg { + value: word[1..word.len() - 1].to_string(), + quoted: true, + quote_char: Some(word.chars().next().unwrap()), + } + } else { + ParsedArg { + value: word, + quoted: false, + quote_char: None, + } + } + }) + .collect(); + + Some(ParsedCommand::Simple { + cmd, + args, + redirects, + }) + } +} + +/// Parse a shell command with support for &&, ||, ;, and () +pub fn parse_shell_command(command: &str) -> Option { + let mut parser = ShellParser::new(command); + parser.parse() +} + +/// Check if a command needs shell features we don't handle +pub fn needs_real_shell(command: &str) -> bool { + // Check for features we don't handle yet + let unsupported = [ + "`", // Command substitution + "$(", // Command substitution + "${", // Variable expansion + "~", // Home expansion (at start of word) + "*", // Glob patterns + "?", // Glob patterns + "[", // Glob patterns + "2>", // stderr redirection + "&>", // Combined redirection + ">&", // File descriptor duplication + "<<", // Here documents + "<<<", // Here strings + ]; + + for feature in &unsupported { + if command.contains(feature) { + return true; + } + } + + false +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_tokenize_simple_command() { + let tokens = tokenize("echo hello world"); + assert_eq!(tokens.len(), 4); // 3 words + EOF + assert!(matches!(tokens[0].token_type, TokenType::Word(_))); + assert!(matches!(tokens[3].token_type, TokenType::Eof)); + } + + #[test] + fn test_tokenize_with_operators() { + let tokens = tokenize("cmd1 && cmd2 || cmd3"); + assert_eq!(tokens.len(), 6); // 3 words + 2 operators + EOF + assert!(matches!(tokens[1].token_type, TokenType::And)); + assert!(matches!(tokens[3].token_type, TokenType::Or)); + } + + #[test] + fn test_tokenize_with_pipe() { + let tokens = tokenize("ls | grep foo"); + assert_eq!(tokens.len(), 5); // 3 words + 1 pipe + EOF + assert!(matches!(tokens[1].token_type, TokenType::Pipe)); + } + + #[test] + fn test_tokenize_with_quotes() { + let tokens = tokenize("echo 'hello world'"); + assert_eq!(tokens.len(), 3); // echo + quoted string + EOF + if let TokenType::Word(w) = &tokens[1].token_type { + assert_eq!(w, "'hello world'"); + } else { + panic!("Expected Word token"); + } + } + + #[test] + fn test_parse_simple_command() { + let cmd = parse_shell_command("echo hello world").unwrap(); + match cmd { + ParsedCommand::Simple { cmd, args, .. } => { + assert_eq!(cmd, "echo"); + assert_eq!(args.len(), 2); + assert_eq!(args[0].value, "hello"); + assert_eq!(args[1].value, "world"); + } + _ => panic!("Expected Simple command"), + } + } + + #[test] + fn test_parse_pipeline() { + let cmd = parse_shell_command("ls | grep foo | wc -l").unwrap(); + match cmd { + ParsedCommand::Pipeline { commands } => { + assert_eq!(commands.len(), 3); + } + _ => panic!("Expected Pipeline"), + } + } + + #[test] + fn test_parse_sequence() { + let cmd = parse_shell_command("cmd1 && cmd2 || cmd3").unwrap(); + match cmd { + ParsedCommand::Sequence { + commands, + operators, + } => { + assert_eq!(commands.len(), 3); + assert_eq!(operators.len(), 2); + assert!(matches!(operators[0], TokenType::And)); + assert!(matches!(operators[1], TokenType::Or)); + } + _ => panic!("Expected Sequence"), + } + } + + #[test] + fn test_needs_real_shell() { + assert!(needs_real_shell("echo $(date)")); + assert!(needs_real_shell("ls *.txt")); + assert!(needs_real_shell("echo ${HOME}")); + assert!(!needs_real_shell("echo hello")); + assert!(!needs_real_shell("ls | grep foo")); + } + + #[test] + fn test_parse_with_redirect() { + let cmd = parse_shell_command("echo hello > output.txt").unwrap(); + match cmd { + ParsedCommand::Simple { + cmd, + args, + redirects, + } => { + assert_eq!(cmd, "echo"); + assert_eq!(args.len(), 1); + assert_eq!(redirects.len(), 1); + assert!(matches!( + redirects[0].redirect_type, + TokenType::RedirectOut + )); + assert_eq!(redirects[0].target, "output.txt"); + } + _ => panic!("Expected Simple command with redirect"), + } + } + + #[test] + fn test_parse_subshell() { + let cmd = parse_shell_command("(echo hello) && echo world").unwrap(); + match cmd { + ParsedCommand::Sequence { commands, .. } => { + assert_eq!(commands.len(), 2); + assert!(matches!(commands[0], ParsedCommand::Subshell { .. })); + } + _ => panic!("Expected Sequence with Subshell"), + } + } +} diff --git a/rust/src/utils.rs b/rust/src/utils.rs new file mode 100644 index 0000000..080f1a8 --- /dev/null +++ b/rust/src/utils.rs @@ -0,0 +1,335 @@ +//! Utility functions and types for command-stream +//! +//! This module provides helper functions for tracing, error handling, +//! and common file system operations used by virtual commands. + +use std::env; +use std::path::{Path, PathBuf}; + +/// Check if tracing is enabled via environment variables +/// +/// Tracing can be controlled via: +/// - COMMAND_STREAM_TRACE=true/false (explicit control) +/// - COMMAND_STREAM_VERBOSE=true (enables tracing unless TRACE=false) +pub fn is_trace_enabled() -> bool { + let trace_env = env::var("COMMAND_STREAM_TRACE").ok(); + let verbose_env = env::var("COMMAND_STREAM_VERBOSE") + .map(|v| v == "true") + .unwrap_or(false); + + match trace_env.as_deref() { + Some("false") => false, + Some("true") => true, + _ => verbose_env, + } +} + +/// Trace function for verbose logging +/// +/// Outputs trace messages to stderr when tracing is enabled. +/// Messages are prefixed with timestamp and category. +pub fn trace(category: &str, message: &str) { + if !is_trace_enabled() { + return; + } + + let timestamp = chrono::Utc::now().to_rfc3339(); + eprintln!("[TRACE {}] [{}] {}", timestamp, category, message); +} + +/// Trace function with lazy message evaluation +/// +/// Only evaluates the message function if tracing is enabled. +pub fn trace_lazy(category: &str, message_fn: F) +where + F: FnOnce() -> String, +{ + if !is_trace_enabled() { + return; + } + + trace(category, &message_fn()); +} + +/// Result type for virtual command operations +#[derive(Debug, Clone)] +pub struct CommandResult { + pub stdout: String, + pub stderr: String, + pub code: i32, +} + +impl CommandResult { + /// Create a success result with stdout output + pub fn success(stdout: impl Into) -> Self { + CommandResult { + stdout: stdout.into(), + stderr: String::new(), + code: 0, + } + } + + /// Create an empty success result + pub fn success_empty() -> Self { + CommandResult { + stdout: String::new(), + stderr: String::new(), + code: 0, + } + } + + /// Create an error result with stderr output + pub fn error(stderr: impl Into) -> Self { + CommandResult { + stdout: String::new(), + stderr: stderr.into(), + code: 1, + } + } + + /// Create an error result with custom exit code + pub fn error_with_code(stderr: impl Into, code: i32) -> Self { + CommandResult { + stdout: String::new(), + stderr: stderr.into(), + code, + } + } + + /// Check if the command was successful + pub fn is_success(&self) -> bool { + self.code == 0 + } +} + +/// Utility functions for virtual commands +pub struct VirtualUtils; + +impl VirtualUtils { + /// Create standardized error response for missing operands + pub fn missing_operand_error(command_name: &str) -> CommandResult { + CommandResult::error(format!("{}: missing operand", command_name)) + } + + /// Create standardized error response for missing operands with custom message + pub fn missing_operand_error_with_message( + command_name: &str, + message: &str, + ) -> CommandResult { + CommandResult::error(format!("{}: {}", command_name, message)) + } + + /// Create standardized error response for invalid arguments + pub fn invalid_argument_error(command_name: &str, message: &str) -> CommandResult { + CommandResult::error(format!("{}: {}", command_name, message)) + } + + /// Create standardized success response + pub fn success(stdout: impl Into) -> CommandResult { + CommandResult::success(stdout) + } + + /// Create standardized error response + pub fn error(stderr: impl Into) -> CommandResult { + CommandResult::error(stderr) + } + + /// Validate that command has required number of arguments + pub fn validate_args( + args: &[String], + min_count: usize, + command_name: &str, + ) -> Option { + if args.len() < min_count { + if min_count == 1 { + return Some(Self::missing_operand_error(command_name)); + } else { + return Some(Self::invalid_argument_error( + command_name, + &format!("requires at least {} arguments", min_count), + )); + } + } + None // No error + } + + /// Resolve file path with optional cwd parameter + pub fn resolve_path(file_path: &str, cwd: Option<&Path>) -> PathBuf { + let path = Path::new(file_path); + if path.is_absolute() { + path.to_path_buf() + } else { + let base_path = cwd.map(|p| p.to_path_buf()).unwrap_or_else(|| { + env::current_dir().unwrap_or_else(|_| PathBuf::from("/")) + }); + base_path.join(path) + } + } +} + +/// ANSI control character utilities +pub struct AnsiUtils; + +impl AnsiUtils { + /// Strip ANSI escape sequences from text + pub fn strip_ansi(text: &str) -> String { + let re = regex::Regex::new(r"\x1b\[[0-9;]*[mGKHFJ]").unwrap(); + re.replace_all(text, "").to_string() + } + + /// Strip control characters from text, preserving newlines, carriage returns, and tabs + pub fn strip_control_chars(text: &str) -> String { + text.chars() + .filter(|c| { + // Preserve newlines (\n = \x0A), carriage returns (\r = \x0D), and tabs (\t = \x09) + !matches!(*c as u32, + 0x00..=0x08 | 0x0B | 0x0C | 0x0E..=0x1F | 0x7F + ) + }) + .collect() + } + + /// Strip both ANSI sequences and control characters + pub fn strip_all(text: &str) -> String { + Self::strip_control_chars(&Self::strip_ansi(text)) + } + + /// Clean data for processing (strips ANSI and control chars) + pub fn clean_for_processing(data: &str) -> String { + Self::strip_all(data) + } +} + +/// Configuration for ANSI handling +#[derive(Debug, Clone)] +pub struct AnsiConfig { + pub preserve_ansi: bool, + pub preserve_control_chars: bool, +} + +impl Default for AnsiConfig { + fn default() -> Self { + AnsiConfig { + preserve_ansi: true, + preserve_control_chars: true, + } + } +} + +impl AnsiConfig { + /// Process output according to config settings + pub fn process_output(&self, data: &str) -> String { + if !self.preserve_ansi && !self.preserve_control_chars { + AnsiUtils::clean_for_processing(data) + } else if !self.preserve_ansi { + AnsiUtils::strip_ansi(data) + } else if !self.preserve_control_chars { + AnsiUtils::strip_control_chars(data) + } else { + data.to_string() + } + } +} + +/// Quote a value for safe shell usage +pub fn quote(value: &str) -> String { + if value.is_empty() { + return "''".to_string(); + } + + // If already properly quoted, check if we can use as-is + if value.starts_with('\'') && value.ends_with('\'') && value.len() >= 2 { + let inner = &value[1..value.len() - 1]; + if !inner.contains('\'') { + return value.to_string(); + } + } + + if value.starts_with('"') && value.ends_with('"') && value.len() > 2 { + // If already double-quoted, wrap in single quotes + return format!("'{}'", value); + } + + // Check if the string needs quoting at all + // Safe characters: alphanumeric, dash, underscore, dot, slash, colon, equals, comma, plus + let safe_pattern = regex::Regex::new(r"^[a-zA-Z0-9_\-./=,+@:]+$").unwrap(); + + if safe_pattern.is_match(value) { + return value.to_string(); + } + + // Default behavior: wrap in single quotes and escape any internal single quotes + format!("'{}'", value.replace('\'', "'\\''")) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_command_result_success() { + let result = CommandResult::success("hello"); + assert!(result.is_success()); + assert_eq!(result.stdout, "hello"); + assert_eq!(result.stderr, ""); + assert_eq!(result.code, 0); + } + + #[test] + fn test_command_result_error() { + let result = CommandResult::error("something went wrong"); + assert!(!result.is_success()); + assert_eq!(result.stdout, ""); + assert_eq!(result.stderr, "something went wrong"); + assert_eq!(result.code, 1); + } + + #[test] + fn test_resolve_path_absolute() { + let path = VirtualUtils::resolve_path("/absolute/path", None); + assert_eq!(path, PathBuf::from("/absolute/path")); + } + + #[test] + fn test_resolve_path_relative() { + let cwd = PathBuf::from("/home/user"); + let path = VirtualUtils::resolve_path("relative/path", Some(&cwd)); + assert_eq!(path, PathBuf::from("/home/user/relative/path")); + } + + #[test] + fn test_strip_ansi() { + let text = "\x1b[31mRed text\x1b[0m"; + assert_eq!(AnsiUtils::strip_ansi(text), "Red text"); + } + + #[test] + fn test_strip_control_chars() { + let text = "Hello\x00World\nNew line\tTab"; + assert_eq!(AnsiUtils::strip_control_chars(text), "HelloWorld\nNew line\tTab"); + } + + #[test] + fn test_quote_empty() { + assert_eq!(quote(""), "''"); + } + + #[test] + fn test_quote_safe_chars() { + assert_eq!(quote("hello"), "hello"); + assert_eq!(quote("/path/to/file"), "/path/to/file"); + } + + #[test] + fn test_quote_special_chars() { + assert_eq!(quote("hello world"), "'hello world'"); + assert_eq!(quote("it's"), "'it'\\''s'"); + } + + #[test] + fn test_validate_args() { + let args = vec!["arg1".to_string()]; + assert!(VirtualUtils::validate_args(&args, 1, "cmd").is_none()); + assert!(VirtualUtils::validate_args(&args, 2, "cmd").is_some()); + } +} diff --git a/rust/tests/builtin_commands.rs b/rust/tests/builtin_commands.rs new file mode 100644 index 0000000..f86dd0d --- /dev/null +++ b/rust/tests/builtin_commands.rs @@ -0,0 +1,549 @@ +//! Integration tests for built-in virtual commands +//! +//! These tests mirror the JavaScript tests in js/tests/builtin-commands.test.mjs + +use command_stream::commands::{ + basename, cat, cd, cp, dirname, echo, env, exit, ls, mkdir, mv, pwd, rm, seq, sleep, test, + touch, which, yes, CommandContext, +}; +use command_stream::utils::CommandResult; +use std::fs; +use std::path::PathBuf; +use tempfile::TempDir; + +/// Helper to create a command context with args +fn ctx(args: Vec<&str>) -> CommandContext { + CommandContext { + args: args.into_iter().map(String::from).collect(), + stdin: None, + cwd: None, + env: None, + output_tx: None, + is_cancelled: None, + } +} + +/// Helper to create a command context with args and cwd +fn ctx_with_cwd(args: Vec<&str>, cwd: PathBuf) -> CommandContext { + CommandContext { + args: args.into_iter().map(String::from).collect(), + stdin: None, + cwd: Some(cwd), + env: None, + output_tx: None, + is_cancelled: None, + } +} + +/// Helper to create a command context with stdin +fn ctx_with_stdin(args: Vec<&str>, stdin: &str) -> CommandContext { + CommandContext { + args: args.into_iter().map(String::from).collect(), + stdin: Some(stdin.to_string()), + cwd: None, + env: None, + output_tx: None, + is_cancelled: None, + } +} + +// ============================================================================ +// Echo Command Tests +// ============================================================================ + +#[tokio::test] +async fn test_echo_basic() { + let result = echo(ctx(vec!["Hello", "World"])).await; + assert!(result.is_success()); + assert_eq!(result.stdout, "Hello World\n"); +} + +#[tokio::test] +async fn test_echo_with_n_flag() { + let result = echo(ctx(vec!["-n", "Hello"])).await; + assert!(result.is_success()); + assert_eq!(result.stdout, "Hello"); +} + +#[tokio::test] +async fn test_echo_empty() { + let result = echo(ctx(vec![])).await; + assert!(result.is_success()); + assert_eq!(result.stdout, "\n"); +} + +#[tokio::test] +async fn test_echo_with_e_flag() { + let result = echo(ctx(vec!["-e", "Hello\\nWorld"])).await; + assert!(result.is_success()); + assert!(result.stdout.contains("\n")); +} + +// ============================================================================ +// Pwd Command Tests +// ============================================================================ + +#[tokio::test] +async fn test_pwd_returns_current_directory() { + let result = pwd(ctx(vec![])).await; + assert!(result.is_success()); + assert!(!result.stdout.is_empty()); +} + +// ============================================================================ +// True/False Command Tests +// ============================================================================ + +#[tokio::test] +async fn test_true_command() { + let result = command_stream::commands::r#true(ctx(vec![])).await; + assert!(result.is_success()); + assert_eq!(result.code, 0); +} + +#[tokio::test] +async fn test_false_command() { + let result = command_stream::commands::r#false(ctx(vec![])).await; + assert!(!result.is_success()); + assert_eq!(result.code, 1); +} + +// ============================================================================ +// Cat Command Tests +// ============================================================================ + +#[tokio::test] +async fn test_cat_read_file() { + let dir = TempDir::new().unwrap(); + let file_path = dir.path().join("test.txt"); + fs::write(&file_path, "Hello World\nLine 2\n").unwrap(); + + let result = cat(ctx(vec![file_path.to_str().unwrap()])).await; + assert!(result.is_success()); + assert_eq!(result.stdout, "Hello World\nLine 2\n"); +} + +#[tokio::test] +async fn test_cat_from_stdin() { + let result = cat(ctx_with_stdin(vec![], "stdin input")).await; + assert!(result.is_success()); + assert_eq!(result.stdout, "stdin input"); +} + +#[tokio::test] +async fn test_cat_nonexistent_file() { + let result = cat(ctx(vec!["nonexistent.txt"])).await; + assert!(!result.is_success()); + assert!(result.stderr.contains("No such file or directory")); +} + +// ============================================================================ +// Ls Command Tests +// ============================================================================ + +#[tokio::test] +async fn test_ls_list_directory() { + let dir = TempDir::new().unwrap(); + fs::write(dir.path().join("file1.txt"), "content").unwrap(); + fs::write(dir.path().join("file2.txt"), "content").unwrap(); + fs::create_dir(dir.path().join("subdir")).unwrap(); + + let result = ls(ctx(vec![dir.path().to_str().unwrap()])).await; + assert!(result.is_success()); + assert!(result.stdout.contains("file1.txt")); + assert!(result.stdout.contains("file2.txt")); + assert!(result.stdout.contains("subdir")); +} + +#[tokio::test] +async fn test_ls_with_a_flag() { + let dir = TempDir::new().unwrap(); + fs::write(dir.path().join(".hidden"), "content").unwrap(); + fs::write(dir.path().join("visible.txt"), "content").unwrap(); + + let result = ls(ctx(vec!["-a", dir.path().to_str().unwrap()])).await; + assert!(result.is_success()); + assert!(result.stdout.contains(".hidden")); + assert!(result.stdout.contains("visible.txt")); +} + +#[tokio::test] +async fn test_ls_with_l_flag() { + let dir = TempDir::new().unwrap(); + fs::write(dir.path().join("test.txt"), "content").unwrap(); + + let result = ls(ctx(vec!["-l", dir.path().to_str().unwrap()])).await; + assert!(result.is_success()); + // Long format should include permissions + assert!(result.stdout.contains("-") || result.stdout.contains("r")); + assert!(result.stdout.contains("test.txt")); +} + +// ============================================================================ +// Mkdir Command Tests +// ============================================================================ + +#[tokio::test] +async fn test_mkdir_create_directory() { + let dir = TempDir::new().unwrap(); + let new_dir = dir.path().join("newdir"); + + let result = mkdir(ctx(vec![new_dir.to_str().unwrap()])).await; + assert!(result.is_success()); + assert!(new_dir.exists()); +} + +#[tokio::test] +async fn test_mkdir_with_p_flag() { + let dir = TempDir::new().unwrap(); + let nested_dir = dir.path().join("parent").join("child"); + + let result = mkdir(ctx(vec!["-p", nested_dir.to_str().unwrap()])).await; + assert!(result.is_success()); + assert!(nested_dir.exists()); +} + +// ============================================================================ +// Touch Command Tests +// ============================================================================ + +#[tokio::test] +async fn test_touch_create_file() { + let dir = TempDir::new().unwrap(); + let file_path = dir.path().join("touched.txt"); + + let result = touch(ctx(vec![file_path.to_str().unwrap()])).await; + assert!(result.is_success()); + assert!(file_path.exists()); +} + +#[tokio::test] +async fn test_touch_update_timestamp() { + let dir = TempDir::new().unwrap(); + let file_path = dir.path().join("existing.txt"); + fs::write(&file_path, "content").unwrap(); + let old_mtime = fs::metadata(&file_path).unwrap().modified().unwrap(); + + // Wait a bit to ensure timestamp difference + std::thread::sleep(std::time::Duration::from_millis(10)); + + let result = touch(ctx(vec![file_path.to_str().unwrap()])).await; + assert!(result.is_success()); + + let new_mtime = fs::metadata(&file_path).unwrap().modified().unwrap(); + assert!(new_mtime > old_mtime); +} + +// ============================================================================ +// Rm Command Tests +// ============================================================================ + +#[tokio::test] +async fn test_rm_remove_file() { + let dir = TempDir::new().unwrap(); + let file_path = dir.path().join("to-remove.txt"); + fs::write(&file_path, "content").unwrap(); + + let result = rm(ctx(vec![file_path.to_str().unwrap()])).await; + assert!(result.is_success()); + assert!(!file_path.exists()); +} + +#[tokio::test] +async fn test_rm_fail_on_directory() { + let dir = TempDir::new().unwrap(); + let sub_dir = dir.path().join("to-remove-dir"); + fs::create_dir(&sub_dir).unwrap(); + + let result = rm(ctx(vec![sub_dir.to_str().unwrap()])).await; + assert!(!result.is_success()); + assert!(result.stderr.contains("Is a directory")); + assert!(sub_dir.exists()); +} + +#[tokio::test] +async fn test_rm_recursive() { + let dir = TempDir::new().unwrap(); + let sub_dir = dir.path().join("to-remove-recursive"); + fs::create_dir(&sub_dir).unwrap(); + fs::write(sub_dir.join("file.txt"), "content").unwrap(); + + let result = rm(ctx(vec!["-r", sub_dir.to_str().unwrap()])).await; + assert!(result.is_success()); + assert!(!sub_dir.exists()); +} + +#[tokio::test] +async fn test_rm_force() { + let result = rm(ctx(vec!["-f", "nonexistent.txt"])).await; + assert!(result.is_success()); +} + +// ============================================================================ +// Cp Command Tests +// ============================================================================ + +#[tokio::test] +async fn test_cp_copy_file() { + let dir = TempDir::new().unwrap(); + let source = dir.path().join("source.txt"); + let dest = dir.path().join("dest.txt"); + fs::write(&source, "test content").unwrap(); + + let result = cp(ctx(vec![source.to_str().unwrap(), dest.to_str().unwrap()])).await; + assert!(result.is_success()); + assert!(dest.exists()); + assert_eq!(fs::read_to_string(&dest).unwrap(), "test content"); +} + +#[tokio::test] +async fn test_cp_recursive() { + let dir = TempDir::new().unwrap(); + let source_dir = dir.path().join("source-dir"); + let dest_dir = dir.path().join("dest-dir"); + fs::create_dir(&source_dir).unwrap(); + fs::write(source_dir.join("file.txt"), "content").unwrap(); + + let result = cp(ctx(vec![ + "-r", + source_dir.to_str().unwrap(), + dest_dir.to_str().unwrap(), + ])) + .await; + assert!(result.is_success()); + assert!(dest_dir.exists()); + assert!(dest_dir.join("file.txt").exists()); +} + +// ============================================================================ +// Mv Command Tests +// ============================================================================ + +#[tokio::test] +async fn test_mv_rename_file() { + let dir = TempDir::new().unwrap(); + let source = dir.path().join("source.txt"); + let dest = dir.path().join("dest.txt"); + fs::write(&source, "test content").unwrap(); + + let result = mv(ctx(vec![source.to_str().unwrap(), dest.to_str().unwrap()])).await; + assert!(result.is_success()); + assert!(!source.exists()); + assert!(dest.exists()); + assert_eq!(fs::read_to_string(&dest).unwrap(), "test content"); +} + +#[tokio::test] +async fn test_mv_to_directory() { + let dir = TempDir::new().unwrap(); + let source = dir.path().join("source.txt"); + let dest_dir = dir.path().join("dest-dir"); + fs::write(&source, "test content").unwrap(); + fs::create_dir(&dest_dir).unwrap(); + + let result = mv(ctx(vec![source.to_str().unwrap(), dest_dir.to_str().unwrap()])).await; + assert!(result.is_success()); + assert!(!source.exists()); + assert!(dest_dir.join("source.txt").exists()); +} + +// ============================================================================ +// Basename/Dirname Command Tests +// ============================================================================ + +#[tokio::test] +async fn test_basename() { + let result = basename(ctx(vec!["/path/to/file.txt"])).await; + assert!(result.is_success()); + assert_eq!(result.stdout.trim(), "file.txt"); +} + +#[tokio::test] +async fn test_basename_with_suffix() { + let result = basename(ctx(vec!["/path/to/file.txt", ".txt"])).await; + assert!(result.is_success()); + assert_eq!(result.stdout.trim(), "file"); +} + +#[tokio::test] +async fn test_dirname() { + let result = dirname(ctx(vec!["/path/to/file.txt"])).await; + assert!(result.is_success()); + assert_eq!(result.stdout.trim(), "/path/to"); +} + +// ============================================================================ +// Which Command Tests +// ============================================================================ + +#[tokio::test] +async fn test_which_builtin() { + let result = which(ctx(vec!["echo"])).await; + assert!(result.is_success()); + assert!(result.stdout.contains("shell builtin")); +} + +// ============================================================================ +// Exit Command Tests +// ============================================================================ + +#[tokio::test] +async fn test_exit_default() { + let result = exit(ctx(vec![])).await; + assert!(result.is_success()); + assert_eq!(result.code, 0); +} + +#[tokio::test] +async fn test_exit_with_code() { + let result = exit(ctx(vec!["42"])).await; + assert!(!result.is_success()); + assert_eq!(result.code, 42); +} + +// ============================================================================ +// Sleep Command Tests +// ============================================================================ + +#[tokio::test] +async fn test_sleep() { + let start = std::time::Instant::now(); + let result = sleep(ctx(vec!["0.1"])).await; + let elapsed = start.elapsed(); + + assert!(result.is_success()); + assert!(elapsed.as_millis() >= 90); + assert!(elapsed.as_millis() < 200); +} + +// ============================================================================ +// Seq Command Tests +// ============================================================================ + +#[tokio::test] +async fn test_seq_simple() { + let result = seq(ctx(vec!["5"])).await; + assert!(result.is_success()); + assert_eq!(result.stdout, "1\n2\n3\n4\n5\n"); +} + +#[tokio::test] +async fn test_seq_range() { + let result = seq(ctx(vec!["2", "5"])).await; + assert!(result.is_success()); + assert_eq!(result.stdout, "2\n3\n4\n5\n"); +} + +#[tokio::test] +async fn test_seq_with_increment() { + let result = seq(ctx(vec!["1", "2", "5"])).await; + assert!(result.is_success()); + assert_eq!(result.stdout, "1\n3\n5\n"); +} + +// ============================================================================ +// Yes Command Tests +// ============================================================================ + +#[tokio::test] +async fn test_yes_with_cancel() { + use std::sync::atomic::{AtomicBool, Ordering}; + use std::sync::Arc; + + // Create a cancellation flag + let cancelled = Arc::new(AtomicBool::new(false)); + let cancelled_clone = cancelled.clone(); + + // Cancel after a short delay + tokio::spawn(async move { + tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; + cancelled_clone.store(true, Ordering::SeqCst); + }); + + let ctx = CommandContext { + args: vec![], + stdin: None, + cwd: None, + env: None, + output_tx: None, + is_cancelled: Some(Box::new(move || cancelled.load(Ordering::SeqCst))), + }; + + let result = yes(ctx).await; + // Yes command should have produced some output before being cancelled + assert!(result.stdout.contains("y") || result.is_success()); +} + +// ============================================================================ +// Test Command Tests +// ============================================================================ + +#[tokio::test] +async fn test_test_file_exists() { + let dir = TempDir::new().unwrap(); + let file_path = dir.path().join("test.txt"); + fs::write(&file_path, "content").unwrap(); + + let result = test(ctx(vec!["-e", file_path.to_str().unwrap()])).await; + assert!(result.is_success()); +} + +#[tokio::test] +async fn test_test_file_not_exists() { + let result = test(ctx(vec!["-e", "nonexistent.txt"])).await; + assert!(!result.is_success()); +} + +#[tokio::test] +async fn test_test_is_file() { + let dir = TempDir::new().unwrap(); + let file_path = dir.path().join("test.txt"); + fs::write(&file_path, "content").unwrap(); + + let result = test(ctx(vec!["-f", file_path.to_str().unwrap()])).await; + assert!(result.is_success()); +} + +#[tokio::test] +async fn test_test_is_directory() { + let dir = TempDir::new().unwrap(); + + let result = test(ctx(vec!["-d", dir.path().to_str().unwrap()])).await; + assert!(result.is_success()); +} + +#[tokio::test] +async fn test_test_string_not_empty() { + let result = test(ctx(vec!["-n", "hello"])).await; + assert!(result.is_success()); +} + +#[tokio::test] +async fn test_test_string_empty() { + let result = test(ctx(vec!["-z", ""])).await; + assert!(result.is_success()); +} + +#[tokio::test] +async fn test_test_string_equals() { + let result = test(ctx(vec!["hello", "=", "hello"])).await; + assert!(result.is_success()); +} + +#[tokio::test] +async fn test_test_string_not_equals() { + let result = test(ctx(vec!["hello", "!=", "world"])).await; + assert!(result.is_success()); +} + +// ============================================================================ +// Env Command Tests +// ============================================================================ + +#[tokio::test] +async fn test_env_list() { + let result = env(ctx(vec![])).await; + assert!(result.is_success()); + // Should contain some environment variables + assert!(!result.stdout.is_empty()); +} diff --git a/rust/tests/process_runner.rs b/rust/tests/process_runner.rs new file mode 100644 index 0000000..93c1c19 --- /dev/null +++ b/rust/tests/process_runner.rs @@ -0,0 +1,286 @@ +//! Integration tests for ProcessRunner +//! +//! These tests mirror the JavaScript $.test.mjs tests + +use command_stream::{run, create, exec, ProcessRunner, RunOptions, StdinOption}; +use std::collections::HashMap; +use std::path::PathBuf; +use tempfile::TempDir; + +// ============================================================================ +// Basic Command Execution Tests +// ============================================================================ + +#[tokio::test] +async fn test_simple_echo() { + let result = run("echo hello").await.unwrap(); + assert!(result.is_success()); + assert!(result.stdout.contains("hello")); +} + +#[tokio::test] +async fn test_echo_with_multiple_words() { + let result = run("echo hello world").await.unwrap(); + assert!(result.is_success()); + assert!(result.stdout.contains("hello world")); +} + +#[tokio::test] +async fn test_command_with_arguments() { + let result = run("echo -n test").await.unwrap(); + assert!(result.is_success()); +} + +#[tokio::test] +async fn test_false_command_returns_nonzero() { + let result = run("false").await.unwrap(); + assert!(!result.is_success()); + assert_eq!(result.code, 1); +} + +#[tokio::test] +async fn test_true_command_returns_zero() { + let result = run("true").await.unwrap(); + assert!(result.is_success()); + assert_eq!(result.code, 0); +} + +// ============================================================================ +// ProcessRunner Tests +// ============================================================================ + +#[tokio::test] +async fn test_process_runner_basic() { + let mut runner = ProcessRunner::new( + "echo hello", + RunOptions { + mirror: false, + ..Default::default() + }, + ); + + let result = runner.run().await.unwrap(); + assert!(result.is_success()); + assert!(result.stdout.contains("hello")); +} + +#[tokio::test] +async fn test_process_runner_with_capture() { + let mut runner = ProcessRunner::new( + "echo captured", + RunOptions { + mirror: false, + capture: true, + ..Default::default() + }, + ); + + let result = runner.run().await.unwrap(); + assert!(result.stdout.contains("captured")); +} + +#[tokio::test] +async fn test_process_runner_is_finished() { + let mut runner = ProcessRunner::new("true", RunOptions::default()); + + assert!(!runner.is_finished()); + let _ = runner.run().await; + assert!(runner.is_finished()); +} + +#[tokio::test] +async fn test_process_runner_result() { + let mut runner = ProcessRunner::new("echo test", RunOptions::default()); + let _ = runner.run().await; + + let result = runner.result(); + assert!(result.is_some()); + assert!(result.unwrap().is_success()); +} + +// ============================================================================ +// Working Directory Tests +// ============================================================================ + +#[tokio::test] +async fn test_custom_working_directory() { + let dir = TempDir::new().unwrap(); + std::fs::write(dir.path().join("test.txt"), "content").unwrap(); + + let mut runner = ProcessRunner::new( + "ls", + RunOptions { + mirror: false, + cwd: Some(dir.path().to_path_buf()), + ..Default::default() + }, + ); + + let result = runner.run().await.unwrap(); + assert!(result.is_success()); + assert!(result.stdout.contains("test.txt")); +} + +// ============================================================================ +// Environment Variables Tests +// ============================================================================ + +#[tokio::test] +async fn test_custom_environment_variable() { + let mut env_vars = HashMap::new(); + env_vars.insert("MY_TEST_VAR".to_string(), "test_value".to_string()); + + let mut runner = ProcessRunner::new( + "printenv MY_TEST_VAR", + RunOptions { + mirror: false, + env: Some(env_vars), + ..Default::default() + }, + ); + + let result = runner.run().await.unwrap(); + assert!(result.is_success()); + assert!(result.stdout.contains("test_value")); +} + +// ============================================================================ +// Stdin Tests +// ============================================================================ + +#[tokio::test] +async fn test_stdin_content() { + let mut runner = ProcessRunner::new( + "cat", + RunOptions { + mirror: false, + stdin: StdinOption::Content("hello from stdin".to_string()), + ..Default::default() + }, + ); + + let result = runner.run().await.unwrap(); + assert!(result.is_success()); + assert!(result.stdout.contains("hello from stdin")); +} + +// ============================================================================ +// exec Function Tests +// ============================================================================ + +#[tokio::test] +async fn test_exec_with_options() { + let result = exec( + "echo test", + RunOptions { + mirror: false, + ..Default::default() + }, + ) + .await + .unwrap(); + + assert!(result.is_success()); + assert!(result.stdout.contains("test")); +} + +// ============================================================================ +// create Function Tests +// ============================================================================ + +#[tokio::test] +async fn test_create_and_run() { + let mut runner = create("echo created", RunOptions::default()); + let result = runner.run().await.unwrap(); + + assert!(result.is_success()); + assert!(result.stdout.contains("created")); +} + +// ============================================================================ +// Virtual Command Tests +// ============================================================================ + +#[tokio::test] +async fn test_virtual_echo() { + let result = run("echo virtual echo").await.unwrap(); + assert!(result.is_success()); + assert!(result.stdout.contains("virtual echo")); +} + +#[tokio::test] +async fn test_virtual_pwd() { + let result = run("pwd").await.unwrap(); + assert!(result.is_success()); + assert!(!result.stdout.is_empty()); +} + +#[tokio::test] +async fn test_virtual_true() { + let result = run("true").await.unwrap(); + assert!(result.is_success()); + assert_eq!(result.code, 0); +} + +#[tokio::test] +async fn test_virtual_false() { + let result = run("false").await.unwrap(); + assert!(!result.is_success()); + assert_eq!(result.code, 1); +} + +#[tokio::test] +async fn test_virtual_sleep() { + let start = std::time::Instant::now(); + let result = run("sleep 0.05").await.unwrap(); + let elapsed = start.elapsed(); + + assert!(result.is_success()); + assert!(elapsed.as_millis() >= 40); +} + +// ============================================================================ +// Stderr Tests +// ============================================================================ + +#[tokio::test] +async fn test_stderr_capture() { + let result = run("cat nonexistent_file_12345").await.unwrap(); + assert!(!result.is_success()); + assert!(!result.stderr.is_empty()); +} + +// ============================================================================ +// Exit Code Tests +// ============================================================================ + +#[tokio::test] +async fn test_exit_code_zero() { + let result = run("exit 0").await.unwrap(); + assert_eq!(result.code, 0); +} + +#[tokio::test] +async fn test_exit_code_nonzero() { + let result = run("exit 42").await.unwrap(); + assert_eq!(result.code, 42); +} + +// ============================================================================ +// Kill/Cancel Tests +// ============================================================================ + +#[tokio::test] +async fn test_kill_process() { + let mut runner = ProcessRunner::new( + "sleep 10", + RunOptions { + mirror: false, + ..Default::default() + }, + ); + + runner.start().await.unwrap(); + let kill_result = runner.kill(); + assert!(kill_result.is_ok()); +} diff --git a/rust/tests/shell_parser.rs b/rust/tests/shell_parser.rs new file mode 100644 index 0000000..240666b --- /dev/null +++ b/rust/tests/shell_parser.rs @@ -0,0 +1,296 @@ +//! Integration tests for shell parser +//! +//! These tests mirror the JavaScript shell parser tests + +use command_stream::shell_parser::{needs_real_shell, parse_shell_command, tokenize, ParsedCommand, TokenType}; + +// ============================================================================ +// Tokenizer Tests +// ============================================================================ + +#[test] +fn test_tokenize_simple_command() { + let tokens = tokenize("echo hello world"); + assert_eq!(tokens.len(), 4); // 3 words + EOF + assert!(matches!(tokens[0].token_type, TokenType::Word(_))); + assert!(matches!(tokens[1].token_type, TokenType::Word(_))); + assert!(matches!(tokens[2].token_type, TokenType::Word(_))); + assert!(matches!(tokens[3].token_type, TokenType::Eof)); +} + +#[test] +fn test_tokenize_with_and_operator() { + let tokens = tokenize("cmd1 && cmd2"); + assert!(tokens.iter().any(|t| matches!(t.token_type, TokenType::And))); +} + +#[test] +fn test_tokenize_with_or_operator() { + let tokens = tokenize("cmd1 || cmd2"); + assert!(tokens.iter().any(|t| matches!(t.token_type, TokenType::Or))); +} + +#[test] +fn test_tokenize_with_pipe() { + let tokens = tokenize("ls | grep foo"); + assert!(tokens.iter().any(|t| matches!(t.token_type, TokenType::Pipe))); +} + +#[test] +fn test_tokenize_with_semicolon() { + let tokens = tokenize("cmd1 ; cmd2"); + assert!(tokens.iter().any(|t| matches!(t.token_type, TokenType::Semicolon))); +} + +#[test] +fn test_tokenize_with_parentheses() { + let tokens = tokenize("(echo hello)"); + assert!(tokens.iter().any(|t| matches!(t.token_type, TokenType::LParen))); + assert!(tokens.iter().any(|t| matches!(t.token_type, TokenType::RParen))); +} + +#[test] +fn test_tokenize_with_redirect() { + let tokens = tokenize("echo hello > file.txt"); + assert!(tokens.iter().any(|t| matches!(t.token_type, TokenType::RedirectOut))); +} + +#[test] +fn test_tokenize_with_append_redirect() { + let tokens = tokenize("echo hello >> file.txt"); + assert!(tokens.iter().any(|t| matches!(t.token_type, TokenType::RedirectAppend))); +} + +#[test] +fn test_tokenize_with_input_redirect() { + let tokens = tokenize("cat < file.txt"); + assert!(tokens.iter().any(|t| matches!(t.token_type, TokenType::RedirectIn))); +} + +#[test] +fn test_tokenize_single_quoted_string() { + let tokens = tokenize("echo 'hello world'"); + assert_eq!(tokens.len(), 3); // echo + quoted string + EOF + if let TokenType::Word(w) = &tokens[1].token_type { + assert_eq!(w, "'hello world'"); + } else { + panic!("Expected Word token"); + } +} + +#[test] +fn test_tokenize_double_quoted_string() { + let tokens = tokenize("echo \"hello world\""); + assert_eq!(tokens.len(), 3); // echo + quoted string + EOF + if let TokenType::Word(w) = &tokens[1].token_type { + assert_eq!(w, "\"hello world\""); + } else { + panic!("Expected Word token"); + } +} + +#[test] +fn test_tokenize_mixed_operators() { + let tokens = tokenize("cmd1 && cmd2 || cmd3 ; cmd4"); + let ops: Vec<_> = tokens + .iter() + .filter(|t| matches!(t.token_type, TokenType::And | TokenType::Or | TokenType::Semicolon)) + .collect(); + assert_eq!(ops.len(), 3); +} + +// ============================================================================ +// Parser Tests +// ============================================================================ + +#[test] +fn test_parse_simple_command() { + let cmd = parse_shell_command("echo hello world").unwrap(); + match cmd { + ParsedCommand::Simple { cmd, args, .. } => { + assert_eq!(cmd, "echo"); + assert_eq!(args.len(), 2); + assert_eq!(args[0].value, "hello"); + assert_eq!(args[1].value, "world"); + } + _ => panic!("Expected Simple command"), + } +} + +#[test] +fn test_parse_command_with_quoted_args() { + let cmd = parse_shell_command("echo 'hello world'").unwrap(); + match cmd { + ParsedCommand::Simple { cmd, args, .. } => { + assert_eq!(cmd, "echo"); + assert_eq!(args.len(), 1); + assert_eq!(args[0].value, "hello world"); + assert!(args[0].quoted); + } + _ => panic!("Expected Simple command"), + } +} + +#[test] +fn test_parse_pipeline() { + let cmd = parse_shell_command("ls | grep foo | wc -l").unwrap(); + match cmd { + ParsedCommand::Pipeline { commands } => { + assert_eq!(commands.len(), 3); + } + _ => panic!("Expected Pipeline"), + } +} + +#[test] +fn test_parse_sequence_with_and() { + let cmd = parse_shell_command("cmd1 && cmd2").unwrap(); + match cmd { + ParsedCommand::Sequence { commands, operators } => { + assert_eq!(commands.len(), 2); + assert_eq!(operators.len(), 1); + assert!(matches!(operators[0], TokenType::And)); + } + _ => panic!("Expected Sequence"), + } +} + +#[test] +fn test_parse_sequence_with_or() { + let cmd = parse_shell_command("cmd1 || cmd2").unwrap(); + match cmd { + ParsedCommand::Sequence { commands, operators } => { + assert_eq!(commands.len(), 2); + assert_eq!(operators.len(), 1); + assert!(matches!(operators[0], TokenType::Or)); + } + _ => panic!("Expected Sequence"), + } +} + +#[test] +fn test_parse_sequence_mixed() { + let cmd = parse_shell_command("cmd1 && cmd2 || cmd3").unwrap(); + match cmd { + ParsedCommand::Sequence { commands, operators } => { + assert_eq!(commands.len(), 3); + assert_eq!(operators.len(), 2); + assert!(matches!(operators[0], TokenType::And)); + assert!(matches!(operators[1], TokenType::Or)); + } + _ => panic!("Expected Sequence"), + } +} + +#[test] +fn test_parse_with_redirect_out() { + let cmd = parse_shell_command("echo hello > output.txt").unwrap(); + match cmd { + ParsedCommand::Simple { cmd, args, redirects } => { + assert_eq!(cmd, "echo"); + assert_eq!(args.len(), 1); + assert_eq!(redirects.len(), 1); + assert!(matches!(redirects[0].redirect_type, TokenType::RedirectOut)); + assert_eq!(redirects[0].target, "output.txt"); + } + _ => panic!("Expected Simple command with redirect"), + } +} + +#[test] +fn test_parse_with_redirect_append() { + let cmd = parse_shell_command("echo hello >> output.txt").unwrap(); + match cmd { + ParsedCommand::Simple { redirects, .. } => { + assert_eq!(redirects.len(), 1); + assert!(matches!(redirects[0].redirect_type, TokenType::RedirectAppend)); + } + _ => panic!("Expected Simple command with redirect"), + } +} + +#[test] +fn test_parse_subshell() { + let cmd = parse_shell_command("(echo hello)").unwrap(); + match cmd { + ParsedCommand::Subshell { command } => { + match *command { + ParsedCommand::Simple { cmd, .. } => { + assert_eq!(cmd, "echo"); + } + _ => panic!("Expected Simple command inside subshell"), + } + } + _ => panic!("Expected Subshell"), + } +} + +#[test] +fn test_parse_subshell_with_sequence() { + let cmd = parse_shell_command("(echo hello) && echo world").unwrap(); + match cmd { + ParsedCommand::Sequence { commands, .. } => { + assert_eq!(commands.len(), 2); + assert!(matches!(commands[0], ParsedCommand::Subshell { .. })); + } + _ => panic!("Expected Sequence with Subshell"), + } +} + +#[test] +fn test_parse_complex_pipeline_and_sequence() { + let cmd = parse_shell_command("ls | grep foo && cat file | wc").unwrap(); + // This should be: (ls | grep foo) && (cat file | wc) + match cmd { + ParsedCommand::Sequence { commands, operators } => { + assert_eq!(commands.len(), 2); + assert_eq!(operators.len(), 1); + // Both commands should be pipelines + } + _ => panic!("Expected Sequence"), + } +} + +// ============================================================================ +// needs_real_shell Tests +// ============================================================================ + +#[test] +fn test_needs_real_shell_command_substitution() { + assert!(needs_real_shell("echo $(date)")); + assert!(needs_real_shell("echo `date`")); +} + +#[test] +fn test_needs_real_shell_variable_expansion() { + assert!(needs_real_shell("echo ${HOME}")); +} + +#[test] +fn test_needs_real_shell_glob_patterns() { + assert!(needs_real_shell("ls *.txt")); + assert!(needs_real_shell("ls file?.txt")); + assert!(needs_real_shell("ls [abc].txt")); +} + +#[test] +fn test_needs_real_shell_stderr_redirect() { + assert!(needs_real_shell("cmd 2>/dev/null")); +} + +#[test] +fn test_needs_real_shell_combined_redirect() { + assert!(needs_real_shell("cmd &>/dev/null")); +} + +#[test] +fn test_needs_real_shell_here_document() { + assert!(needs_real_shell("cat << EOF")); +} + +#[test] +fn test_needs_real_shell_simple_commands() { + assert!(!needs_real_shell("echo hello")); + assert!(!needs_real_shell("ls | grep foo")); + assert!(!needs_real_shell("cmd1 && cmd2")); +} diff --git a/rust/tests/utils.rs b/rust/tests/utils.rs new file mode 100644 index 0000000..97693d7 --- /dev/null +++ b/rust/tests/utils.rs @@ -0,0 +1,282 @@ +//! Integration tests for utility functions +//! +//! These tests mirror the JavaScript utility tests + +use command_stream::utils::{AnsiConfig, AnsiUtils, CommandResult, VirtualUtils, quote}; +use std::path::PathBuf; + +// ============================================================================ +// CommandResult Tests +// ============================================================================ + +#[test] +fn test_command_result_success() { + let result = CommandResult::success("hello"); + assert!(result.is_success()); + assert_eq!(result.stdout, "hello"); + assert_eq!(result.stderr, ""); + assert_eq!(result.code, 0); +} + +#[test] +fn test_command_result_success_empty() { + let result = CommandResult::success_empty(); + assert!(result.is_success()); + assert_eq!(result.stdout, ""); + assert_eq!(result.stderr, ""); + assert_eq!(result.code, 0); +} + +#[test] +fn test_command_result_error() { + let result = CommandResult::error("something went wrong"); + assert!(!result.is_success()); + assert_eq!(result.stdout, ""); + assert_eq!(result.stderr, "something went wrong"); + assert_eq!(result.code, 1); +} + +#[test] +fn test_command_result_error_with_code() { + let result = CommandResult::error_with_code("not found", 127); + assert!(!result.is_success()); + assert_eq!(result.code, 127); +} + +// ============================================================================ +// VirtualUtils Tests +// ============================================================================ + +#[test] +fn test_missing_operand_error() { + let result = VirtualUtils::missing_operand_error("cat"); + assert!(!result.is_success()); + assert!(result.stderr.contains("cat")); + assert!(result.stderr.contains("missing operand")); +} + +#[test] +fn test_invalid_argument_error() { + let result = VirtualUtils::invalid_argument_error("test", "invalid option"); + assert!(!result.is_success()); + assert!(result.stderr.contains("test")); + assert!(result.stderr.contains("invalid option")); +} + +#[test] +fn test_validate_args_sufficient() { + let args = vec!["arg1".to_string()]; + let error = VirtualUtils::validate_args(&args, 1, "cmd"); + assert!(error.is_none()); +} + +#[test] +fn test_validate_args_insufficient() { + let args = vec![]; + let error = VirtualUtils::validate_args(&args, 1, "cmd"); + assert!(error.is_some()); +} + +#[test] +fn test_resolve_path_absolute() { + let path = VirtualUtils::resolve_path("/absolute/path", None); + assert_eq!(path, PathBuf::from("/absolute/path")); +} + +#[test] +fn test_resolve_path_relative_with_cwd() { + let cwd = PathBuf::from("/home/user"); + let path = VirtualUtils::resolve_path("relative/path", Some(&cwd)); + assert_eq!(path, PathBuf::from("/home/user/relative/path")); +} + +#[test] +fn test_resolve_path_relative_without_cwd() { + let path = VirtualUtils::resolve_path("relative/path", None); + // Should resolve relative to current directory + assert!(path.ends_with("relative/path")); +} + +// ============================================================================ +// AnsiUtils Tests +// ============================================================================ + +#[test] +fn test_strip_ansi_basic() { + let text = "\x1b[31mRed text\x1b[0m"; + assert_eq!(AnsiUtils::strip_ansi(text), "Red text"); +} + +#[test] +fn test_strip_ansi_multiple() { + let text = "\x1b[1m\x1b[31mBold Red\x1b[0m normal"; + let result = AnsiUtils::strip_ansi(text); + assert_eq!(result, "Bold Red normal"); +} + +#[test] +fn test_strip_ansi_no_sequences() { + let text = "plain text"; + assert_eq!(AnsiUtils::strip_ansi(text), "plain text"); +} + +#[test] +fn test_strip_ansi_color_codes() { + let text = "\x1b[32mGreen\x1b[0m and \x1b[34mBlue\x1b[0m"; + assert_eq!(AnsiUtils::strip_ansi(text), "Green and Blue"); +} + +#[test] +fn test_strip_control_chars_basic() { + let text = "Hello\x00World"; + assert_eq!(AnsiUtils::strip_control_chars(text), "HelloWorld"); +} + +#[test] +fn test_strip_control_chars_preserve_newline() { + let text = "Line1\nLine2"; + assert_eq!(AnsiUtils::strip_control_chars(text), "Line1\nLine2"); +} + +#[test] +fn test_strip_control_chars_preserve_tab() { + let text = "Col1\tCol2"; + assert_eq!(AnsiUtils::strip_control_chars(text), "Col1\tCol2"); +} + +#[test] +fn test_strip_control_chars_preserve_carriage_return() { + let text = "Line1\r\nLine2"; + assert_eq!(AnsiUtils::strip_control_chars(text), "Line1\r\nLine2"); +} + +#[test] +fn test_strip_all() { + let text = "\x1b[31mRed\x00text\x1b[0m\nNewline"; + let result = AnsiUtils::strip_all(text); + assert_eq!(result, "Redtext\nNewline"); +} + +// ============================================================================ +// AnsiConfig Tests +// ============================================================================ + +#[test] +fn test_ansi_config_default() { + let config = AnsiConfig::default(); + assert!(config.preserve_ansi); + assert!(config.preserve_control_chars); +} + +#[test] +fn test_ansi_config_process_preserve_all() { + let config = AnsiConfig { + preserve_ansi: true, + preserve_control_chars: true, + }; + let text = "\x1b[31mRed\x00text\x1b[0m"; + let result = config.process_output(text); + assert_eq!(result, text); +} + +#[test] +fn test_ansi_config_process_strip_ansi_only() { + let config = AnsiConfig { + preserve_ansi: false, + preserve_control_chars: true, + }; + let text = "\x1b[31mRed text\x1b[0m"; + let result = config.process_output(text); + assert_eq!(result, "Red text"); +} + +#[test] +fn test_ansi_config_process_strip_control_only() { + let config = AnsiConfig { + preserve_ansi: true, + preserve_control_chars: false, + }; + let text = "Hello\x00World\nNewline"; + let result = config.process_output(text); + assert_eq!(result, "HelloWorld\nNewline"); +} + +#[test] +fn test_ansi_config_process_strip_all() { + let config = AnsiConfig { + preserve_ansi: false, + preserve_control_chars: false, + }; + let text = "\x1b[31mRed\x00text\x1b[0m"; + let result = config.process_output(text); + assert_eq!(result, "Redtext"); +} + +// ============================================================================ +// Quote Function Tests +// ============================================================================ + +#[test] +fn test_quote_empty_string() { + assert_eq!(quote(""), "''"); +} + +#[test] +fn test_quote_simple_string() { + assert_eq!(quote("hello"), "hello"); +} + +#[test] +fn test_quote_path() { + assert_eq!(quote("/path/to/file"), "/path/to/file"); +} + +#[test] +fn test_quote_with_spaces() { + assert_eq!(quote("hello world"), "'hello world'"); +} + +#[test] +fn test_quote_with_single_quote() { + assert_eq!(quote("it's"), "'it'\\''s'"); +} + +#[test] +fn test_quote_already_single_quoted() { + assert_eq!(quote("'hello'"), "'hello'"); +} + +#[test] +fn test_quote_already_double_quoted() { + let result = quote("\"hello\""); + assert!(result.contains("hello")); +} + +#[test] +fn test_quote_safe_characters() { + // These should not need quoting + assert_eq!(quote("file.txt"), "file.txt"); + assert_eq!(quote("path/to/file"), "path/to/file"); + assert_eq!(quote("key=value"), "key=value"); + assert_eq!(quote("user@host"), "user@host"); +} + +#[test] +fn test_quote_special_characters() { + // These need quoting + let result = quote("hello$world"); + assert!(result.starts_with("'") && result.ends_with("'")); + + let result = quote("hello;world"); + assert!(result.starts_with("'") && result.ends_with("'")); + + let result = quote("hello|world"); + assert!(result.starts_with("'") && result.ends_with("'")); +} + +#[test] +fn test_quote_newline() { + let result = quote("hello\nworld"); + assert!(result.contains("hello")); + assert!(result.contains("world")); +} diff --git a/rust/tests/virtual_commands.rs b/rust/tests/virtual_commands.rs new file mode 100644 index 0000000..0c9bbe8 --- /dev/null +++ b/rust/tests/virtual_commands.rs @@ -0,0 +1,199 @@ +//! Integration tests for virtual command system +//! +//! These tests mirror the JavaScript tests in js/tests/virtual.test.mjs + +use command_stream::commands::{ + are_virtual_commands_enabled, disable_virtual_commands, enable_virtual_commands, + CommandContext, VirtualCommandRegistry, +}; +use command_stream::{run, ProcessRunner, RunOptions}; + +// ============================================================================ +// Virtual Commands Enable/Disable Tests +// ============================================================================ + +#[test] +fn test_virtual_commands_default_enabled() { + assert!(are_virtual_commands_enabled()); +} + +#[test] +fn test_disable_virtual_commands() { + enable_virtual_commands(); // Ensure enabled first + disable_virtual_commands(); + assert!(!are_virtual_commands_enabled()); + enable_virtual_commands(); // Restore +} + +#[test] +fn test_enable_virtual_commands() { + disable_virtual_commands(); + enable_virtual_commands(); + assert!(are_virtual_commands_enabled()); +} + +// ============================================================================ +// Virtual Command Registry Tests +// ============================================================================ + +#[test] +fn test_registry_new() { + let registry = VirtualCommandRegistry::new(); + assert!(registry.list().is_empty()); +} + +#[test] +fn test_registry_contains() { + let registry = VirtualCommandRegistry::new(); + assert!(!registry.contains("nonexistent")); +} + +// ============================================================================ +// CommandContext Tests +// ============================================================================ + +#[test] +fn test_command_context_new() { + let ctx = CommandContext::new(vec!["arg1".to_string(), "arg2".to_string()]); + assert_eq!(ctx.args.len(), 2); + assert_eq!(ctx.args[0], "arg1"); + assert_eq!(ctx.args[1], "arg2"); +} + +#[test] +fn test_command_context_get_cwd() { + let ctx = CommandContext::new(vec![]); + let cwd = ctx.get_cwd(); + assert!(cwd.exists()); +} + +#[test] +fn test_command_context_with_cwd() { + let ctx = CommandContext { + args: vec![], + stdin: None, + cwd: Some(std::path::PathBuf::from("/tmp")), + env: None, + output_tx: None, + is_cancelled: None, + }; + assert_eq!(ctx.get_cwd(), std::path::PathBuf::from("/tmp")); +} + +#[test] +fn test_command_context_is_cancelled_default() { + let ctx = CommandContext::new(vec![]); + assert!(!ctx.is_cancelled()); +} + +#[test] +fn test_command_context_is_cancelled_with_fn() { + let ctx = CommandContext { + args: vec![], + stdin: None, + cwd: None, + env: None, + output_tx: None, + is_cancelled: Some(Box::new(|| true)), + }; + assert!(ctx.is_cancelled()); +} + +// ============================================================================ +// Built-in Command Execution Tests +// ============================================================================ + +#[tokio::test] +async fn test_execute_virtual_echo() { + enable_virtual_commands(); + let result = run("echo Hello World").await.unwrap(); + assert!(result.is_success()); + assert!(result.stdout.contains("Hello World")); +} + +#[tokio::test] +async fn test_execute_virtual_pwd() { + enable_virtual_commands(); + let result = run("pwd").await.unwrap(); + assert!(result.is_success()); + assert!(!result.stdout.is_empty()); +} + +#[tokio::test] +async fn test_execute_virtual_true() { + enable_virtual_commands(); + let result = run("true").await.unwrap(); + assert!(result.is_success()); + assert_eq!(result.code, 0); +} + +#[tokio::test] +async fn test_execute_virtual_false() { + enable_virtual_commands(); + let result = run("false").await.unwrap(); + assert!(!result.is_success()); + assert_eq!(result.code, 1); +} + +#[tokio::test] +async fn test_execute_virtual_exit() { + enable_virtual_commands(); + + let result = run("exit 0").await.unwrap(); + assert_eq!(result.code, 0); + + let result = run("exit 42").await.unwrap(); + assert_eq!(result.code, 42); +} + +#[tokio::test] +async fn test_execute_virtual_which() { + enable_virtual_commands(); + let result = run("which echo").await.unwrap(); + assert!(result.is_success()); + assert!(result.stdout.contains("shell builtin")); +} + +#[tokio::test] +async fn test_execute_virtual_sleep() { + enable_virtual_commands(); + let start = std::time::Instant::now(); + let result = run("sleep 0.1").await.unwrap(); + let elapsed = start.elapsed(); + + assert!(result.is_success()); + assert!(elapsed.as_millis() >= 90); + assert!(elapsed.as_millis() < 300); +} + +#[tokio::test] +async fn test_execute_echo_with_n_flag() { + enable_virtual_commands(); + let result = run("echo -n Hello").await.unwrap(); + assert!(result.is_success()); + assert_eq!(result.stdout.trim(), "Hello"); + // -n flag should not add newline + assert!(!result.stdout.ends_with("\n\n")); +} + +// ============================================================================ +// Process Runner with Virtual Commands Tests +// ============================================================================ + +#[tokio::test] +async fn test_process_runner_virtual_echo() { + enable_virtual_commands(); + let mut runner = ProcessRunner::new("echo test virtual", RunOptions::default()); + let result = runner.run().await.unwrap(); + assert!(result.is_success()); + assert!(result.stdout.contains("test virtual")); +} + +#[tokio::test] +async fn test_process_runner_virtual_pwd() { + enable_virtual_commands(); + let mut runner = ProcessRunner::new("pwd", RunOptions::default()); + let result = runner.run().await.unwrap(); + assert!(result.is_success()); + assert!(!result.stdout.is_empty()); +} diff --git a/temp-unicode-test.txt b/temp-unicode-test.txt new file mode 100644 index 0000000..e69de29