Skip to content

TimeWarpEngineering/timewarp-amuru

Repository files navigation

Stars workflow Forks License Issues Open OpenSSF Scorecard

nuget nuget

Twitter Dotnet

Discord Twitter Twitter

logo

TimeWarp.Amuru

Amuru means "command" in Swahili

TimeWarp.Amuru is a powerful fluent API library for elegant command-line execution in C#. It transforms shell scripting into a type-safe, IntelliSense-friendly experience with a simple static Builder() method, async operations, and proper error handling.

Designed for modern C# developers, TimeWarp.Amuru brings the power of shell scripting directly into your C# code. Whether you're building automation tools, DevOps scripts, or integrating command-line tools into your applications, TimeWarp.Amuru provides the elegant, type-safe API you need.

Why TimeWarp.Amuru?

  • Zero Learning Curve: If you know C#, you already know how to use TimeWarp.Amuru
  • IntelliSense Everything: Full IDE support with autocomplete, parameter hints, and documentation
  • Type Safety: Catch errors at compile-time, not runtime
  • No String Escaping Hell: Use C# arrays and parameters naturally
  • Built for .NET 10: Modern C# features and performance optimizations
  • Script or Library: Use it in quick scripts or production applications

Give a Star! ⭐

If you find this project useful, please give it a star. Thanks!

Quick Start

#!/usr/bin/dotnet run
#:package TimeWarp.Amuru

using TimeWarp.Amuru;

// Default behavior - stream to console (like bash/PowerShell)
await Shell.Builder("npm", "install").RunAsync();

// Capture output when needed
var result = await Shell.Builder("git", "status").CaptureAsync();
if (result.Success)
{
    Console.WriteLine($"Git says: {result.Stdout}");
}

// Stream large files without memory issues
await foreach (var line in Shell.Builder("tail", "-f", "/var/log/app.log").StreamStdoutAsync())
{
    Console.WriteLine($"Log: {line}");
}

// Chain commands with pipelines
var result = await Shell.Builder("find", ".", "-name", "*.cs")
    .Pipe("grep", "async")
    .CaptureAsync();
Console.WriteLine($"Found {result.Lines.Length} async files");

// Work with CommandOutput
var output = await Shell.Builder("docker", "ps").CaptureAsync();
Console.WriteLine($"Exit code: {output.ExitCode}");
Console.WriteLine($"Success: {output.Success}");
Console.WriteLine($"Stdout: {output.Stdout}");
Console.WriteLine($"Stderr: {output.Stderr}");
Console.WriteLine($"Combined: {output.Combined}");

// Use the fluent builder API for complex commands
var result = await Shell.Builder("git")
    .WithArguments("log", "--oneline", "-n", "10")
    .WithWorkingDirectory("/my/repo")
    .WithCancellationToken(cancellationToken)
    .CaptureAsync();

// Provide standard input to commands
var grepResult = await Shell.Builder("grep")
    .WithArguments("pattern")
    .WithStandardInput("line1\nline2 with pattern\nline3")
    .CaptureAsync();

// Interactive selection with Fzf
var selectedFile = await Fzf.Builder()
    .FromInput("file1.txt", "file2.txt", "file3.txt")
    .WithPreview("cat {}")
    .SelectAsync();

// Interactive pipeline - find and select files
var chosenFile = await Shell.Builder("find")
    .WithArguments(".", "-name", "*.cs")
    .Pipe("fzf", "--preview", "head -20 {}")
    .SelectAsync();

// Full interactive mode for editors, REPLs, etc.
await Shell.Builder("vim")
    .WithArguments("myfile.txt")
    .PassthroughAsync();

Installation

dotnet add package TimeWarp.Amuru --prerelease

Or reference in your C# script:

#:package TimeWarp.Amuru@1.0.0-beta.3

Check out the latest NuGet package: TimeWarp.Amuru nuget

DotNet Commands

// Global dotnet options
var sdks = await DotNet.WithListSdks().CaptureAsync();
var runtimes = await DotNet.WithListRuntimes().CaptureAsync();
var version = await DotNet.WithVersion().CaptureAsync();

// Base builder for custom arguments
var result = await DotNet.Builder()
    .WithArguments("--list-sdks")
    .CaptureAsync();

// Build and test with streaming output
await DotNet.Build()
    .WithConfiguration("Release")
    .RunAsync();

