Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ProjectUnified/CraftCommand/llms.txt

Use this file to discover all available pages before exploring further.

The Standalone platform lets you use CraftCommand in any Java application that has no Minecraft dependency. This is ideal for CLI tools, Discord bots, test harnesses, or any other program that needs a structured command dispatch system. Because the platform is Minecraft-agnostic, the sender type is plain Object — any value can be passed as the caller, including a String, a custom user object, or a mock in tests.
The Standalone platform uses Object as the sender type. You can pass any value — a String like "Console", a custom user object, or even null — as the sender when calling execute or tabComplete. This makes it easy to embed CraftCommand in any kind of host application.

Maven Setup

1

Add the runtime and annotations dependencies

The craftcommand-annotations artifact provides @Command, @Subcommand, @Default, @Optional, @Suggest, and the other compile-time annotations. The craftcommand-standalone-runtime artifact provides StandaloneCommandManager and StandaloneCommand, and must be included in your final JAR.
<dependencies>
    <!-- Compile-time annotations (provided scope is fine if you shade the runtime) -->
    <dependency>
        <groupId>io.github.projectunified</groupId>
        <artifactId>craftcommand-annotations</artifactId>
        <version>0.5.1</version>
        <scope>provided</scope>
    </dependency>

    <!-- Runtime: StandaloneCommandManager, StandaloneCommand -->
    <dependency>
        <groupId>io.github.projectunified</groupId>
        <artifactId>craftcommand-standalone-runtime</artifactId>
        <version>0.5.1</version>
    </dependency>
</dependencies>
2

Configure the annotation processor

Add craftcommand-standalone-processor to <annotationProcessorPaths> inside maven-compiler-plugin. The processor runs at compile time and generates the ClassName_Standalone wrapper classes — no reflection required at runtime.
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.15.0</version>
            <configuration>
                <annotationProcessorPaths>
                    <path>
                        <groupId>io.github.projectunified</groupId>
                        <artifactId>craftcommand-standalone-processor</artifactId>
                        <version>0.5.1</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

Writing a Command

Annotate your command class with @Command and define sub-operations with @Subcommand. Use @Default to handle the bare command invocation (no sub-label). The sender is always Object on the Standalone platform. The example below is the real CalculatorCommand from the CraftCommand standalone example module:
package io.github.projectunified.craftcommand.example.standalone;

import io.github.projectunified.craftcommand.annotation.*;
import io.github.projectunified.craftcommand.validation.annotation.Min;
import io.github.projectunified.craftcommand.validation.annotation.ValidateWith;

import java.util.Arrays;
import java.util.List;

@Command(value = "calc", aliases = {"c"}, description = "A simple CLI calculator demonstrating CraftCommand")
public class CalculatorCommand {

    // Operators list for suggestions
    public final List<String> operators = Arrays.asList("add", "subtract", "multiply", "divide");

    // Custom Resolver for Sender
    public CustomSender resolveSender(Object sender) {
        return new CustomSender(sender.toString());
    }

    // Custom Validation Method for validateDivider
    public void validateDivider(int num) {
        if (num == 0) {
            throw new IllegalArgumentException("Cannot divide by zero!");
        }
    }

    // Default action: addition of two numbers
    @Default
    public void execute(Object sender, int num1, int num2) {
        System.out.println("Result: " + (num1 + num2));
    }

    @Subcommand(value = "add")
    public void add(Object sender, int num1, int num2) {
        System.out.println("Result: " + (num1 + num2));
    }

    @Subcommand(value = "sub", aliases = {"subtract"})
    public void subtract(Object sender, int num1, int num2) {
        System.out.println("Result: " + (num1 - num2));
    }

    @Subcommand(value = "mul", aliases = {"multiply"})
    public void multiply(Object sender, int num1, int num2) {
        System.out.println("Result: " + (num1 * num2));
    }

    @Subcommand(value = "div", aliases = {"divide"})
    public void divide(Object sender, int num1, @ValidateWith("validateDivider") int num2) {
        System.out.println("Result: " + ((double) num1 / num2));
    }

