Crust CLI Framework
repository·main·Indexed 19 days ago
https://github.com/chenxin-yan/crustA TypeScript-first, Bun-native CLI framework for building modular and composable command-line tools. It provides high-level abstractions for command routing, argument parsing, interactive prompts, and typed persistence. The ecosystem includes @crustjs/core for the command engine, @crustjs/store for typed persistence, @crustjs/prompts for interactive terminals, and tooling for distributing CLIs as standalone binaries or Bun runtime packages.
What's inside Crust
- Crust is a TypeScript-first, Bun-native framework designed for building command-line applications. It uses a chainable builder pattern to declare commands, providing end-to-end type safety through automatic type inference. The framework is modular, allowing you to opt into specific features (like help, versioning, or completions) via composable modules to keep your application lightweight.
Overview of Crust packages
mainCrust is a modular ecosystem of packages designed for building TypeScript and Bun-native CLI tools. Key packages include:
@crustjs/crust: CLI tooling for building and distributing standalone executables.@crustjs/core: The engine for command definition, argument parsing, routing, plugins, and error handling.@crustjs/extensions: Official extensions providinghelp,version, andcompletionfunctionality.@crustjs/style: The foundation for terminal styling.@crustjs/progress: Progress indicators for managing async CLI tasks.@crustjs/prompts: Tools for creating interactive terminal prompts.@crustjs/store: Typed persistence with separation forconfig,data,state, andcache.@crustjs/skills: Generates agent skills from Crust command definitions.@crustjs/create: A headless scaffolding engine for building your owncreate-xxxtools.create-crust: The primary project scaffolding tool.
The RunnableApp interface
mainThe testing helpers work with any object that implements the
RunnableAppstructural contract. An application does not need to extendCrustto be compatible, as long as it provides arunmethod with the following signature:interface RunnableApp { run( argv: readonly string[], io?: { stdout?: (text: string) => void; stderr?: (text: string) => void; }, ): Promise<void>; }Note: An inert
CommandDefinitiondoes not implementrun()and must be mounted into a runnable application before it can be used with these helpers.How multi-target builds and resolvers work
mainWhen running
crust buildwithout a specific--target, the tool generates a distribution package in thedist/directory containing:- Platform-specific binaries: Named
dist/<name>-<target>(e.g.,dist/my-cli-bun-darwin-arm64). - A Shell Resolver: A file named
dist/cli(customizable via--resolver) which is a#!/usr/bin/env bashscript. It detects the host platform viaunameand usesexecto run the correct binary with no child process overhead. - A Windows Resolver: A
dist/cli.cmdbatch file for Windows support.
This resolver is what you should point to in your
package.json'sbinfield to allow users to run your CLI regardless of their OS.# Default multi-target build output structure dist/ ├── cli # Shell resolver (entry point) ├── cli.cmd # Windows batch resolver ├── my-cli-bun-linux-x64-baseline # Linux x64 binary ├── my-cli-bun-linux-arm64 # Linux ARM64 binary ├── my-cli-bun-darwin-x64 # macOS Intel binary ├── my-cli-bun-darwin-arm64 # macOS Apple Silicon binary ├── my-cli-bun-windows-x64-baseline.exe # Windows x64 binary └── my-cli-bun-windows-arm64.exe # Windows ARM64 binary- Platform-specific binaries: Named
How the no-color flag affects environment variables
mainThe
noColorextension implements standard boolean negation for thecolorflag. It modifies the environment for the duration of the command execution and restores previous values once finished.Flag Behavior
--color: SetsFORCE_COLOR=3and clearsNO_COLOR. This forces ANSI truecolor output, overriding non-TTY detection. Any library sensitive toFORCE_COLORwill obey this.--no-color: SetsNO_COLOR=1and clearsFORCE_COLOR. This suppresses colors following the no-color.org specification, while still allowing non-color modifiers and hyperlinks to follow standard TTY detection.
CLI Usage Examples
my-cli --color my-cli --no-color my-cli deploy --no-colorImportant Notes
- No Default: If the flag is omitted, the extension does nothing. Color detection remains the responsibility of
@crustjs/style(using standardNO_COLOR,FORCE_COLOR, and TTY detection). - Concurrency: In overlapping programmatic runs, the last-writer-wins policy applies while active. The ambient environment is restored once the last run finishes.
Core Concepts of Crust
mainTo use Crust effectively, understand these core architectural principles:
- Inference over annotation: Types flow automatically from your command definitions (args and flags) to your command handlers. You do not need to use decorators, code generation, or manual generics.
- Immutable Builders: Definitions and Extensions are treated as frozen data. Every method call on a builder returns a new builder instance.
- One-way Pattern: The recommended pattern is to use
defineCommand()for every command and instantiatenew Crust()exactly once at the root of your application. - Composable Modules: The core (
@crustjs/core) is zero-dependency. Features like help, versioning, and completions are provided via Extensions attached at the root, and build tooling is provided by@crustjs/crust. - Subcommands and Inheritance: You can build nested command trees. Subcommands can inherit flags from parent commands by setting
inherit: true. - Contexts: These are named command dependencies that are constructed lazily only for the specific command path being resolved.
Avoid multiple .flags() calls on a builder
mainWhen using a builder, calling
.flags()multiple times will replace the previous local flag definitions rather than merging them. This behavior differs from the Commander or yargs convention where flags are additive. To ensure all flags are correctly defined and to maintain the best TypeScript type inference, use a single.flags()call containing all flag definitions.// ❌ INCORRECT: This will result in only the second set of flags being preserved. builder.flags(flagsA).flags(flagsB); // ✅ CORRECT: Use a single call to include all definitions. builder.flags({ ...flagsA, ...flagsB });How to author a custom Extension
mainIf the official extensions do not meet your needs, you can author your own custom Extension using thedefineExtension(name, config)function provided by@crustjs/core.Configure flag aliases and negation
mainYou can define short forms and aliases for flags to provide multiple ways to invoke them.
short: A single-character form (e.g.,short: "o").aliases: An array of additional long or short forms (e.g.,aliases: ["out"]).- Boolean Negation: Boolean flags automatically support
--no-<spelling>for the canonical name and all long aliases (e.g., if the flag isoutput,--no-outworks). - Disabling Negation: Set
noNegate: trueto reject any--no-prefix with aPARSEerror.
Note: Boolean flags do not accept the
--name=truesyntax..flags({ name: "output", type: "path", short: "o", aliases: ["out"] })Configure build-time constants with PUBLIC_*
mainThe
crust buildcommand uses a model similar to Bun'sPUBLIC_*prefix. Only environment variables starting withPUBLIC_are eligible to be embedded into the binary as build-time constants.Warning: Embedded
PUBLIC_*values are visible in the binary. Never use thePUBLIC_prefix for secrets like API keys or private tokens.| Value | Location | | ------------------------------------------- | ------------------------------- | | API keys, tokens, private deployment config | Runtime environment | | Public API origins, public build labels | PUBLIC_* build-time constants |Understand the Crust invocation lifecycle
mainWhen an application is invoked, Crust follows a strict sequence of operations to ensure proper setup, validation, and cleanup:
- Prepare definitions: Apply Extensions and check for command/flag/alias collisions.
- Route: Resolve the command path.
- Parse syntax: Consume positional and flag tokens.
- Run
preRunhooks: Invoke Extension hooks in.extend()order. Returningctx.finish()from a hook skips remaining hooks, validation, and the Command Handler. - Validate structure: Enforce required values and choices.
- Apply schemas: Validate and transform schema-backed inputs.
- Construct Contexts: Create Context values in topological order based on their requirements.
- Run the Command Handler: Execute the core logic.
- Dispose Contexts: Run
Symbol.disposeorSymbol.asyncDisposein reverse construction order (even if a failure occurs). - Run
postRunhooks: Invoke Extension hooks in reverse.extend()order. This acts as thefinallyblock for the invocation.
Structure of a generated Crust project
mainEvery project scaffolded by
create-crustincludes the following files:src/cli.ts: The entry point containing a sample command.package.json: Configured specifically for your chosen distribution mode (binary or runtime).tsconfig.json: A strict TypeScript configuration.README.md: Initial getting started instructions..gitignore: Default ignores for Node/Bun projects.