CLI Block
The cli {} block is a top-level language construct in Tova that turns function signatures into complete command-line interfaces. You declare commands as functions with typed parameters, and the compiler generates a zero-dependency CLI executable with argument parsing, validation, help text, subcommands, and rich error messages -- all from your function signature alone.
Why a Dedicated CLI Block?
Without first-class CLI support, building command-line tools means choosing between:
- Manual
if/elifdispatch onargs()-- brittle, no help text, no validation, no type coercion - External frameworks (Click, Cobra, Commander.js) -- dependency overhead, framework-specific APIs, boilerplate decorators or builder chains
The cli {} block eliminates both:
- The function signature IS the CLI interface -- parameter names become argument names, types provide validation,
--prefixes mark flags - Zero dependencies -- the compiler generates all parsing, validation, and help text directly. No packages to install
- Compile-time checks -- the analyzer warns on duplicate commands, positional arguments after flags, and missing config
- Auto-generated help --
--helpshows usage, arguments, options, and defaults for every command - Type coercion --
Intparameters auto-parse from strings with error messages on invalid input - Single-command optimization -- if you define only one command, the subcommand layer is skipped entirely
Syntax Overview
cli {
name: "deploy"
version: "1.0.0"
description: "Deploy your app"
fn deploy(target: String, --env: String = "staging", --port: Int = 3000, --verbose: Bool) {
print(bold("Deploying ") + green(target) + " to " + env)
if verbose {
print(dim("Port: {port}"))
}
}
fn init(name: String?) {
project = name ?? "my-app"
print("Initializing {project}")
}
}This compiles to a standalone executable with:
$ deploy --help
deploy -- Deploy your app
Version: 1.0.0
USAGE:
deploy <command> [options]
COMMANDS:
deploy
init
OPTIONS:
--help, -h Show help
--version, -v Show version
$ deploy deploy production --port 8080 --verbose
Deploying production to staging
Port: 8080
$ deploy init
Initializing my-app
$ deploy deploy
Error: Missing required argument <target>
USAGE:
deploy deploy <target> [--env <String>] [--port <Int>] [--verbose]Config Fields
The cli {} block supports three config fields at the top level:
| Field | Type | Description |
|---|---|---|
name | String | The CLI tool name (used in help text and usage lines) |
version | String | Version string (enables --version / -v flag) |
description | String | One-line description shown in help text |
cli {
name: "mytool"
version: "2.1.0"
description: "A fantastic CLI tool"
}All fields are optional. If name is omitted, the analyzer warns with W_CLI_MISSING_NAME.
Commands
Each fn declaration inside a cli {} block becomes a subcommand. The function name is the command name, parameters become arguments and flags, and the function body is executed when the command is invoked.
cli {
name: "git-like"
fn clone(url: String, --depth: Int) {
print("Cloning {url}")
}
fn push(--force: Bool, --remote: String = "origin") {
print("Pushing to {remote}")
}
}Async Commands
Commands can be async for operations that need await:
cli {
name: "fetcher"
async fn download(url: String, --output: String = "out.txt") {
data = await fetch(url)
fs.write_text(output, data)
print(green("Saved to {output}"))
}
}Single-Command Mode
If you define only one command, the compiler skips subcommand routing entirely. The user invokes the tool directly without specifying a subcommand name:
cli {
name: "greet"
version: "1.0.0"
fn greet(name: String, --loud: Bool) {
greeting = "Hello, {name}!"
if loud {
print(upper(greeting))
} else {
print(greeting)
}
}
}$ greet Alice --loud
HELLO, ALICE!
# NOT "greet greet Alice" -- single-command modeParameters
Positional Arguments
Parameters without -- are positional arguments, matched by position on the command line:
fn copy(source: String, dest: String) {
// source = first arg, dest = second arg
}$ tool copy /tmp/a.txt /tmp/b.txtFlags
Parameters prefixed with -- are named flags:
fn serve(--host: String = "localhost", --port: Int = 3000) {
print("Serving on {host}:{port}")
}$ tool serve --host 0.0.0.0 --port 8080Flags also support --flag=value syntax:
$ tool serve --port=8080Type Annotations
Parameter types control how argv strings are parsed:
| Type | Parsing | Error on invalid |
|---|---|---|
String | No conversion (default) | -- |
Int | parseInt() with NaN check | "Error: --port must be an integer, got "abc"" |
Float | parseFloat() with NaN check | "Error: --rate must be a number, got "xyz"" |
Bool | Toggle flag (no value needed) | -- |
Bool Flags
Bool flags are toggles -- they don't take a value. They default to false and become true when present:
fn build(--verbose: Bool, --minify: Bool) {
if verbose { print("Verbose mode on") }
if minify { print("Minifying output") }
}$ tool build --verbose --minifyBool flags also support --no- prefix to explicitly set false:
$ tool build --no-minifyOptional Parameters
Add ? after the type to make a positional argument optional:
fn init(name: String?) {
project = name ?? "my-app"
print("Initializing {project}")
}$ tool init # name is undefined, falls back to "my-app"
$ tool init my-proj # name is "my-proj"Default Values
Both positional and flag parameters support default values:
fn deploy(target: String, --env: String = "staging", --replicas: Int = 1) {
print("Deploying {target} to {env} with {replicas} replicas")
}Defaults are shown in help text and used when the argument is not provided.
Repeated Flags
Use [Type] to collect multiple values for a flag:
fn build(--include: [String]) {
for path in include {
print("Including {path}")
}
}$ tool build --include src --include lib --include vendorEach occurrence of --include appends to the array.
Help and Version
The compiler auto-generates help handlers:
--help/-hshows overall help (command list + global options)<command> --helpshows per-command help (arguments, flags, defaults)--version/-vshows the version (only whenversion:is configured)
Overall Help
mytool -- A fantastic CLI tool
Version: 2.1.0
USAGE:
mytool <command> [options]
COMMANDS:
deploy
init
OPTIONS:
--help, -h Show help
--version, -v Show versionPer-Command Help
USAGE:
mytool deploy <target> [--env <String>] [--port <Int>] [--verbose]
ARGUMENTS:
target <String>
OPTIONS:
--env <String> (default: "staging")
--port <Int> (default: 3000)
--verbose
--help, -h Show helpError Handling
The generated CLI produces clear error messages for common mistakes:
Missing required argument:
Error: Missing required argument <target>
USAGE:
deploy deploy <target> [--env <String>]Invalid type:
Error: --port must be an integer, got "abc"Unknown flag:
Error: Unknown flag --foobarAll errors exit with code 1.
Rich Output
Tova provides stdlib functions designed for CLI output. These are available everywhere, but especially useful inside cli {} blocks:
Colors
print(green("Success!"))
print(red("Error: something failed"))
print(yellow("Warning: check your config"))
print(bold("Important message"))
print(dim("Less important"))Available: green(), red(), yellow(), blue(), cyan(), magenta(), gray(), bold(), dim(), underline(), strikethrough(), color(text, name).
All color functions respect NO_COLOR and non-TTY environments.
Tables
table([
{name: "Alice", role: "Admin"},
{name: "Bob", role: "User"}
]) name | role
-------+------
Alice | Admin
Bob | UserPanels
panel("Status", "All systems operational\nUptime: 99.9%")┌─ Status ───────────────────┐
│ All systems operational │
│ Uptime: 99.9% │
└─────────────────────────────┘Progress Bars
for item in progress(items, label: "Processing") {
process(item)
}Shows a progress bar on stderr that updates in place:
Processing [████████░░░░░░░░] 50% 5/10Spinners
result = await spin("Deploying", async fn() {
await deploy_to_server()
})Shows a braille spinner animation while the async function runs, then a checkmark or cross on completion.
Interactive Prompts
For commands that need user input, Tova provides async prompt functions:
cli {
name: "setup"
async fn init() {
name = await ask("Project name:", default: "my-app")
lang = await choose("Language:", ["Tova", "TypeScript", "Python"])
confirmed = await confirm("Create project?")
if confirmed {
print(green("Creating {name} with {lang}"))
}
}
}| Function | Description |
|---|---|
ask(prompt, default?) | Text input with optional default |
confirm(prompt, default?) | Yes/no with [Y/n] or [y/N] hint |
choose(prompt, options) | Numbered list, returns selected value |
choose_many(prompt, options) | Comma-separated multi-select |
secret(prompt) | Hidden input with * masking |
See Scripting I/O for the full reference.
Running and Building
Running
tova run mycli.tova -- add "Buy milk" --priority 1Everything after -- is passed to the CLI as arguments.
Building
tova build src --output distCLI files produce a single .js file with a #!/usr/bin/env node shebang and executable permissions:
$ node dist/mycli.js --help
$ chmod +x dist/mycli.js && ./dist/mycli.js --helpStandalone Binary
tova build --binary mycliCompiles to a self-contained binary via bun build --compile.
Compile-Time Warnings
The analyzer produces warnings for:
| Code | Warning |
|---|---|
W_UNKNOWN_CLI_CONFIG | Unknown config key (valid: name, version, description) |
W_DUPLICATE_CLI_COMMAND | Two commands with the same name in a cli block |
W_POSITIONAL_AFTER_FLAG | Positional argument declared after a flag -- positionals should come first |
W_CLI_MISSING_NAME | No name: field in any cli block |
W_CLI_WITH_SERVER | cli {} and server {} in the same file -- cli produces a standalone executable, not a web server |
Top-Level Code
Code outside the cli {} block is included in the output as shared code. This is useful for type definitions, helper functions, and constants:
type Priority = Low | Medium | High
fn format_priority(p) {
match p {
Low => green("low")
Medium => yellow("medium")
High => red("high")
}
}
cli {
name: "tasks"
fn add(task: String, --priority: String = "medium") {
print("Added: {task} ({priority})")
}
}Complete Example
Here is a full-featured CLI tool:
cli {
name: "todo"
version: "0.1.0"
description: "A simple todo list manager"
fn add(task: String, --priority: Int = 3) {
print(green("Added: ") + bold(task) + dim(" (priority: {priority})"))
}
fn list(--all: Bool) {
print(bold("Your tasks:"))
print(" 1. Buy groceries (priority: 2)")
print(" 2. Write docs (priority: 1)")
if all {
print(dim(" 3. [done] Setup project"))
}
}
fn remove(id: Int) {
print(yellow("Removed task #{id}"))
}
}$ todo add "Buy milk" --priority 1
Added: Buy milk (priority: 1)
$ todo list --all
Your tasks:
1. Buy groceries (priority: 2)
2. Write docs (priority: 1)
3. [done] Setup project
$ todo remove 2
Removed task #2
$ todo --help
todo -- A simple todo list manager
Version: 0.1.0
USAGE:
todo <command> [options]
COMMANDS:
add
list
remove
OPTIONS:
--help, -h Show help
--version, -v Show version