Winix.Peep
0.4.0
Prefix Reserved
dotnet tool install --global Winix.Peep --version 0.4.0
dotnet new tool-manifest
dotnet tool install --local Winix.Peep --version 0.4.0
#tool dotnet:?package=Winix.Peep&version=0.4.0
nuke :add-package Winix.Peep --version 0.4.0
peep
Run a command repeatedly and display output on a refreshing screen.
Supports interval polling, file-watch triggers, diff highlighting, time-machine history, and auto-exit conditions.
watch + entr replacement (and works on Linux/macOS too).
Install
Scoop (Windows)
scoop bucket add winix https://github.com/Yortw/winix
scoop install winix/peep
Winget (Windows, stable releases)
winget install Winix.Peep
.NET Tool (cross-platform)
dotnet tool install -g Winix.Peep
Direct Download
Download native binaries from GitHub Releases.
Usage
peep [options] [--] <command> [args...]
Examples
# Watch a command every 2 seconds (default)
peep git status
# Custom interval
peep -n 5 df -h
# Re-run on file changes (no interval polling)
peep -w "src/**/*.cs" dotnet test
# Combine interval + file watching
peep -n 10 -w "*.config" dotnet build
# Highlight differences between runs
peep -d kubectl get pods
# Exit when output changes
peep -g curl -s https://api.example.com/status
# Exit when command succeeds (exit code 0)
peep --exit-on-success -- dotnet build
# Exit when output matches a regex
peep --exit-on-match "READY" -- kubectl get pods
# Run once and display (no loop)
peep --once -- docker ps
# JSON summary on exit (for scripts)
peep --json -n 5 -- dotnet test
# Use -- when child args look like peep flags
peep -- myapp --help
Interactive Controls
While running, peep responds to keyboard input:
| Key | Action |
|---|---|
q / Ctrl+C |
Quit |
Space |
Pause/unpause display |
r / Enter |
Force immediate re-run |
d |
Toggle diff highlighting |
Up/Down |
Scroll while paused |
PgUp/PgDn |
Scroll by page |
Left/Right |
Time travel (older/newer snapshots) |
t |
History overlay (browse all snapshots) |
? |
Show/hide help overlay |
Escape |
Exit time-machine or close overlay |
Time Machine
Press Left to enter time-machine mode, browsing historical snapshots of command output. Use Left/Right to navigate, t for an overview, Enter to jump to a specific snapshot, and Space or Escape to return to live mode.
Options
| Option | Description |
|---|---|
-n, --interval N |
Seconds between runs (default: 2) |
-w, --watch GLOB |
Re-run on file changes matching glob (repeatable) |
--debounce N |
Milliseconds to debounce file changes (default: 300) |
--history N |
Max history snapshots to retain (default: 1000, 0=unlimited) |
-g, --exit-on-change |
Exit when output changes |
--exit-on-success |
Exit when command returns exit code 0 |
-e, --exit-on-error |
Exit when command returns non-zero |
--exit-on-match PAT |
Exit when output matches regex (repeatable) |
-d, --differences |
Highlight changed lines between runs |
--no-gitignore |
Disable automatic .gitignore filtering for --watch |
--once |
Run once, display, and exit |
-t, --no-header |
Hide the header lines |
--json |
JSON summary to stderr on exit |
--json-output |
Include last captured output in JSON (implies --json) |
--no-color |
Disable colored output |
--color[=auto\|always\|never] |
Colored output: auto (default when omitted), always, or never. |
--version |
Show version |
-h, --help |
Show help |
File Watching
When --watch is used, peep monitors for file changes matching the glob pattern. Multiple patterns can be specified. By default, files matching .gitignore rules are excluded (disable with --no-gitignore).
If only --watch is specified (no -n), interval polling is disabled — peep only re-runs on file changes. If both are specified, either trigger causes a re-run.
Exit Codes
| Code | Meaning |
|---|---|
| 0 | Auto-exit condition met, or manual quit with last child exit 0 |
| N | Last child process exit code (manual quit) |
| 125 | Usage error (bad arguments) |
| 126 | Command not executable |
| 127 | Command not found |
Colour
- Automatic: colour when outputting to a terminal, plain when piped
--colorforces colour on (overridesNO_COLOR)--no-colorforces colour off- Respects the
NO_COLORenvironment variable (no-color.org)
Part of Winix
peep is part of the Winix CLI toolkit.
| Product | Versions 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. |
This package has no dependencies.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.4.0 | 189 | 6/13/2026 |
| 0.3.0 | 135 | 5/26/2026 |
| 0.3.0-rc2 | 113 | 5/10/2026 |
| 0.2.0 | 126 | 4/16/2026 |
| 0.2.0-test4 | 126 | 4/15/2026 |
| 0.1.0 | 130 | 4/2/2026 |
| 0.1.0-preview.4 | 76 | 3/30/2026 |
| 0.1.0-preview.3 | 70 | 3/30/2026 |
| 0.1.0-preview.2 | 70 | 3/30/2026 |
| 0.1.0-preview.1 | 70 | 3/30/2026 |
## [0.3.0] - 2026-05-10
### Changed
- `--json` / `--json-output` envelopes now reach every exit path, including parser errors, missing-command, regex-parse errors, and unexpected runtime exceptions in once-mode. Pre-fix, several validation early-returns and the once-mode catch list emitted bare stderr lines, breaking JSON-aware automation that decided "envelope or not" by inspecting `--json-output`.
- JSON `last_output` field is now trimmed of trailing newlines to match `bash $(cmd)` command-substitution semantics. Internal newlines are preserved unchanged; the captured `PeepResult.Output` stays byte-faithful for stdout / `--exit-on-match` / history-diff consumers.
- Captured child output is normalised to `\n` line endings at the API boundary, so JSON envelopes and `--exit-on-match` regexes behave identically across Windows and POSIX (pre-fix, `dotnet --version` produced `"10.0.200\r\n"` on Windows but `"10.0.200\n"` on Linux).
- `--exit-on-match` and `--exit-on-change` now return exit code 0 for the auto-exit success case, matching the documented "0 = Auto-exit condition met" contract. Pre-fix the child's last exit code (often non-zero on the run that triggered the match) was passed through.
- Once-mode JSON envelope now emits `history_retained: 0` so the field is always present, matching `--describe`'s declared `int|null` schema.
### Fixed
- `peep <command>` no longer crashes with an SR resource-key exception (`InvalidOperationException: InvalidOperation_ConsoleKeyAvailableOnFile`) when stdin is redirected (pipes, `/dev/null`, CI, parent-process `RedirectStandardInput=true`). The redraw loop now probes `Console.IsInputRedirected` once at entry and skips the keyboard branch on every tick when stdin is not a terminal.
- Two related stream-merge bugs fixed: child stdout and stderr are now merged line-atomically rather than in 4096-char chunks (a chunk from one stream could land mid-line of the other, producing output like `"this naCould not execute...me could not be found"`), and the line buffer enforces the 64 MB output cap mid-stream so a child writing without `\n` (binary dumps, unfolded base64, large `curl` payloads) can no longer bypass the OOM bound.
- File-change triggers are no longer silently dropped while a child is running. The previous `Interlocked.CompareExchange` ordering consumed the trigger flag before checking whether a child was already running, so saves during a long `dotnet test` were silently lost.
- ANSI-strip regex now also matches OSC sequences (`ESC ] ... BEL` and `ESC ] ... ESC \`) used by modern shells, fish, oh-my-posh, gcc, and OSC-8 hyperlinks. Pre-fix `--exit-on-match` patterns failed against lines carrying a leading title-set escape, and `--json-output last_output` leaked raw escape bytes into the JSON envelope.
- `RegexMatchTimeoutException` from `--exit-on-match` against a pathological pattern is now caught (treated as non-match) with a one-shot stderr warning per pattern. Pre-fix the timeout was silently swallowed and the user's auto-exit condition never fired with no diagnostic.
- Captured child output is bounded by a 64 MB cap with a single trailing truncation marker; readers continue draining after the cap so the child sees clean EOF rather than wedging on a full pipe. A runaway child can no longer OOM peep itself.
- `GitIgnoreChecker` no longer leaks orphan `git` processes when `git rev-parse` / `check-ignore` hangs (credential helper prompts, network FS, antivirus scanning `.pack` files). Hung calls are killed on timeout, gitignore filtering is process-wide-disabled with a one-shot stderr warning, and subsequent calls short-circuit. `git` subprocesses now also redirect and immediately close stdin so an interactive credential helper can't hold the parent process hostage.
- `peep --once` now respects Ctrl+C (exits 130 with a complete JSON envelope under `--json` / `--json-output`), handles `CommandStreamException`, and has a last-resort catch-all so any unexpected exception escaping `CommandExecutor.RunAsync` produces a typed envelope rather than an unhandled-exception stack trace. `OutOfMemoryException` and `StackOverflowException` deliberately remain uncaught.
- Interactive Ctrl+C handler no longer races against handler-unregister: `cts.Cancel()` is wrapped in `try/catch (ObjectDisposedException)` so a Ctrl+C arriving just before unregister can no longer crash the cancel-key thread.
- `process.StandardInput.Close()` no longer throws `IOException` when a fast-exiting child has already disconnected the pipe. Closing a pipe that's already gone is the success state.
- `Process.Start` exception coverage extended beyond `Win32Exception` to also catch `FileNotFoundException` (.NET 5+ may surface missing executables directly) → `command_not_found` (127) and `InvalidOperationException` / `PlatformNotSupportedException` / `ArgumentException` → `command_not_executable` (126), per the `--describe` contract.
- Alternate-screen-buffer enter / exit now swallows `IOException` and `ObjectDisposedException` from the underlying writer, so an I/O error at teardown can no longer leave the user's terminal stuck in alt-buffer mode with a hidden cursor for the rest of the shell session.
- File-watcher monitor task now has a strictly-weaker last-resort catch with a stderr warning (`file-change monitor crashed; file-change triggering disabled`). Pre-fix an unexpected crash silently disabled file-change triggering and peep kept polling on interval-only with no diagnostic.
- `--version` output no longer carries the `+gitsha` SourceLink suffix the .NET SDK appends by default. Users see plain `peep 0.3.0`, matching the suite-wide convention.
### Internal
- Library seam `SessionHelpers` extracted from `InteractiveSession` and `Program`: `WarnOnceForRegexTimeout`, `TryGetAutoExit`, `ResolveExitCode`, `RequestCancellationSilently`, and `ShouldDispatch` are now testable as named static methods rather than buried inside event-loop lambdas.
- `CommandExecutor.MaxOutputChars` changed from a process-global mutable static test seam (caused parallel-test flakiness and leaked test-overridden caps into user-facing truncation markers) to an optional per-call parameter.
- UTF-8 console adoption via `ConsoleEnv.UseUtf8Streams` so `--describe` em-dashes and any non-ASCII pipe output round-trip cleanly through Windows `cmd.exe`.
- Standard `<PackageTags>` set on the NuGet package so peep appears in nuget.org filtered searches (previously discoverable only by exact-name lookup).
- Platform-skipped tests migrated to `SkippableFact + Skip.IfNot` so non-applicable tests report Skipped rather than Passed on the wrong platform.
See full changelog at https://github.com/Yortw/winix/blob/main/src/peep/CHANGELOG.md