Sign in

    admiral

    Declarative CLI builder for MoonBit, inspired by gunshi

    cli
    argparse
    moonbit
    Download zip
    Author
    Version
    0.1.1
    License
    MIT
    Last updated
    last month
    Downloads
    15K

    #admiral

    Declarative CLI builder for MoonBit, inspired by gunshi.

    A thin wrapper around moonbitlang/core/argparse that provides:

    • Typed option helpers (string, bool, int, positional)
    • Nested subcommand definitions with run callbacks
    • Structured JSON schema output for AI agent integration
    • Shell completion generation (bash, zsh, fish)
    • Auto-generated --help / --version

    #Install

    Add to moon.mod.json:

    { "deps": { "mizchi/admiral": "0.1.0" } }

    Add to moon.pkg.json:

    { "import": ["mizchi/admiral"] }

    #Quick Start

    fn main {
    let app = @admiral.cli(
    name="myapp",
    version="1.0.0",
    description="My CLI tool",
    commands=[
    @admiral.command(
    name="greet",
    description="Greet someone",
    options=[
    @admiral.string("name", short='n', description="Name to greet", required=true),
    @admiral.bool("verbose", short='v', description="Verbose output"),
    @admiral.int("count", short='c', description="Repeat count", default=Some(1)),
    ],
    examples=["myapp greet --name Alice", "myapp greet -n Bob -v -c 3"],
    run=Some(fn(ctx) {
    let name = try { ctx.get_string_required("name") } catch { _ => return }
    let verbose = ctx.get_bool("verbose")
    let count = match ctx.get_int("count") { Some(n) => n; None => 1 }
    for i = 0; i < count; i = i + 1 {
    if verbose {
    println("Hello, " + name + "! (" + (i + 1).to_string() + ")")
    } else {
    println("Hello, " + name + "!")
    }
    }
    }),
    ),
    ],
    )
    try { app.run() } catch { err => println(err) }
    }

    $ myapp greet --name Alice Hello, Alice! $ myapp greet -n Bob -v -c 3 Hello, Bob! (1) Hello, Bob! (2) Hello, Bob! (3) $ myapp --help Usage: myapp [command] My CLI tool Commands: greet Greet someone Options: -h, --help Show help information. -V, --version Show version information.

    #Guide

    #Defining Options

    Three types of options, plus positional arguments:

    // String option: --name value or -n value
    @admiral.string("name", short='n', description="User name", required=true)

    // Bool flag: --verbose or -v
    @admiral.bool("verbose", short='v', description="Verbose output")

    // Int option: --port 8080 or -p 8080
    @admiral.int("port", short='p', description="Port number", default=Some(3000))

    // Positional argument
    @admiral.positional("file", description="Input file", required=true)

    short is optional — omit it to only allow the long form (--name).

    #Reading Values from Context

    Inside a run callback, use Context methods to read parsed values:

    run=Some(fn(ctx) {
    // Bool — returns false if not specified
    let verbose = ctx.get_bool("verbose")

    // String — returns None if not specified
    let name = ctx.get_string("name") // String?

    // String (required) — raises if missing
    let name = try { ctx.get_string_required("name") } catch { _ => return }

    // Int — parses string value to Int, returns None if missing or invalid
    let port = ctx.get_int("port") // Int?

    // Int (required) — raises if missing or not a valid integer
    let port = try { ctx.get_int_required("port") } catch { _ => return }

    // Multiple values (e.g., positional args that accept multiple values)
    let files = ctx.get_strings("files") // Array[String]
    })

    #Nested Subcommands

    Commands can nest arbitrarily deep:

    let app = @admiral.cli(
    name="myapp",
    commands=[
    @admiral.command(
    name="db",
    description="Database commands",
    subcommands=[
    @admiral.command(
    name="migrate",
    description="Run migrations",
    subcommands=[
    @admiral.command(
    name="up",
    description="Apply pending migrations",
    options=[
    @admiral.bool("dry-run", description="Preview without applying"),
    @admiral.int("steps", short='s', description="Number of steps"),
    ],
    examples=[
    "myapp db migrate up",
    "myapp db migrate up --dry-run",
    "myapp db migrate up --steps 5",
    ],
    run=Some(fn(ctx) {
    if ctx.get_bool("dry-run") {
    println("[DRY RUN] Would apply migrations")
    } else {
    match ctx.get_int("steps") {
    Some(n) => println("Applying " + n.to_string() + " migrations...")
    None => println("Applying all pending migrations...")
    }
    }
    }),
    ),
    @admiral.command(
    name="down",
    description="Rollback migrations",
    options=[
    @admiral.int("steps", short='s', description="Steps to rollback", default=Some(1)),
    ],
    run=Some(fn(ctx) {
    let steps = match ctx.get_int("steps") { Some(n) => n; None => 1 }
    println("Rolling back " + steps.to_string() + " migration(s)...")
    }),
    ),
    ],
    ),
    @admiral.command(
    name="seed",
    description="Seed the database",
    options=[
    @admiral.string("file", short='f', description="Seed file", default=Some("seeds/default.sql")),
    ],
    run=Some(fn(ctx) {
    let file = match ctx.get_string("file") { Some(f) => f; None => "seeds/default.sql" }
    println("Seeding from: " + file)
    }),
    ),
    ],
    ),
    ],
    )

    $ myapp db migrate up --dry-run [DRY RUN] Would apply migrations $ myapp db migrate down --steps 3 Rolling back 3 migration(s)... $ myapp db seed --file custom.sql Seeding from: custom.sql

    #Positional Arguments

    @admiral.command(
    name="cat",
    description="Concatenate files",
    positionals=[
    @admiral.positional("files", description="Files to concatenate"),
    ],
    run=Some(fn(ctx) {
    let files = ctx.get_strings("files")
    for file in files {
    println("Reading: " + file)
    }
    }),
    )

    $ myapp cat a.txt b.txt c.txt Reading: a.txt Reading: b.txt Reading: c.txt

    #Testing with Explicit argv

    // In tests, pass argv explicitly:
    app.run(argv=Some(["greet", "--name", "alice"]))

    // In production, omit argv to use process args:
    app.run()

    #Structured Schema Output

    admiral can output the full CLI definition as JSON — useful for AI agents, documentation generators, and tooling:

    println(app.render_schema()) // -> JSON string
    let json = app.render_schema_json() // -> Json value

    Example output:

    { "name": "myapp", "version": "1.0.0", "description": "My CLI tool", "commands": { "greet": { "description": "Greet someone", "options": { "name": { "type": "string", "description": "Name to greet", "required": true, "short": "n" }, "verbose": { "type": "bool", "description": "Verbose output", "required": false, "short": "v" }, "count": { "type": "int", "description": "Repeat count", "required": false, "short": "c", "default": "1" } }, "examples": ["myapp greet --name Alice", "myapp greet -n Bob -v -c 3"] }, "db": { "description": "Database commands", "commands": { "migrate": { "description": "Run migrations", "commands": { "up": { "description": "Apply pending migrations", "options": { "dry-run": { "type": "bool", "description": "Preview without applying", "required": false }, "steps": { "type": "int", "description": "Number of steps", "required": false, "short": "s" } }, "examples": ["myapp db migrate up", "myapp db migrate up --dry-run"] } } } } } } }

    This enables AI agents to understand CLI interfaces without parsing --help text — types, required/optional, defaults, and examples are all machine-readable.

    #Shell Completion

    Generate completion scripts for bash, zsh, and fish:

    // Bash
    println(app.render_bash_completion())

    // Zsh
    println(app.render_zsh_completion())

    // Fish
    println(app.render_fish_completion())

    Typical usage — add a completion subcommand:

    @admiral.command(
    name="completion",
    description="Generate shell completion script",
    options=[@admiral.string("shell", short='s', description="Shell type (bash, zsh, fish)", required=true)],
    run=Some(fn(ctx) {
    match ctx.get_string("shell") {
    Some("bash") => println(app.render_bash_completion())
    Some("zsh") => println(app.render_zsh_completion())
    Some("fish") => println(app.render_fish_completion())
    _ => println("Unsupported shell. Use: bash, zsh, fish")
    }
    }),
    )

    # Bash: add to ~/.bashrc eval "$(myapp completion --shell bash)" # Zsh: add to ~/.zshrc eval "$(myapp completion --shell zsh)" # Fish: save to completions dir myapp completion --shell fish > ~/.config/fish/completions/myapp.fish

    #API Reference

    #Option Helpers

    FunctionDescription
    string(name, short?, description?, required?, default?)String option (--name value)
    bool(name, short?, description?)Boolean flag (--verbose)
    int(name, short?, description?, required?, default?)Integer option (--port 8080)
    positional(name, description?, required?)Positional argument

    #Command Definition

    FunctionDescription
    command(name, description?, options?, positionals?, examples?, subcommands?, run?)Define a command or subcommand
    cli(name, version?, description?, options?, commands?)Create a CLI app

    #Context Methods

    MethodReturnDescription
    get_bool(name)BoolFlag value (default: false)
    get_string(name)String?First string value
    get_string_required(name)String raiseFirst value, raises if missing
    get_int(name)Int?Parsed integer value
    get_int_required(name)Int raiseParsed integer, raises if missing/invalid
    get_strings(name)Array[String]All values for an option
    get_subcommand()(String, Context)?Selected subcommand name and context

    #Schema & Completion

    MethodReturnDescription
    render_schema()StringFull CLI definition as JSON string
    render_schema_json()JsonFull CLI definition as Json value
    render_bash_completion()StringBash completion script
    render_zsh_completion()StringZsh completion script
    render_fish_completion()StringFish completion script

    #Running

    app.run() // use process args
    app.run(argv=Some(["greet", "--name", "x"])) // explicit args (for testing)

    --help and --version are automatically handled by argparse.

    #Targets

    Supports all MoonBit targets: native, js, wasm-gc.

    #License

    MIT

    CliApp

    pub(all) struct CliApp {
    name : String
    version : String
    description : String
    root_options : Array[OptionDef]
    commands : Array[CommandDef]
    }

    CliApp::render_bash_completion

    fn CliApp::render_bash_completion(self : CliApp) -> String

    CliApp::render_fish_completion

    fn CliApp::render_fish_completion(self : CliApp) -> String

    CliApp::render_schema

    fn CliApp::render_schema(self : CliApp) -> String

    CliApp::render_schema_json

    fn CliApp::render_schema_json(self : CliApp) -> Json

    CliApp::render_zsh_completion

    fn CliApp::render_zsh_completion(self : CliApp) -> String

    CliApp::run

    fn CliApp::run(self : CliApp, argv? : Array[String]?) -> Unit raise

    CommandDef

    pub(all) struct CommandDef {
    name : String
    description : String
    options : Array[OptionDef]
    positionals : Array[PositionalDef]
    examples : Array[String]
    subcommands : Array[CommandDef]
    run : (Context) -> Unit?
    }

    Context

    pub(all) struct Context {
    flags : Map[String, Bool]
    values : Map[String, Array[String]]
    subcommand : (String, Context)?
    }

    Context::get_bool

    fn Context::get_bool(self : Context, name : String) -> Bool

    Context::get_int

    fn Context::get_int(self : Context, name : String) -> Int?

    Context::get_int_required

    fn Context::get_int_required(self : Context, name : String) -> Int raise

    Context::get_string

    fn Context::get_string(self : Context, name : String) -> String?

    Context::get_string_required

    fn Context::get_string_required(self : Context, name : String) -> String raise

    Context::get_strings

    fn Context::get_strings(self : Context, name : String) -> Array[String]

    Context::get_subcommand

    fn Context::get_subcommand(self : Context) -> (String, Context)?

    OptionDef

    pub(all) struct OptionDef {
    name : String
    type_ : OptionType
    short : Char
    description : String
    required : Bool
    default_value : String?
    } derive(
    Debug
    )

    OptionType

    pub(all) enum OptionType {
    StringOpt
    BoolOpt
    IntOpt
    } derive(Eq,
    Debug
    )

    impl Show for OptionType

    PositionalDef

    pub(all) struct PositionalDef {
    name : String
    description : String
    required : Bool
    }

    bool

    fn bool(name : String, short? : Char, description? : String) -> OptionDef

    build_command

    cli

    fn cli(name~ : String, version? : String, description? : String, options? : Array[OptionDef], commands? : Array[CommandDef]) -> CliApp

    command

    fn command(name~ : String, description? : String, options? : Array[OptionDef], positionals? : Array[PositionalDef], examples? : Array[String], subcommands? : Array[CommandDef], run? : (Context) -> Unit?) -> CommandDef

    int

    fn int(name : String, short? : Char, description? : String, required? : Bool, default? : Int?) -> OptionDef

    positional

    fn positional(name : String, description? : String, required? : Bool) -> PositionalDef

    string

    fn string(name : String, short? : Char, description? : String, required? : Bool, default? : String?) -> OptionDef