    // Subcommand showcasing custom tab completions from a Field
    @Subcommand(value = "op")
    public void runOp(Object sender, @Suggest("operators") String op, int num1, int num2) {
        switch (op.toLowerCase()) {
            case "add":
                add(sender, num1, num2);
                break;
            case "subtract":
                subtract(sender, num1, num2);
                break;
            case "multiply":
                multiply(sender, num1, num2);
                break;
            case "divide":
                divide(sender, num1, num2);
                break;
            default:
                throw new IllegalArgumentException("Unknown operator: " + op);
        }
    }

    // Subcommand showcasing @Optional and @Greedy parameter matching
    @Subcommand(value = "print")
    public void print(Object sender, @Optional("Result:") String prefix, @Greedy String text) {
        System.out.println(prefix + " " + text);
    }

    @Subcommand("enumop")
    public void enumOp(Object sender, MathOp op, int num1, int num2) {
        switch (op) {
            case ADD:
                add(sender, num1, num2);
                break;
            case SUBTRACT:
                subtract(sender, num1, num2);
                break;
            case MULTIPLY:
                multiply(sender, num1, num2);
                break;
            case DIVIDE:
                divide(sender, num1, num2);
                break;
        }
    }

    // Use Case: Custom resolver for parameter 0 (the sender)
    @Subcommand("whoami")
    public void whoAmI(@Resolve("resolveSender") CustomSender sender) {
        System.out.println("You are: " + sender.getName());
    }

    // Dynamic type demo: Enum parameter using registerProvider in CLIApp
    public enum MathOp {
        ADD, SUBTRACT, MULTIPLY, DIVIDE
    }

    public static class CustomSender {
        private final String name;

        public CustomSender(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }
    }

    // Use Case: Nested subcommand class (inner class) with validation annotations (@Min, @Max)
    @Subcommand("advanced")
    public class AdvancedCommands {
        @Default
        public void execute(Object sender) {
            System.out.println("Advanced operations: power, sqrt");
        }

        @Subcommand("power")
        public void power(Object sender, double base, double exponent) {
            System.out.println("Result: " + Math.pow(base, exponent));
        }

        // Showcases @Min validation annotation on parameter
        @Subcommand("sqrt")
        public void sqrt(Object sender, @Min(0) double number) {
            System.out.println("Result: " + Math.sqrt(number));
        }
    }
}

@Default

Handles bare invocation — calc 5 3 — when no sub-label is provided.

@Subcommand

Routes calc add 5 3 to the add method. Supports aliases and nesting via inner classes.

@Optional

Makes a parameter optional with a default value: @Optional("Result:").

@Suggest

Sources tab-completion suggestions from a public field or method on the same class.

Registering and Executing

The real CLIApp from the example module shows the full lifecycle — creating the manager, registering a dynamic provider, registering the command class, dispatching execution, and requesting tab-completion suggestions:
package io.github.projectunified.craftcommand.example.standalone;

import io.github.projectunified.craftcommand.standalone.StandaloneCommand;
import io.github.projectunified.craftcommand.standalone.StandaloneCommandManager;

import java.util.Arrays;
import java.util.List;

public class CLIApp {
    public static void main(String[] args) {
        StandaloneCommandManager manager = new StandaloneCommandManager();

        // Register a dynamic provider for Java Enums
        manager.registerProvider(type -> {
            if (type.isEnum()) {
                return (sender, cmdArgs, current) -> Enum.valueOf((Class<Enum>) type, current.toUpperCase());
            }
            return null;
        });

        manager.register(new CalculatorCommand());

        System.out.println("=== Standalone CraftCommand CLI Calculator ===");
        System.out.println("Available commands: calc (or c)");
        System.out.println("Try typing: calc add 5 10");
        System.out.println("Try typing: calc op multiply 3 4");
        System.out.println("Try typing: calc print custom_prefix Hello World!");
        System.out.println("To see tab completions, type: suggest calc op ");
        System.out.println("Type 'exit' or 'quit' to close.");
        System.out.println("==============================================");

        // Run direct demonstration
        System.out.println("\n--- RUNNING DEMONSTRATION ---");
        System.out.println("Executing: calc add 40 2");
        executeLine(manager, "calc add 40 2");
        System.out.println("Executing: calc sub 100 50");
        executeLine(manager, "calc sub 100 50");
        System.out.println("Executing: calc op multiply 6 7");
        executeLine(manager, "calc op multiply 6 7");
        System.out.println("Executing: calc print custom_prefix Hello World!");
        executeLine(manager, "calc print custom_prefix Hello World!");
        System.out.println("Executing: calc advanced");
        executeLine(manager, "calc advanced");
        System.out.println("Executing: calc advanced power 2 10");
        executeLine(manager, "calc advanced power 2 10");
        System.out.println("Executing: calc advanced sqrt 16");
        executeLine(manager, "calc advanced sqrt 16");
        System.out.println("Executing: calc enumop multiply 7 8");
        executeLine(manager, "calc enumop multiply 7 8");
        System.out.println("Suggesting: calc op ");
        suggestLine(manager, "calc op ");
        System.out.println("Suggesting: calc advanced ");
        suggestLine(manager, "calc advanced ");
        System.out.println("-----------------------------\n");
    }

