Build CLI apps

Ende is a great way to ship command-line tools. Your tool is just TypeScript, so there's no build step to run it. When you're ready to distribute it, ende compile turns it into a single self-contained executable that runs without Ende installed.

Read command-line arguments

Raw arguments are available on Ende.args. For real tools, parse them with @ende/cli, which handles flags, options, and defaults:

greet.ts
import { parseArgs } from "jsr:@ende/cli/parse-args";

const flags = parseArgs(Ende.args, {
  string: ["name"],
  default: { name: "world" }
});

console.log(`Hello, ${flags.name}!`);
$ ende run greet.ts --name=Ende
Hello, Ende!

Read from stdin and detect a TTY

Good CLI tools compose with pipes. Ende.stdin exposes standard input as a web-standard ReadableStream, so the easiest way to read all piped input as text is to wrap it in a Response:

const input = await new Response(Ende.stdin.readable).text();

When your tool can be used both ways, use Ende.stdin.isTerminal() to tell whether stdin is an interactive terminal or a pipe, and branch accordingly. This is also how tools decide whether to print colors and progress bars (interactive) or plain machine-readable output (piped). Here the tool falls back to asking the user when nothing is piped in:

count.ts
let input: string;

if (Ende.stdin.isTerminal()) {
  input = prompt("Enter some text:") ?? "";
} else {
  input = await new Response(Ende.stdin.readable).text();
}

console.log(`Got ${input.trim().length} characters`);
$ echo "hello from a pipe" | ende run count.ts
Got 17 characters
$ ende run count.ts
Enter some text: hi
Got 2 characters

Reading stdin requires no permission flags.

Prompt the user

For simple interactive input, Ende ships the browser globals prompt(), confirm(), and alert(). They block until the user responds (execution pauses at that line and nothing else runs until input arrives), and they need no imports or permissions:

setup.ts
const name = prompt("Project name:", "my-app");
const ok = confirm(`Create ${name}?`);
if (!ok) Ende.exit(1);
console.log(`Creating ${name}...`);

For anything fancier, reach for @ende/cli. Besides argument parsing, it includes spinners, progress bars, and select prompts. These live in modules prefixed with unstable-, so their APIs may still change. The Spinner class shows an animated status line while your tool works:

spin.ts
import { Spinner } from "jsr:@ende/cli/unstable-spinner";

const spinner = new Spinner({ message: "Downloading..." });
spinner.start();
await new Promise((resolve) => setTimeout(resolve, 2000));
spinner.stop();
console.log("Done");
ende run spin.ts

Exit with the right code

Shells, scripts, and CI systems rely on your tool's exit code: zero means success, anything else means failure. Ende.exit() terminates the process immediately with the given code:

if (Ende.args.length === 0) {
  console.error("Usage: check <file>...");
  Ende.exit(2);
}

Because Ende.exit() stops the process on the spot, pending work like flushing logs or finally blocks never runs. When you want to record a failure but keep going, set Ende.exitCode instead. The process continues, finishes naturally, and then exits with the code you set. This is the right pattern for linters and batch tools that should report every problem rather than stop at the first one:

check.ts
for (const file of Ende.args) {
  try {
    await Ende.lstat(file);
  } catch {
    console.error(`Missing: ${file}`);
    Ende.exitCode = 1;
  }
}
console.log("Check finished");
$ ende run --allow-read check.ts real.txt nope.txt
Missing: nope.txt
Check finished
$ echo $?
1

There is no fixed meaning for nonzero codes beyond "failure", but a common convention is 1 for the errors your tool checks for and 2 for usage mistakes like missing arguments.

Compile to a single executable

ende compile bundles your program and the Ende runtime into one binary: no install step for your users.

ende compile greet.ts
./greet --name=Ende

Name the binary with --output. If your tool needs permissions, bake them in with the usual --allow-* flags so it runs without prompting:

ende compile --output greet greet.ts

If binary size matters more than raw execution speed, you can build against the experimental QuickJS engine instead of V8 with --engine quickjs. See choosing a JavaScript engine for the trade-offs.

Embed assets in the binary

A single-file executable should stay single-file even when your tool needs data such as templates, word lists, or default configs. Pass --include to bundle files or directories into the binary:

ende compile --include names.csv --output greet greet.ts

At runtime, read the embedded files relative to your module's directory with import.meta.dirname:

const names = Ende.readTextFileSync(import.meta.dirname + "/names.csv");

The same code works during development with ende run and inside the compiled binary, regardless of where the user runs it from. See including data files for directory includes and for configuring the paths once in ende.json.

Cross-compile for other platforms

Build for a platform other than your own with --target, so you can ship binaries for Windows, macOS, and Linux from one machine:

ende compile --target x86_64-pc-windows-msvc --output greet.exe greet.ts
ende compile --target aarch64-apple-darwin --output greet greet.ts

The full list of targets covers x86_64 and ARM builds of Linux, macOS, and Windows; see supported targets. If you distribute binaries to end users, you can also sign them so operating systems trust your tool; see code signing for the macOS and Windows steps.

Install a tool from source

To make a script available as a command on your own machine (without compiling), install it globally with ende install:

ende install --global --name greet greet.ts
greet --name=Ende

Going further

  • ende compile. All targets, flags, asset embedding, icons, and code signing.
  • ende install. Install tools and dependencies.
  • @ende/cli. Argument parsing, spinners, progress bars, and interactive prompts.
  • Ende API reference. Everything on the Ende namespace, including stdin, exit codes, and file APIs.
  • Permissions. Choose exactly what your compiled tool may access.