CLI

Obsidian includes a built-in CLI to run custom commands from the terminal. Any class annotated with @Command and implementing Runnable is automatically discovered at startup — no registration needed.

Create a command
@Command(name = "greet", description = "Greet a user", aliases = {"g"})
public class GreetCommand implements Runnable {

    @Param(index = 0, name = "name", description = "Name to greet")
    private String name;

    @Option(name = "--upper", description = "Print in uppercase", flag = true)
    private boolean upper;

    @Override
    public void run() {
        String message = "Hello, " + name + "!";
        Printer.ok(upper ? message.toUpperCase() : message);
    }
}
Run from the terminal
obsidian greet Alice           # Run a command
obsidian greet Alice --upper   # With an option
obsidian --help                # List all commands
obsidian greet --help          # Help for a specific command
obsidian --version             # Framework version
Arguments and options
@Command(name = "migrate", description = "Run database migrations")
public class MigrateCommand implements Runnable {

    @Param(index = 0, name = "target", description = "Target migration", required = false)
    private String target;

    // Captures all remaining positional args
    @Param(index = 1, name = "files", description = "Migration files", variadic = true)
    private String[] files;

    // Option with a default value
    @Option(name = "--env", description = "Target environment", defaultValue = "development")
    private String env;

    // Boolean flag — no value expected
    @Option(name = "--dry-run", description = "Simulate without applying", flag = true)
    private boolean dryRun;

    // Required option — fails if missing
    @Option(name = "--db", description = "Database name", required = true)
    private String db;

    @Override
    public void run() { ... }
}
Printer — styled output
Printer.ok("Migration completed");       // green  [OK]
Printer.error("Connection failed");      // red    [ERROR]
Printer.warning("Deprecated usage");     // yellow [WARNING]
Printer.info("Starting server...");      // blue   [INFO]
Printer.note("Config file loaded");      // cyan   [NOTE]
Printer.caution("This will drop data");  // red    [CAUTION]
Printer.print("Plain text output");
Printer.print();                         // blank line
Interactive prompts
String name = CliComponents.Prompt.text("What is your name?", "Guest");

boolean confirmed = CliComponents.Prompt.confirm("Are you sure?", false);

String env = CliComponents.Prompt.select(
    "Select environment",
    "development", "staging", "production"
);
Progress bar and spinner
// Progress bar
CliComponents.ProgressBar bar = new CliComponents.ProgressBar(100, "Importing");
for (int i = 0; i <= 100; i++) {
    bar.update(i);
    Thread.sleep(20);
}
bar.finish();

// Spinner
CliComponents.Spinner spinner = new CliComponents.Spinner("Connecting to database...");
spinner.start();
// ... do work ...
spinner.stop("Connected!");
Table output
new CliComponents.Table("ID", "Name", "Status")
    .addRow("1", "Alice", "active")
    .addRow("2", "Bob",   "inactive")
    .addRow("3", "Carol", "active")
    .print();
💡 Behavior
  • • If no args are passed, the server starts normally — CLI doesn't interfere
  • • If the first arg matches a command name or alias, the command runs and the process exits
  • • ANSI colors are automatically disabled when no terminal is detected
  • • Use aliases in @Command to provide shorthand names