Releases: dsherret/dax
0.48.2
0.48.1
0.48.0
What's Changed
Abortable Prompts
Every prompt ($.alert, $.prompt, $.confirm, $.select, $.multiSelect, and their maybe* variants) now accepts a signal: AbortSignal that dismisses the prompt when aborted.
The maybe* variants treat abort like ctrl+c and resolve to undefined — handy when you want to cancel a prompt because a concurrent task finished:
const ac = new AbortController();
doSomeWork().then(() => ac.abort());
await $.maybePrompt({
message: "Press [enter] to stop early.",
signal: ac.signal,
});Callers that need to distinguish an abort from a user ctrl+c can check signal.aborted after the call. The non-maybe* variants instead reject the returned promise with signal.reason:
try {
const name = await $.prompt("What's your name?", {
signal: AbortSignal.timeout(5_000),
});
} catch (err) {
if (err.name !== "AbortError") throw err;
// prompt timed out
}Full Changelog: 0.47.0...0.48.0
0.47.0
What's Changed
- feat(BREAKING): update
jsr:@david/whichto 0.7 to support Windows app execution aliases by @dsherret in #392
Before:
> await $`winget --version`
dax: winget: command not foundAfter:
> await $`winget --version`
v1.28.240The fact that I had to do a bug fix to specifically support these means Windows messed up.
Full Changelog: 0.46.1...0.47.0
0.46.1
What's Changed
- feat: add
$.allfor awaiting multiple command builders with.tailDisplay()by @dsherret in #386 - fix: do not include
@deno/shim-deno-testin npm dependencies by @dsherret in #381 - fix(request): cancel upstream body when reading is interrupted by @dsherret in #387
$.all — Promise.all with auto-sized tail display
$.all(values) behaves like Promise.all, but enables .tailDisplay() on any command builders in the list with maxLines sized to share the screen:
await $.all([
$`deno task build frontend`,
$`deno task build backend`,
$`deno task build worker`,
]);The per-item maxLines aims for ~90% of the terminal height divided evenly across the items, with a minimum of 3 lines per item — so when there are many items, the combined region may extend past the screen height. The size is recomputed per draw, so it adjusts when the terminal is resized mid-run.
To opt a builder out of the auto-applied tail display, wrap it in a plain Promise (e.g. (async () => await $\...`)()`).
For structured output (.text(), .json(), etc.) pass a transform as the second argument — it's invoked on each builder after tailDisplay is applied, so the auto-sizing is preserved:
const [frontend, backend] = await $.all(
[$`deno task build frontend`, $`deno task build backend`],
(b) => b.text(),
);Full Changelog: 0.46.0...0.46.1
0.46.0
This is a large release and there's now a website: https://dax.land
- feat(BREAKING): use Node APIs internally and for errors (#364)
- feat(BREAKING): extract out the shell to jsr:@david/shell (#365, #367)
- fix: improve stack traces on error (dsherret/shell#1)
- feat: add
linesIter()(dsherret/shell#3) - feat: resolve path-like commands with PATHEXT on Windows (dsherret/shell#4)
- feat(BREAKING): support multiline commands and set +e (dsherret/shell#5)
- feat: support ctrl+n and ctrl+p in selections (#366)
- feat: add an
$.alert()function (#368) - fix(request): consume body before throw (#369)
- feat: add new
.tailDisplay()function and$.setTailDisplay(...)global function (#370, dsherret/shell#7, dsherret/shell#8) - feat(request): add
.onProgresscallback (#371, #372) - feat: add
.beforeRequest,.beforeCommand, and.beforeCommandSyncmethods (#373, dsherret/shell#10) - feat(BREAKING): return an object from select functions (#374)
- Note: still coerces to a number.
- feat:
.errorTail()and$.setErrorTail(true)(#376, dsherret/shell#11) - fix: support interactive prompts when stdin is piped via /dev/tty / CONIN$ fallback (#377)
BREAKING: Runtime-agnostic internals — now backed by Node APIs (#364)
Dax now uses Node.js APIs internally for filesystem and process operations, including for the errors it throws. The path APIs no longer surface Deno.errors.* classes — instead, errors come through as Node-style errors and you should branch on the code property (e.g. ENOENT, EEXIST). The upside: Dax behaves the same across Deno and Node so you no longer need runtime-specific error handling.
Shell extracted to jsr:@david/shell (#365, #367)
The shell parser/executor that powers $`...` lives in its own package now: jsr:@david/shell. It can be used standalone if you want the cross-platform shell without the rest of Dax.
BREAKING: Multiline commands and set +e (#367, dsherret/shell#5)
The shell now properly handles commands that span lines instead of treating newlines as plain whitespace. In multi-line input, errexit (set -e) is on by default — the first failing line stops the rest. Single-line input continues to run on failure as before. Use set +e to opt back out within a script.
await $`
echo one
echo two
echo three
`;
// keep running even if an earlier line fails
await $`
set +e
(exit 3)
echo still-ran
`;This is breaking for anyone who was relying on the old whitespace behavior across newlines.
PATHEXT resolution on Windows (#367, dsherret/shell#4)
Path-like commands are now resolved against PATHEXT on Windows, so ./script will find ./script.cmd, ./script.bat, ./script.ps1, etc. — no need to spell out the extension.
linesIter() for streaming output (#367, dsherret/shell#3)
A new helper for streaming a command's stdout line-by-line without buffering the whole output into memory:
for await (const line of $`cat big.txt`.linesIter()) {
console.log(line);
}
// also works for stderr
for await (const line of $`some-command`.linesIter("stderr")) {
console.log(line);
}Breaking out of the loop early kills the child process.
Better stack traces on shell errors (#367, dsherret/shell#1)
Errors thrown from the shell now carry more useful stack traces, making it easier to track down which call site triggered a failure.
$.alert() (#368)
A new $.alert() function for displaying a message and waiting for the user to acknowledge — pairs with the existing $.confirm() and $.prompt(). By default any key press dismisses the alert:
await $.alert("Backup complete!");
// or require the user to press Enter
await $.alert("Backup complete!", {
requireEnter: true,
});
// or provide an object
await $.alert({
message: "Backup complete!",
noClear: true, // don't clear the text on dismissal
});Live tail display — .tailDisplay() and $.setTailDisplay() (#370, dsherret/shell#7, dsherret/shell#8)
Long-running commands can now show a live tail of their output that's pinned to a fixed-height region at the bottom of the terminal — only the most recent lines stay live, and on success they're cleared from that region (the full output remains in scrollback above for failed commands). Concurrent tailing commands compose into a single shared scrolling region.
// keep the last 5 lines of `./build.sh` pinned while it runs
await $`./build.sh`.tailDisplay();
// configure visible row count and header
await $`./build.sh`.tailDisplay({ maxLines: 2, header: false });
// percentage sizing — re-fits if the terminal is resized mid-run
await $`./build.sh`.tailDisplay({ maxLines: "50%" });
// custom header rendered verbatim
await $`./build.sh`.tailDisplay({
maxLines: 10,
header: ({ command }) => `building ${command}…`,
});
// concurrent tails compose
await Promise.all([
$`./build.sh frontend`.tailDisplay({ maxLines: 4 }),
$`./build.sh backend`.tailDisplay({ maxLines: 4 }),
]);Toggle on globally with $.setTailDisplay(true) (or pass options).
Error tails — .errorTail() and $.setErrorTail() (#376, dsherret/shell#11)
Complementary to .tailDisplay(): when a command fails, automatically include a tail of its captured output in the error message so you can see what actually went wrong without re-running with .stderr("inherit"). It targets streams the user can't see — piped, redirected to a file, sent to a WritableStream, or discarded — and skips streams routed to the terminal since those bytes already reached the scrollback. The most common case is .quiet().
// surfaces the trailing stdout/stderr bytes in the error if the command fails
await $`./build.sh`.errorTail().quiet();
// raise the per-stream cap (default: 8 KiB)
await $`./build.sh`.errorTail({ maxBytes: 16 * 1024 }).quiet();
// only capture stderr
await $`./build.sh > out.log`.errorTail({ stdout: false }).quiet();
// merge stdout and stderr into one interleaved buffer so the error
// message preserves the order the bytes were written
await $`./build.sh > out.log 2> err.log`.errorTail({ combined: true }).quiet();Toggle on globally with $.setErrorTail(true) (or pass options).
Lifecycle hooks — .beforeRequest, .beforeCommand, .beforeCommandSync (#373, dsherret/shell#10)
You can now register callbacks that fire before each request or shell command, giving you a hook point for logging, mutation, auth injection, or instrumentation without wrapping every call site. The callback receives the current builder and may return a (possibly modified) builder.
// inject an auth header fetched asynchronously
$.request(`${baseUrl}${path}`)
.header("Content-Type", "application/json")
.beforeRequest(async (builder) => {
return builder.header("Authorization", `Bearer ${await getAccessToken()}`);
});
// same idea for commands — useful for env vars that depend on async work
await $`./build.sh`
.beforeCommand(async (builder) => {
return builder.env("AUTH_TOKEN", await getAccessToken());
});Multiple hooks compose in registration order. Async hooks only run on the await / .then() path — for the streaming .spawn() case, use the synchronous variant:
const child = $`./build.sh`
.beforeCommandSync((builder) => builder.env("BUILD_ID", crypto.randomUUID()))
.spawn();Request progress callbacks — .onProgress() (#371, #372)
RequestBuilder now supports an .onProgress(callback) method for tracking download progress when you'd rather render your own UI (or report progress somewhere other than the terminal). The callback fires once per chunk read from the response body with the cumulative bytes received and the total expected size:
await $.request(url)
.onProgress(({ loaded, total }) => {
if (total != null) {
console.log(`${(loaded / total * 100).toFixed(1)}%`);
} else {
console.log(`${loaded} bytes`);
}
})
.pipeToPath();total is taken from the content-length response header and will be undefined if the server doesn't provide one. Multiple callbacks may be registered by calling .onProgress repeatedly — each is invoked in the order it was added. .onProgress is independent of .showProgress, so the two can be combined or used on their own.
BREAKING: select() returns an object (#374)
Selection prompts now return an object that exposes index and value (and coerces to a number), rather than a bare number — this lets you access the selected item directly without re-indexing your input array. The numeric coercion keeps existing code working in most cases.
const colours = ["Red", "Green", "Blue"];
const result = await $.select({
message: "What's your favourite colour?",
options: colours,
});
console.log(result.index); // e.g. 0
console.log(result.value); // e.g. "Red"
console.log(colours[result]); // also works — coerces to the indexSelection navigation: ctrl+n / ctrl+p (#366)
Emacs-style next/previous bindings work in selection prompts, in addition to arrow keys.
Interactive prompts work when stdin is piped (#377)
$.prompt(), $.confirm(), and friends now fall back to /dev/tty (Unix) or CONIN$ (Windows) when stdin is piped, so prompts continue to work in pipelines like echo data | my-script.ts.
0.45.0
Breaking changes
Turn off failglob by default (#355)
failglob is now off by default, matching standard bash behavior where unmatched glob patterns are passed through literally rather than throwing an error.
Previously:
// Would throw if no .txt files exist
await $`echo *.txt`;Now:
// Outputs "*.txt" literally if no .txt files exist
await $`echo *.txt`;To restore the previous behavior, enable failglob using the new shell options:
await $`shopt -s failglob && echo *.txt`;
// or via the builder
await $`echo *.txt`.failglob();This was turned off because it was found to be problematic (ex. curl https://example.com?example would fail due to having an unmatched glob).
Match ? as a literal character by default (#356)
The ? character is no longer treated as a glob character by default. This change follows the same rationale as fish shell—? is commonly used in URLs and other CLI arguments, making glob behavior surprising and often problematic.
Previously:
// Would glob, matching files like "a.txt", "b.txt"
await $`echo ?.txt`;Now:
// Outputs "?.txt" literally
await $`echo ?.txt`;To restore the previous behavior, use the new .questionGlob() builder method:
await $`echo ?.txt`.questionGlob();Note: Unlike failglob, questionGlob is only available via the builder API and not through shopt/set commands.
Features
Shell options and shopt/set commands (#353)
Added support for shell options that can be configured either via the shopt/set built-in commands or through builder methods.
Available shell options:
| Option | Command | Builder Method | Default | Description |
|---|---|---|---|---|
failglob |
shopt |
.failglob() |
off | Error when glob patterns match no files |
globstar |
shopt |
.globstar() |
off | Enable ** to match directories recursively |
nullglob |
shopt |
.nullglob() |
off | Glob patterns matching no files expand to empty |
pipefail |
set |
.pipefail() |
off | Pipeline returns the exit code of the last failing command |
Using shell commands:
// Enable failglob
await $`shopt -s failglob && echo *.txt`;
// Disable failglob
await $`shopt -u failglob && echo *.txt`;The set command is available for pipefail:
await $`set -o pipefail && cat missing.txt | echo 1`;Using builder methods:
await $`echo *.txt`.failglob();
await $`echo *.txt`.failglob(false);
await $`echo *.txt`.nullglob();
await $`cat missing.txt | echo 1`.pipefail();Accept AbortSignal for CommandBuilder (#357)
signal() on CommandBuilder now accepts standard web AbortSignal in addition to KillSignal. This makes it easier to integrate with existing cancellation patterns.
const controller = new AbortController();
const promise = $`sleep 1000s`.signal(controller.signal);
// Cancel after 1 second
setTimeout(() => controller.abort(), 1000);
await promise; // throws after 1 secondThe KillSignal API continues to work as before and is more flexible.
Accept AbortSignal for RequestBuilder (#358)
Added .signal() method to RequestBuilder for cancelling requests.
const controller = new AbortController();
const request = $.request("https://example.com/large-file.zip")
.signal(controller.signal);
const promise = request.pipeToPath();
// cancel the request
controller.abort();
await promise; // throws AbortErrorAdd built-in true and false commands (#359)
Added cross-platform built-in true and false commands so it works on Windows and executes faster.
// true always exits with code 0
await $`true && echo "success"`;
// false always exits with code 1
await $`false || echo "failed"`;Full Changelog: 0.44.2...0.45.0
Install Instructions: https://github.com/dsherret/dax?tab=readme-ov-file#install
0.44.2
0.44.1
0.44.0
What's Changed
- feat: implement negation by @jeff-hykin in #341
- feat(BREAKING): rename
KillSignalControllertoKillControllerby @dsherret in #339 - fix: remove previously removed
$.cdfrom runtime code by @dsherret in #343 - feat:
.code()helper by @dsherret in #344 - feat: glob expansion by @dsherret in #338
- feat: add combined output redirect support by @jeff-hykin in #340
New Contributors
- @jeff-hykin made their first contribution in #341
Full Changelog: 0.43.2...0.44.0