TimeWarp.Amuru 1.0.0-beta.17

Prefix Reserved
This is a prerelease version of TimeWarp.Amuru.
dotnet add package TimeWarp.Amuru --version 1.0.0-beta.17
                    
NuGet\Install-Package TimeWarp.Amuru -Version 1.0.0-beta.17
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="TimeWarp.Amuru" Version="1.0.0-beta.17" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="TimeWarp.Amuru" Version="1.0.0-beta.17" />
                    
Directory.Packages.props
<PackageReference Include="TimeWarp.Amuru" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add TimeWarp.Amuru --version 1.0.0-beta.17
                    
#r "nuget: TimeWarp.Amuru, 1.0.0-beta.17"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package TimeWarp.Amuru@1.0.0-beta.17
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=TimeWarp.Amuru&version=1.0.0-beta.17&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=TimeWarp.Amuru&version=1.0.0-beta.17&prerelease
                    
Install as a Cake Tool

Stars workflow Forks License Issues Open OpenSSF Scorecard

nuget nuget

Twitter Dotnet

Discord Twitter Twitter

<img src="https://raw.githubusercontent.com/TimeWarpEngineering/timewarpengineering.github.io/refs/heads/master/images/LogoNoMarginNoShadow.svg" alt="logo" height="120" style="float: right" />

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!

Installation

# Core library for shell scripting and process execution
dotnet add package TimeWarp.Amuru

Or reference in your C# runfile:

#:package TimeWarp.Amuru@<latest-version>

Optional: CLI Tools

# Global CLI tool with additional utilities (private package)
dotnet tool install --global TimeWarp.Ganda --source https://nuget.pkg.github.com/TimeWarpEngineering/index.json

The CLI tool includes various utilities like timestamp conversion, color generation, and more. See the Ganda repository for details.

Quick Start

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

using TimeWarp.Amuru;
using static System.Console;

// 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)
{
    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())
{
    WriteLine($"Log: {line}");
}

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

// Work with CommandOutput
var output = await Shell.Builder("docker", "ps").CaptureAsync();
WriteLine($"Exit code: {output.ExitCode}");
WriteLine($"Success: {output.Success}");
WriteLine($"Stdout: {output.Stdout}");
WriteLine($"Stderr: {output.Stderr}");
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 stream-based tools (fzf, REPLs)
await Shell.Builder("fzf")
    .PassthroughAsync();

// TUI applications (vim, nano, edit) need true TTY passthrough
await Shell.Builder("vim")
    .WithArguments("myfile.txt")
    .TtyPassthroughAsync();

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();

Conditional Configuration

The When() extension method allows you to apply configuration conditionally without breaking the fluent chain:

// Without When() - breaks the fluent chain
DotNetAddPackageBuilder builder = DotNet.AddPackage(packageName);
if (version != null)
{
    builder = builder.WithVersion(version);
}
else
{
    builder = builder.WithPrerelease();
}
await builder.CaptureAsync();

// With When() - keeps the fluent chain intact
await DotNet.AddPackage(packageName)
    .WithProject(projectFile)
    .When(version != null, b => b.WithVersion(version!))
    .When(version == null, b => b.WithPrerelease())
    .CaptureAsync();

// Works with all builders
await Shell.Builder("git")
    .WithArguments("push")
    .When(force, b => b.WithArguments("--force"))
    .When(dryRun, b => b.WithArguments("--dry-run"))
    .When(workDir != null, b => b.WithWorkingDirectory(workDir!))
    .RunAsync();

Available Extension Methods

All extension methods work on any builder that implements ICommandBuilder<T>:

When(condition, configure) - Apply configuration when condition is true

.When(version != null, b => b.WithVersion(version!))

WhenNotNull(value, configure) - Apply configuration when value is not null, passing the value

.WhenNotNull(version, (b, v) => b.WithVersion(v))  // Cleaner than When!

Unless(condition, configure) - Apply configuration when condition is false

.Unless(isProduction, b => b.WithVerbose())

Apply(configure) - Extract and reuse configuration logic

static DotNetBuildBuilder AddProductionSettings(DotNetBuildBuilder b) =>
  b.WithConfiguration("Release").WithNoRestore();

await DotNet.Build()
  .Apply(AddProductionSettings)
  .RunAsync();

ForEach(items, configure) - Apply configuration for each item

.ForEach(sources, (b, source) => b.WithSource(source))

Tap(action) - Side effects without modifying the builder (logging, debugging)

.Tap(b => Console.WriteLine($"Building with config: {b}"))

These extensions:

  • Maintain type safety and IntelliSense support
  • Keep method chains fluent and readable
  • Work with all command builders (Shell, DotNet, Fzf, etc.)
  • Enable functional programming patterns

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
  • Conditional Configuration: When() extension for fluent conditional logic
  • 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 stream-based tools, TtyPassthroughAsync() for TUI apps (vim, nano), 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() - Stream-based interactive tools (fzf, REPLs)
await Shell.Builder("fzf").PassthroughAsync();
// Returns: ExecutionResult
// Console output: piped through Console streams

// TtyPassthroughAsync() - True TTY for TUI applications (vim, nano, edit)
await Shell.Builder("vim", "file.txt").TtyPassthroughAsync();
// Returns: ExecutionResult
// Console output: inherits parent TTY (required for TUI apps)

// 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() ✅ Piped ExecutionResult Stream-based interactive (3%)
TtyPassthroughAsync() ✅ TTY ExecutionResult TUI apps (vim, nano) (1%)
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

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on TimeWarp.Amuru:

Package Downloads
TimeWarp.Jaribu

Lightweight testing helpers for single-file C# programs and scripts. Jaribu (Swahili: test/trial) provides TestRunner pattern and assertion helpers for executable .cs files.

GitHub repositories (1)

Showing the top 1 popular GitHub repositories that depend on TimeWarp.Amuru:

Repository Stars
TimeWarpEngineering/timewarp-nuru
Route-based CLI framework for .NET - Nuru means 'light' in Swahili
Version Downloads Last Updated
1.0.0-beta.17 101 12/12/2025
1.0.0-beta.16 374 12/11/2025
1.0.0-beta.15 340 10/21/2025
1.0.0-beta.14 135 10/21/2025
1.0.0-beta.13 417 10/8/2025
1.0.0-beta.12 150 9/30/2025
1.0.0-beta.11 146 9/25/2025
1.0.0-beta.10 273 9/16/2025
1.0.0-beta.9 222 9/15/2025
1.0.0-beta.8 214 8/28/2025
1.0.0-beta.7 195 8/28/2025
1.0.0-beta.6 194 8/28/2025
1.0.0-beta.5 200 8/27/2025
1.0.0-beta.4 194 8/26/2025
1.0.0-beta.3 69 8/23/2025
1.0.0-beta.2 95 8/22/2025
1.0.0-beta.1 110 8/3/2025