| name | dax |
| description | This skill should be used when creating shell scripts, CLI tools, automation scripts, or any task that needs to run shell commands programmatically. Trigger when user asks to write a script, automate a task, create a CLI tool, process files in batch, or build any executable that runs shell commands. Also trigger when user mentions "dax", "deno script", or "write me a script". Deno with @david/dax is the default scripting stack on this machine - prefer it over bash scripts for anything beyond trivial one-liners. |
Dax - Deno Shell Scripting
Deno + @david/dax is the default scripting stack on this machine. Use it for all new scripts instead of bash/zsh scripts (unless trivially simple).
Script Template
#!/usr/bin/env -S deno run --allow-run --allow-env
import $ from "jsr:@david/dax";
Minimal shebang is --allow-run --allow-env (both always required by dax). Add --allow-read, --allow-write, --allow-net as needed.
Every script follows these three steps:
- First line is always the shebang:
#!/usr/bin/env -S deno run <permissions>
- Import from JSR:
import $ from "jsr:@david/dax";
- After writing, run
chmod +x <file> via Bash tool
Place scripts in ~/dotfiles/scripts/<category>/ for auto-symlinking to ~/bin. Use minimal required permissions in the shebang.
Deno Permissions
--allow-run and --allow-env are always required - dax reads env internally even for simple commands.
| Flag | When needed |
|---|
--allow-run | Always (dax executes commands) |
--allow-env | Always (dax reads env internally) |
--allow-read | Reading files/dirs |
--allow-write | Writing files |
--allow-net | HTTP requests via $.request() |
-A | All permissions (quick scripts only) |
Command Execution
await $`echo hello`;
const text = await $`echo hello`.text();
const lines = await $`ls`.lines();
const json = await $`cat data.json`.json();
const bytes = await $`cat img.png`.bytes();
const err = await $`command`.text("stderr");
const both = await $`command`.text("combined");
Exit Codes
const code = await $`command`.noThrow().code();
await $`command`.noThrow(1);
Argument Interpolation
const name = "my dir";
await $`mkdir ${name}`;
const files = ["a.txt", "b.txt"];
await $`rm ${files}`;
await $.raw`echo ${raw}`;
await $`echo ${$.rawArg(x)}`;
Piping
const out = await $`echo foo`.pipe($`grep foo`).text();
await $`echo foo | grep foo`;
Configuration
await $`cmd`.env("KEY", "val").cwd("./subdir");
await $`cmd`.quiet();
await $`cmd`.quiet("stdout");
await $`cmd`.timeout("30s");
await $`cmd`.printCommand();
Spawn & Abort
const child = $`sleep 100`.spawn();
child.kill();
const src = $`echo data`.stdout("piped").spawn();
await $`process`.stdin(src.stdout());
Redirects
await $`echo hello > output.txt`;
await $`echo hello`.stdout($.path("out.txt"));
await $`gzip < ${data}`.bytes();
await $`gzip < ${$.path("file.txt")}`.bytes();
Path API
Immutable Path via $.path() (from @david/path):
const dir = $.path("output");
await dir.mkdir();
const file = dir.join("data.txt");
await file.writeText("content");
const text = await file.readText();
file.basename();
file.extname();
file.resolve().toString();
file.isFileSync();
dir.isDirSync();
await $`cat ${file}`;
Interactive Prompts
const name = await $.prompt("Your name?");
const name = await $.prompt("Name?", { default: "Dax", mask: true });
const yes = await $.confirm("Continue?");
const idx = await $.select({ message: "Pick:", options: ["A", "B", "C"] });
const idxs = await $.multiSelect({ message: "Pick many:", options: ["A", "B", "C"] });
$.maybePrompt(), $.maybeConfirm(), $.maybeSelect() return undefined on cancel.
Logging
All log to stderr (keeps stdout clean for piping):
$.log("info");
$.logStep("Done");
$.logError("Failed");
$.logWarn("Warning");
$.logLight("Detail");
await $.logGroup(async () => {
$.log("indented");
});
Progress
await $.progress("Working...").with(async () => { });
const pb = $.progress("Processing", { length: items.length });
await pb.with(async () => {
for (const item of items) {
await doWork(item);
pb.increment();
}
});
HTTP Requests
const data = await $.request("https://api.example.com").json();
const html = await $.request("https://example.com").text();
await $.request("https://example.com/file.zip")
.showProgress()
.pipeToPath($.path("file.zip"));
await $.request("https://api.example.com")
.header("Authorization", "Bearer token")
.timeout("10s")
.json();
Helpers
await $.sleep("2s");
await $.which("deno");
await $.commandExists("ffmpeg");
await $.withRetries({
count: 3, delay: "1s",
action: async () => { },
});
const s = $.dedent` // strip leading indent
multi
line`;
$.stripAnsi(text);
Built-in Shell Commands
Cross-platform, no external deps: cd, echo, exit, cp, mv, rm, mkdir (-p), pwd, sleep, test, touch, cat, which, true, false, printenv, unset
Custom $ Instance
const $ = build$({
commandBuilder: (b) => b.cwd("./subDir").env("VAR", "value"),
requestBuilder: (b) => b.header("Auth", "token"),
extras: { myHelper: (a: string) => a.toUpperCase() },
});
Workflow
- Determine script purpose and required Deno permissions
- Write script with shebang as first line, JSR import
- Run
chmod +x <script> via Bash tool
- Place in
~/dotfiles/scripts/<category>/ if it should be in PATH
Notes
- Always import from JSR (
jsr:@david/dax), never npm
- Prefer
$.path() over string path manipulation
- Use
$.log* over console.log for CLI output (stderr, clean piping)
- Use
.noThrow() when non-zero exit codes are expected
- Use
.quiet() when capturing results without terminal output
- Shell variables work:
await $\VAR=1 && echo $VAR``
- For
pipefail, nullglob, globstar use set/shopt in commands