    private static void executeLine(StandaloneCommandManager manager, String line) {
        String[] parts = line.split(" ");
        String label = parts[0];
        StandaloneCommand cmd = manager.getCommand(label);
        if (cmd == null) {
            System.out.println("Unknown command: " + label);
        } else {
            String[] cmdArgs = Arrays.copyOfRange(parts, 1, parts.length);
            cmd.execute("Console", cmdArgs);
        }
    }

    private static void suggestLine(StandaloneCommandManager manager, String line) {
        String[] parts = line.split(" ", -1);
        String label = parts[0];
        StandaloneCommand cmd = manager.getCommand(label);
        if (cmd == null) {
            System.out.println("Unknown command: " + label);
        } else {
            String[] cmdArgs = Arrays.copyOfRange(parts, 1, parts.length);
            List<String> suggestions = cmd.tabComplete("Console", cmdArgs);
            System.out.println("Suggestions: " + suggestions);
        }
    }
}

StandaloneCommandManager API

StandaloneCommandManager extends CommandManager<Object> and manages the full lifecycle of command registration and lookup.

register(Object commandInstance)

Scans for a generated ClassName_Standalone wrapper class (produced by the annotation processor at compile time) and registers it along with all its aliases. Throws IllegalArgumentException if no wrapper is found.

registerCommand(StandaloneCommand command)

Registers a pre-built StandaloneCommand wrapper directly — useful when you construct the wrapper yourself or in tests.

getCommand(String label)

Looks up a registered command by its primary name or any alias. The lookup is case-insensitive. Returns null if no match is found.

getCommands()

Returns the full Map<String, StandaloneCommand> of every registered label (including aliases) to its command wrapper.

StandaloneCommand Interface

StandaloneCommand is the interface that every generated ClassName_Standalone wrapper implements. You can also implement it directly to integrate hand-written commands into the same manager.
MethodReturn typeDescription
getName()StringThe primary command name declared in @Command(value = …).
getAliases()List<String>The list of aliases declared in @Command(aliases = …).
getDescription()StringThe description string declared in @Command(description = …).
execute(Object sender, String[] args)booleanDispatches the command. Returns true on success. The first element of args is interpreted as the sub-label (or the first argument for @Default).
tabComplete(Object sender, String[] args)List<String>Returns completion suggestions based on the current argument tokens. Uses @Suggest field/method sources and enum constants automatically.

Error Handling

By default, StandaloneCommandManager rethrows any exception thrown during command execution as a RuntimeException:
// Default constructor — exceptions bubble up as RuntimeException
StandaloneCommandManager manager = new StandaloneCommandManager();
Supply a custom ErrorHandler<Object> to the constructor to change this behaviour — for example, logging to stderr and continuing instead of crashing:
import io.github.projectunified.craftcommand.ErrorHandler;

ErrorHandler<Object> handler = (sender, exception) -> {
    System.err.println("[Error] " + exception.getMessage());
    // Optionally log the full stack trace:
    // exception.printStackTrace();
};

StandaloneCommandManager manager = new StandaloneCommandManager(handler);
The ErrorHandler receives the sender object that was passed to execute or tabComplete, so you can tailor the error message to whatever your application uses as a “user” representation.

Build docs developers (and LLMs) love