await DotNet.Test()
    .WithFilter("Category=Unit")
    .RunAsync();

Key Features

  • Shell-Like Default: RunAsync() streams to console just like bash/PowerShell
  • Explicit Capture: CaptureAsync() for when you need to process output
  • Memory-Efficient Streaming: IAsyncEnumerable for large data without buffering
  • Complete Output Access: CommandOutput with Stdout, Stderr, Combined, and ExitCode
  • Fluent Interface: Chain operations naturally with .Pipe() and builder methods
  • Async-First Design: All operations support modern async/await patterns
  • Smart Error Handling: Commands throw on errors by default, with opt-in graceful degradation
  • Pipeline Support: Chain commands with Unix-like pipe semantics
  • Standard Input Support: Provide stdin to commands with .WithStandardInput()
  • NO CACHING Philosophy: Like shells, commands run fresh every time
  • Configuration Options: Working directory, environment variables, and more
  • Cancellation Support: Full CancellationToken support for timeouts and manual cancellation
  • Cross-Platform: Works on Windows, Linux, and macOS
  • Command Builders: Fluent builders for complex commands (DotNet, Fzf, Ghq, Gwq)
  • Interactive Commands: PassthroughAsync() for editors, SelectAsync() for selection tools
  • .NET 10 Script Support: AppContext extensions and ScriptContext for file-based apps

Output Handling

Core API Methods

TimeWarp.Amuru provides clear, purpose-built methods for different scenarios:

// RunAsync() - Default shell behavior, streams to console
await Shell.Builder("npm", "install").RunAsync();
// Returns: exit code (int)
// Console output: real-time streaming

// CaptureAsync() - Silent execution with full output capture
var result = await Shell.Builder("git", "status").CaptureAsync();
// Returns: CommandOutput with all streams
// Console output: none (silent)

// PassthroughAsync() - Full terminal control for interactive tools
await Shell.Builder("vim", "file.txt").PassthroughAsync();
// Returns: void
// Console output: direct terminal passthrough

// SelectAsync() - Selection tools (shows UI, captures selection)
var selected = await Fzf.Builder()
    .FromInput("option1", "option2")
    .SelectAsync();
// Returns: selected string
// Console output: UI on stderr, selection captured from stdout

The CommandOutput Type

var output = await Shell.Builder("docker", "ps").CaptureAsync();

// Access individual streams
Console.WriteLine($"Stdout: {output.Stdout}");
Console.WriteLine($"Stderr: {output.Stderr}");
Console.WriteLine($"Combined: {output.Combined}"); // Both in chronological order

// Check status
Console.WriteLine($"Exit code: {output.ExitCode}");
Console.WriteLine($"Success: {output.Success}"); // ExitCode == 0

// Convenience properties for line processing
foreach (var line in output.Lines) // Combined.Split('\n')
{
    ProcessLine(line);
}

Streaming Large Data

For commands that produce large amounts of data:

// Stream lines as they arrive (no buffering)
await foreach (var line in Shell.Builder("tail", "-f", "/var/log/app.log")
    .StreamStdoutAsync(cancellationToken))
{
    Console.WriteLine($"Log: {line}");
}

// Stream with LINQ-style processing
var errorLines = Shell.Builder("cat", "huge.log")
    .StreamStdoutAsync()
    .Where(line => line.Contains("ERROR"))
    .Take(100);

await foreach (var error in errorLines)
{
    LogError(error);
}

Method Comparison

Method Console Output Captures Returns Primary Use Case
RunAsync() ✅ Real-time Exit code Default scripting (80%)
CaptureAsync() ❌ Silent ✅ All streams CommandOutput Process output (15%)
PassthroughAsync() ✅ Direct void Interactive tools (4%)
SelectAsync() ✅ UI only ✅ Selection string Selection tools (1%)
StreamStdoutAsync() ✅ As stream IAsyncEnumerable Large data

Design Philosophy: NO CACHING

TimeWarp.Amuru intentionally does NOT cache command results:

// Shells don't cache - neither do we
await Shell.Builder("date").RunAsync();  // Shows current time
await Shell.Builder("date").RunAsync();  // Shows NEW current time

// If you need caching, it's trivial in C#:
private static CommandOutput? cachedResult;
var result = cachedResult ??= await Shell.Builder("expensive-command").CaptureAsync();

