Crust logoCrust

Quick Start

Scaffold, run, and extend a typed Crust CLI.

Create a new project

Create a project

npx create-crust@latest my-cli
cd my-cli

Pick a package manager once; the tabs remember it across the docs. create-crust detects the package manager that launched it and installs dependencies with the same one. It also asks which runtime the project develops and builds for: Bun (the default), Node.js, or Deno; pass --runtime bun|node|deno to skip the prompt. Deno projects are the exception: they always install with deno install, whichever package manager launched the scaffolder. The scaffolder creates:

cli.ts
package.json
tsconfig.json
README.md
.gitignore

Run it

The generated dev script runs src/cli.ts with the runtime you picked (bun run, node, or deno run -A), whichever package manager installed the dependencies.

npm run dev -- Ada --greet Welcome
# Welcome, Ada!

The help() extension generates usage from the same definitions:

npm run dev -- --help
my-cli - A CLI built with Crust

Usage:
  my-cli [name] [options]

Arguments:
  [name]             Your name [default: "world"]

Options:
  -g, --greet                  Greeting to use [default: "Hello"]
  -v, --version                Show version number
  -h, --help                   Show help

Add a subcommand

Chain .command() onto the root:

src/cli.ts
import { Crust } from "@crustjs/core";
import { help, version } from "@crustjs/extensions";

import pkg from "../package.json" with { type: "json" };

const app = new Crust("my-cli", { description: "A CLI built with Crust", version: pkg.version })
	.extend(version(), help())
	.args({
		name: "name",
		type: "string",
		description: "Your name",
		default: "world",
	})
	.flags({
		name: "greet",
		type: "string",
		description: "Greeting to use",
		default: "Hello",
		short: "g",
	})
	.action(({ args, flags, stdout }) => {
		stdout(`${flags.greet}, ${args.name}!`);
	})
	.command("build", (command) =>
		command
			.flags({ name: "minify", type: "boolean", description: "Minify output" })
			.action(({ flags, stdout }) => stdout(`minify: ${flags.minify ?? false}`)),
	);

await app.execute();
npm run dev -- build --minify
# minify: true

npm run dev -- Ada
# Hello, Ada!

my-cli build routes to the subcommand; anything else still reaches the root action. Inline .command() suits app-local commands. Move to defineCommand and split files when a command needs its own file or reuse.

Build it

npm run build
npm run start Ada

crust build stages the publishable npm package(s) in .crust/: Bun and Deno projects get one standalone binary per platform behind a Node launcher at .crust/root/bin/my-cli.js; Node projects get one JavaScript bundle at that path. npm run release publishes them. See Build and distribution for cross-platform targets and the .crust/ layout.

Both create-crust and crust CLI are built with crust. If you would like to see crust in action. Check out the crust repo on GitHub

Add to an existing project

Install

npm install @crustjs/core @crustjs/extensions
npm install -D @crustjs/crust

@crustjs/crust is only needed for crust build. Skip it if you bundle another way or run the source directly. Deno projects install with deno add npm:; see Runtime support.

Configure package.json

package.json
{
  "private": true,
  "type": "module",
  "crust": { "runtime": "bun" },
  "bin": { "my-cli": "src/cli.ts" },
  "scripts": {
    "dev": "bun run src/cli.ts",
    "build": "crust build",
    "release": "crust publish",
    "start": "bun .crust/root/bin/my-cli.js"
  }
}

This is the Bun shape; dev runs the TypeScript source with Bun regardless of which package manager installed the dependencies. bin names the command and points at its source entry: crust build compiles it into .crust/, and npm link exposes the command locally by running the source, so start src/cli.ts with #!/usr/bin/env bun. private keeps a stray npm publish from uploading the source; crust publish ships the staged packages, which are not private. Node and Deno projects set "crust": { "runtime": "node" } or "deno" and use their own dev and start values; see Set up package.json.

Configure tsconfig.json

The scaffolder ships this config for Bun. Match types, module, and moduleResolution if you merge into an existing one; the rest is strictness. Node projects use "types": ["node"]; Deno projects use "lib": ["ESNext", "deno.window"] and "types": [] because deno check reads this file and a supplied lib replaces Deno's own globals.

tsconfig.json
{
  "compilerOptions": {
    "lib": ["ESNext"],
    "types": ["bun"],
    "target": "ESNext",
    "module": "Preserve",
    "moduleDetection": "force",
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "verbatimModuleSyntax": true,
    "noEmit": true,
    "strict": true,
    "skipLibCheck": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true
  },
  "include": ["src"]
}

Add the entrypoint

Create src/cli.ts with the file from Add a subcommand, then:

npm run dev -- --help

Next steps

The Guide walks through each layer of a Crust application, from arguments and flags to Extensions, Contexts, testing, and distribution. Pick the topic you need next.

On this page