Repository navigation
First stable 2.x release. TimeWarp.Amuru and TimeWarp.Amuru.Tools ship together at 2.0.0 (one <Version> in source/Directory.Build.props). Install either package without --prerelease. The baseline is 1.1.1. This section consolidates everything that shipped in 2.0.0-beta.1 and 2.0.0-beta.2, plus the concurrency contract for CommandResult.
Breaking changes
TimeWarp.Amuru
WithStandardInput("")attaches immediate EOF.nullstill means the caller owns stdin. Callers who passed an empty string and expected "no stdin configured" now close the child's stdin immediately.TtyPassthroughAsyncthrowsInvalidOperationException(TtyPassthroughAsync requires console stdin; configured standard input cannot be used with a TTY) when standard input was configured, including the empty string. It also throwsTtyPassthroughAsync cannot run a pipeline; the upstream stage would be droppedwhen the command is a pipeline. The throw happens before the process starts.
TimeWarp.Amuru.Tools
dotnet builders:
DotNetPackBuilder.WithFrameworkis removed.dotnet packrejects--framework.DotNetWatchBuilder.WithInclude,WithExclude, andWithPropertyare removed.DotNetNuGetDeleteBuilder.WithConfigFileis removed.dotnet nuget deleteno longer accepts--configfile.WithNonInteractive()emits--non-interactive.DotNetTestBuilder.WithCollect()now requires a collector name:WithCollect(string dataCollector).DotNetDevCertsHttpsBuilder.WithExport()withoutWithExportPaththrows.DotNetRunBuilderthrows whenWithProjectandWithFileare both set.
Git:
Git.GetMasterWorktreePathAsyncis removed. UseGit.GetDefaultWorktreePathAsync.Git.UpdateMasterWorktreeAsyncis removed. UseGit.UpdateDefaultWorktreeAsync.Git.GetWorktreePathAsync,Git.UpdateBranchAsync, andGit.UpdateWorktreeAsyncno longer defaultbranchNameto"master". Pass the branch name. Working-directory overloads remain:(string branchName, CancellationToken cancellationToken = default).Git.FetchAsyncreturnsGitFetchResultinstead ofbool.Git.BranchExistsAsyncreturnsGitBranchExistsResult(Success,Exists,ErrorMessage) instead ofbool. It runsgit show-ref --verify --quiet. Exit code 1 (missing ref) isSuccesstrue andExistsfalse. Other failures setSuccessfalse and keep the git message.Git.ConfigureFetchRefspecAsyncreturnsGitConfigureFetchRefspecResultinstead ofbool.Git.SetRemoteHeadAutoAsyncreturnsGitSetRemoteHeadResultinstead ofbool.Git.WorktreeListPorcelainAsyncreturnsGitWorktreeListResult(Success,Porcelain,ErrorMessage) instead ofstring. Failure isSuccessfalse andPorcelainnull, notstring.Empty.GetCommitsAheadAsync(string branchName = "master", ...)still defaults the comparison branch to"master".GetCommitsAheadOfDefaultBranchAsyncremains the default-branch entry point.
Naming:
DotNetListPackagesBuilder.WithConfig(string)is renamed toWithConfigFile(string).DotNetWatchBuilder.WithTargetFramework(string)is renamed toWithFramework(string).
Behavior changes
PassthroughAsyncfeeds a configured standard-input string to the child. Console stdin is opened only when no string was configured. Callers who never set stdin keep the previous behavior.UpdateBranchAsyncfast-forwards a branch that is checked out in any worktree withgit pull --ff-only(pull.rebase=false). A branch that is not checked out still usesgit fetch origin <branch>:<branch>.BranchPathis set for a normal repository.UpdateWorktreeAsyncuses the same fast-forward-only pull. It no longer runs a plaingit pullthat could merge or rebase from config.GetDefaultBranchAsyncstrips only a leadingorigin/prefix.SelectWithFzfforwards the builder's option flags tofzf.FromInputwrites each item as its own stdin line.FromFileswalks the working directory.FromCommandkeeps quoted text, including spaces, as one argument.WithPreviewLabelPosandWithBorderLabelPoseach accept fzf'sN[:top|bottom]string and a columnint.
New
TimeWarp.Amuru
CommandResult.Pipe(string executable, CommandOptions options, params string[] arguments)and the matchingShellBuilder.Pipeoverload apply working directory, environment, and validation to the downstream stage.CommandMock.GetConfiguredStandardInputreturns the standard-input string recorded for the latest matching call. Null means the caller owns stdin.
Concurrency and output contract for CommandResult:
- Execution methods may be called concurrently on the same instance. Each call returns its own
CommandOutput, exit code, or streamed lines. LastOutputis the most recent execution on that instance. The last writer wins, so it is not meaningful under concurrent use. Prefer the returnedCommandOutput.CaptureAsyncandRunAndCaptureAsyncretain every line plus the combined strings.StreamStdoutAsync,StreamStderrAsync, andStreamCombinedAsynckeep one line at a time.RunAsyncandPassthroughAsyncdo not buffer output.- There is no size limit. The limit is process memory.
Native text search and in-place replace (TimeWarp.Amuru.Native.Text). Commands returns CommandOutput and does not throw. Direct returns typed results and throws.
SelectString (Commands.SelectString, Direct.SelectString, and Bash Grep / GrepDirect):
- Inputs are a path, an
IEnumerable<string>of paths, a root plus glob, or aTextReader/Stream. There is no string-content overload; pass aStringReader. A reader or stream with no path is labeled-. DirectstreamsTextMatch(Path,LineNumber,Line,Match,ContextBefore,ContextAfter) one line at a time.Commandswritespath:line:text. Exit codes match grep: 0 when a line matched, 1 when none matched, 2 on error.- A file whose first 8000 bytes contain a NUL byte and have no UTF-16 or UTF-32 BOM is binary and yields no matches.
ReplaceInFiles (Commands.ReplaceInFiles, Direct.ReplaceInFiles, and Bash Sed / SedDirect):
- Inputs are a pattern, a replacement (
$1and${name}expand to captures), and the same file inputs asSelectStringexcept a reader or stream. Directyields oneReplaceResultper file (Path,ReplacementCount,Changed,Preview,BackupPath), including files that did not change.Commandswritespath: N replacement(s)for each changed file. Exit 0 when the walk finishes, including when nothing matched. Exit 1 on a regex or I/O error.
Options (SelectStringOptions, ReplaceInFilesOptions): literal match (SimpleMatch), case, invert, context lines, per-file limits (MaxMatches counts matching lines, the same as grep -m; MaxReplacements), include and exclude globs, dry run, .bak backup, multiline, and singleline. Dry run fills Preview with a unified diff and writes nothing.
Write safety for ReplaceInFiles:
- A BOM is kept. A file with no BOM is written as UTF-8 without one.
- The first newline style is kept: CRLF when the first line feed follows a carriage return, CR when the file has no line feed, otherwise LF. A trailing newline is kept. A lone CR in an LF or CRLF file stays a literal character.
- A file whose text does not change is not rewritten, so its bytes and mtime stay.
- The write is a temp file in the target directory, then a move on that volume.
- A UTF-8 or no-BOM file that contains a NUL byte is skipped as binary, which includes UTF-16 and UTF-32 files without a BOM. Invalid bytes in the detected encoding throw
InvalidDataExceptionand are not rewritten as U+FFFD. - A symlink is followed and the target is rewritten, so the link stays a link. The
.bakis written beside the path that was passed in, only after the temp write succeeds and only when the text changes.
Per-command timeout. There is no process-wide default. CommandOptions.Timeout null means no limit. TimeoutGracePeriod defaults to CommandOptions.DefaultTimeoutGracePeriod (5 seconds).
CommandOptions.Timeout,TimeoutGracePeriod,WithTimeout(TimeSpan),WithTimeoutGracePeriod(TimeSpan), andDefaultTimeoutGracePeriodShellBuilder.WithTimeoutandShellBuilder.WithTimeoutGracePeriodCommandOutput.TimedOutCommandResult.TimeoutExitCodeis 124CommandResult.LastOutputis set when an execution finishes, including a timeout, and is left unchanged when the caller cancelsMockSetup.TimesOut()
On expiry the graceful signal is sent (SIGINT on Linux and macOS, Ctrl+C on Windows). The process tree is killed after the grace period. TtyPassthroughAsync sends SIGINT on Linux and macOS, then kills the tree after the grace period. Windows non-console children do not receive that signal, so the grace period ends in the kill.
Pipe uses one window: the shorter timeout, and the grace period from that shorter side. Equal timeouts keep the smaller grace. The window applies to the last stage. Earlier stages are cancelled when the pipe stops.
| Outcome | Caller sees |
|---|---|
| Timeout, default validation | TimedOut true, exit 124, Success false, no throw. The child's own exit status is not preserved |
Timeout, WithZeroExitCodeValidation() |
TimeoutException (Command timed out after {n} second(s) and was terminated.). LastOutput is set first |
TimesOut() with no timeout configured, strict validation |
TimeoutException (Command timed out and was terminated.) |
| Caller cancels | OperationCanceledException. LastOutput is unchanged |
TtyPassthroughAsync timeout |
Exit 124, including under strict validation. This method does not throw |
TimeWarp.Amuru.Tools
- Every command builder exposes
WithNoValidation()andWithZeroExitCodeValidation()with the same semantics asShellBuilder. Default validation staysNone.DotNetBuildergainedWithZeroExitCodeValidation()so derived builders inherit it. DotNetBuilder.WithTimeout(TimeSpan)andDotNetBuilder.WithTimeoutGracePeriod(TimeSpan). Derived builders inherit them. A builder that already holds aCommandOptionssnapshot keeps the timeout stored on that snapshot. The result contract is the core contract above.Fzf.FromCommandandSelectWithFzfpass the builder'sCommandOptionsthrough to the fzf pipe stage.- Repository-path overloads (null path means the process working directory) for
GetDefaultBranchAsync,UpdateDefaultBranchAsync,GetCommitsAheadAsync,GetCommitsAheadOfDefaultBranchAsync,GetWorktreePathAsync,GetDefaultWorktreePathAsync,UpdateBranchAsync,UpdateDefaultBranchAsync,UpdateWorktreeAsync, andUpdateDefaultWorktreeAsync. Existing zero-argument and(CancellationToken)calls of the default-branch helpers still compile.
Fixed
dotnetbuilders emit flags SDK 10.0.x accepts.WithTerminalLoggeremits one argument,--tl:<mode>, on build, restore, run, test, publish, and pack.nuget whytakes the project as a positional argument before the package id.SelectWithFzfno longer drops configured fzf options.FromInputno longer feeds items throughecho(so-n,-e, and-Estay data, including on Windows).FromFilesno longer shells out to Unixfind.FromCommandno longer splits the command on spaces.UpdateBranchAsyncupdates the currently checked-out branch.git fetch origin <branch>:<branch>refuses that case; the method pulls with--ff-onlyinstead.FindMainRepositoryFromWorktreeresolves a relativegitdir:against the worktree directory and returns the repository directory, not<repo>/.git.TimeWarp.Amuru.Toolsshipslib/net10.0/timewarp-amuru-tools.xml. IntelliSense covers every public Tools member.
Upgrade guide
One line per removed or renamed member, or per behavior that changes an existing call.
Git.GetMasterWorktreePathAsync→Git.GetDefaultWorktreePathAsyncGit.UpdateMasterWorktreeAsync→Git.UpdateDefaultWorktreeAsyncGit.GetWorktreePathAsync(branch)/Git.UpdateBranchAsync(branch)/Git.UpdateWorktreeAsync(branch)→ passbranch; the"master"default is goneGit.FetchAsync→ readGitFetchResultinstead ofboolGit.BranchExistsAsync→ readGitBranchExistsResult(Success,Exists,ErrorMessage) instead ofboolGit.ConfigureFetchRefspecAsync→ readGitConfigureFetchRefspecResultinstead ofboolGit.SetRemoteHeadAutoAsync→ readGitSetRemoteHeadResultinstead ofboolGit.WorktreeListPorcelainAsync→ readGitWorktreeListResult; failure isPorcelain == null, notstring.EmptyDotNetListPackagesBuilder.WithConfig→WithConfigFileDotNetWatchBuilder.WithTargetFramework→WithFrameworkDotNetPackBuilder.WithFramework→ removed (dotnet packhas no--framework)DotNetWatchBuilder.WithInclude→ removedDotNetWatchBuilder.WithExclude→ removedDotNetWatchBuilder.WithProperty→ removedDotNetNuGetDeleteBuilder.WithConfigFile→ removed; useWithNonInteractive()for--non-interactiveDotNetTestBuilder.WithCollect()→WithCollect(string dataCollector)DotNetDevCertsHttpsBuilder.WithExport()→ callWithExport().WithExportPath(path); export without a path throwsDotNetRunBuilder.WithProjecttogether withWithFile→ set only one; both together throwsWithStandardInput("")→ immediate EOF (null still means the caller owns stdin)TtyPassthroughAsyncwith configured stdin → throwsInvalidOperationException; leave stdin unconfigured for a real TTYTtyPassthroughAsyncon a pipeline → throws; usePassthroughAsyncwhen an upstream stage must feed the child