Why no caching?

  • Commands can have side effects
  • Results change over time
  • Shells don't cache
  • Users can trivially cache in C# if needed

Error Handling

TimeWarp.Amuru provides intelligent error handling that distinguishes between different failure types:

Default Behavior (Throws Exceptions)

// Throws CommandExecutionException on non-zero exit code
await Shell.Builder("ls", "/nonexistent").RunAsync();

// CaptureAsync also throws on failure by default
var result = await Shell.Builder("git", "invalid-command").CaptureAsync();

Graceful Degradation (Opt-in)

// Disable validation for graceful degradation
var result = await Shell.Builder("ls", "/nonexistent")
    .WithValidation(CommandResultValidation.None)
    .CaptureAsync();

if (!result.Success)
{
    Console.WriteLine($"Command failed with exit code: {result.ExitCode}");
    Console.WriteLine($"Error: {result.Stderr}");
}

Cancellation and Timeouts

// With explicit cancellation token
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
await Shell.Builder("long-running-command")
    .RunAsync(cts.Token);

// With timeout via builder
await Shell.Builder("slow-command")
    .WithTimeout(TimeSpan.FromSeconds(10))
    .RunAsync();

// Timeout and external token are combined
await Shell.Builder("another-command")
    .WithTimeout(TimeSpan.FromSeconds(5))
    .RunAsync(userCancellationToken);

Testing and Mocking

TimeWarp.Amuru provides built-in support for mocking commands during testing through the CliConfiguration class:

Basic Mocking

// Set up mock commands for testing
CliConfiguration.SetCommandPath("fzf", "/path/to/mock/fzf");
CliConfiguration.SetCommandPath("git", "/path/to/mock/git");

// Your code using these commands will now use the mocks
var selected = await Fzf.Builder()
    .FromInput("option1", "option2", "option3")
    .SelectAsync(); // Uses mock fzf

var status = await Shell.Builder("git", "status")
    .CaptureAsync(); // Uses mock git

// Clean up after tests
CliConfiguration.Reset();

Creating Mock Executables

// Create a simple mock script
File.WriteAllText("/tmp/mock-fzf", "#!/bin/bash\necho 'mock-selection'");
await Shell.Builder("chmod", "+x", "/tmp/mock-fzf").RunAsync();

// Configure TimeWarp.Amuru to use it
CliConfiguration.SetCommandPath("fzf", "/tmp/mock-fzf");

// Now SelectAsync will use the mock
var selected = await Fzf.Builder()
    .FromInput("a", "b", "c")
    .SelectAsync(); // Returns "mock-selection"

Testing Interactive Commands

For commands like fzf that are normally interactive, you can either:

  1. Use mock executables as shown above
  2. Use non-interactive modes (e.g., fzf --filter)

API Reference

  • CliConfiguration.SetCommandPath(command, path) - Set custom executable path
  • CliConfiguration.ClearCommandPath(command) - Remove custom path for a command
  • CliConfiguration.Reset() - Clear all custom paths
  • CliConfiguration.HasCustomPath(command) - Check if command has custom path
  • CliConfiguration.AllCommandPaths - Get all configured paths

.NET 10 File-Based App Support

TimeWarp.Amuru provides specialized support for .NET 10's new file-based apps (single-file C# scripts) with AppContext extensions and ScriptContext for directory management.

  • AppContext Extensions - Clean access to script metadata without magic strings
  • ScriptContext - Automatic working directory management with cleanup guarantees
  • ProcessExit Handling - Cleanup runs even with Environment.Exit()

📖 See the documentation for detailed usage guides and examples.

Architecture

TimeWarp.Amuru is built on several key architectural principles:

  • Static Entry Point: Minimal ceremony with global Builder() method
  • Immutable Design: Thread-safe, readonly objects throughout
  • Integration Testing: Real command validation over mocking
  • Predictable Error Handling: Clear distinction between failure types
  • Opt-in Complexity: Advanced features available when needed

See our Architectural Decision Records for detailed design rationale.

Documentation

Example Scripts

See Spikes/CsScripts/ for example scripts demonstrating TimeWarp.Amuru usage patterns.

Unlicense

License
This project is licensed under the Unlicense.

Contributing

Your contributions are welcome! Before starting any work, please open a discussion.

See our Kanban board for current development tasks and priorities.

Contact

If you have an issue and don't receive a timely response, feel free to reach out on our Discord server.

Discord

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •