TimeWarp.Amuru v1.0 introduces a redesigned API that better aligns with shell scripting expectations while providing more complete access to command output. This guide will help you migrate from the old API to the new one.
The old API provided different methods for different output formats (GetStringAsync(), GetLinesAsync(), ExecuteAsync()), which was confusing and lost important information (stderr was often discarded).
The new API provides methods that clearly indicate their behavior:
RunAsync()- Default shell behavior (stream to console)CaptureAsync()- Silent execution with full output capturePassthroughAsync()- Interactive toolsSelectAsync()- Selection tools
| Old Method | New Approach | Notes |
|---|---|---|
GetStringAsync() |
CaptureAsync().Result.Stdout |
Now preserves stderr |
GetLinesAsync() |
CaptureAsync().Result.Lines |
Includes all output |
ExecuteAsync() |
CaptureAsync() or RunAsync() |
Choose based on needs |
ExecuteInteractiveAsync() |
PassthroughAsync() |
Clearer name |
GetStringInteractiveAsync() |
SelectAsync() |
Purpose-built for selection |
Cached() |
No replacement | NO CACHING by design |
CommandResult→CommandBuilder(for building)ExecutionResult→CommandOutput(for results)- "Execute" terminology → "Run" or "Capture" (clearer intent)
// Confusing - "Execute" doesn't actually show output
await Shell.Builder("npm", "install").ExecuteAsync();
// Lost stderr!
var output = await Shell.Builder("git", "status").GetStringAsync();// Clear - streams to console like a shell
await Shell.Builder("npm", "install").RunAsync();
// Captures everything
var result = await Shell.Builder("git", "status").CaptureAsync();
var output = result.Stdout; // or result.Combined for both stdout and stderr// Lost stderr, no exit code access
var text = await Shell.Builder("cat", "file.txt").GetStringAsync();
var lines = await Shell.Builder("ls").GetLinesAsync();// Full access to all output
var result = await Shell.Builder("cat", "file.txt").CaptureAsync();
var text = result.Stdout;
var lines = result.Lines;
var exitCode = result.ExitCode;
var success = result.Success;var filtered = await Shell.Builder("find", ".", "-name", "*.cs")
.Pipe("grep", "async")
.GetLinesAsync(); // Lost stderr from both commandsvar result = await Shell.Builder("find", ".", "-name", "*.cs")
.Pipe("grep", "async")
.CaptureAsync();
var filtered = result.Lines; // All output preserved
if (!result.Success)
{
Console.WriteLine($"Pipeline failed: {result.Stderr}");
}// Unclear what this does
await Shell.Builder("vim", "file.txt").ExecuteInteractiveAsync();
// Confusing name
var selected = await Fzf.Builder()
.FromInput("a", "b", "c")
.GetStringInteractiveAsync();// Clear - full terminal passthrough
await Shell.Builder("vim", "file.txt").PassthroughAsync();
// Purpose-built for selection
var selected = await Fzf.Builder()
.FromInput("a", "b", "c")
.SelectAsync();// Had to use special options object
var options = new CommandOptions().WithValidation(CommandResultValidation.None);
var result = await Shell.Builder("git", "status", options).GetStringAsync();
// result is empty string on failure - no error info!// Fluent API with full error information
var result = await Shell.Builder("git", "status")
.WithValidation(CommandResultValidation.None)
.CaptureAsync();
if (!result.Success)
{
Console.WriteLine($"Exit code: {result.ExitCode}");
Console.WriteLine($"Error: {result.Stderr}");
}// Built-in caching (now removed)
var cached = Shell.Builder("expensive-command").Cached();
var result1 = await cached.GetStringAsync();
var result2 = await cached.GetStringAsync(); // Used cache// NO CACHING - be explicit if you need it
private static CommandOutput? cachedResult;
var result = cachedResult ??= await Shell.Builder("expensive-command").CaptureAsync();
// Or use any standard C# caching pattern you prefer// No built-in cancellation support
await Shell.Builder("long-command").ExecuteAsync();// All async methods accept CancellationToken
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
await Shell.Builder("long-command").RunAsync(cts.Token);
// Or use timeout
await Shell.Builder("slow-command")
.WithTimeout(TimeSpan.FromSeconds(10))
.RunAsync();// Old: Had to use ExecuteAsync() which didn't stream
await Shell.Builder("npm", "install").ExecuteAsync();
// New: Use RunAsync() for default shell behavior
await Shell.Builder("npm", "install").RunAsync();// Old: Lost stderr if command failed
var json = await Shell.Builder("aws", "s3", "ls", "--output", "json").GetStringAsync();
// New: Full error information available
var result = await Shell.Builder("aws", "s3", "ls", "--output", "json").CaptureAsync();
if (result.Success)
{
var json = JsonSerializer.Deserialize<S3Output>(result.Stdout);
}
else
{
Console.WriteLine($"AWS CLI failed: {result.Stderr}");
}// Old: No way to see output while capturing
await Shell.Builder("dotnet", "test").ExecuteAsync();
// New: Stream to console by default
var exitCode = await Shell.Builder("dotnet", "test").RunAsync();
if (exitCode != 0)
{
throw new Exception($"Tests failed with exit code {exitCode}");
}// Old: Had to load entire file into memory
var lines = await Shell.Builder("cat", "huge.log").GetLinesAsync();
// New: Stream lines without buffering
await foreach (var line in Shell.Builder("cat", "huge.log").StreamStdoutAsync())
{
ProcessLine(line);
}-
Do you want to see output in the console?
- Yes, and don't need to capture →
RunAsync() - No, need to process it →
CaptureAsync()
- Yes, and don't need to capture →
-
Is it an interactive tool (vim, nano, REPL)?
- Yes →
PassthroughAsync()
- Yes →
-
Is it a selection tool (fzf, dialog)?
- Yes →
SelectAsync()
- Yes →
-
Processing very large output?
- Yes →
StreamStdoutAsync()orStreamStderrAsync()
- Yes →
A: Replace ExecuteAsync() with RunAsync(). The old ExecuteAsync() captured output, the new RunAsync() streams it.
A: Use CaptureAsync() which returns CommandOutput with separate Stdout and Stderr properties.
A: Use CaptureAsync().Result.Stdout to get just stdout, or .Combined for both streams.
A: Caching was removed by design. Implement your own caching if needed:
private static readonly Dictionary<string, CommandOutput> cache = new();
var result = cache.TryGetValue(key, out var cached)
? cached
: cache[key] = await Shell.Builder(cmd).CaptureAsync();A: Use the new timeout support:
await Shell.Builder("slow-command")
.WithTimeout(TimeSpan.FromSeconds(30))
.RunAsync();- Predictable Behavior: Method names clearly indicate what happens
- Complete Information: Never lose stderr or exit codes
- Shell-Like Defaults:
RunAsync()behaves like running in a shell - Better Error Handling: Full access to all error information
- Memory Efficient: Streaming APIs for large data
- Cancellation Support: All methods accept CancellationToken
- No Hidden State: No caching means no surprises
If you encounter issues not covered in this guide:
- Check the updated README.md for examples
- Review the Architecture documents in
/Analysis/Architecture/ - Look at test files for usage patterns
- Open an issue on GitHub with your specific migration challenge