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.

This guide uses the Standalone platform — no Minecraft server required. You will define an annotated command class, wire up the annotation processor in Maven, and execute your first command from a plain main method. The same annotation model applies to the Bukkit and Paper platforms; only the runtime dependency and processor artifact change.
1

Add the BOM

Import the craftcommand-bom into your project’s <dependencyManagement> block. This lets you declare all other CraftCommand dependencies without specifying a version on each one.
pom.xml
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>io.github.projectunified</groupId>
            <artifactId>craftcommand-bom</artifactId>
            <version>0.5.1</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
2

Add the runtime and annotation dependencies

Add craftcommand-annotations (contains @Command, @Subcommand, @Default, @Optional, etc.) and craftcommand-standalone-runtime (contains StandaloneCommandManager and StandaloneCommand) to your <dependencies>. Because the BOM manages versions, no <version> tag is needed here.
pom.xml
<dependencies>
    <dependency>
        <groupId>io.github.projectunified</groupId>
        <artifactId>craftcommand-annotations</artifactId>
    </dependency>
    <dependency>
        <groupId>io.github.projectunified</groupId>
        <artifactId>craftcommand-standalone-runtime</artifactId>
    </dependency>
</dependencies>
3

Configure the annotation processor

Register craftcommand-standalone-processor as an annotation processor path in the maven-compiler-plugin. The processor runs during compilation and generates the *_Standalone wrapper classes — it must be in <annotationProcessorPaths>, not in <dependencies>.
pom.xml
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <annotationProcessorPaths>
                    <path>
                        <groupId>io.github.projectunified</groupId>
                        <artifactId>craftcommand-standalone-processor</artifactId>
                        <version>0.5.1</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>
4

Write your command class

Annotate a plain Java class with @Command to declare the root command name and description. Mark the fallback method with @Default and individual subcommands with @Subcommand. Use @Optional to make a parameter optional with a default value, and @Min (from the validation module) to enforce a numeric lower bound.The example below is taken directly from the CraftCommand standalone example module:
CalculatorCommand.java
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 tab-completion suggestions
    public final List<String> operators = Arrays.asList("add", "subtract", "multiply", "divide");

    // Custom resolver: converts the raw sender object to a typed CustomSender
    public CustomSender resolveSender(Object sender) {
        return new CustomSender(sender.toString());
    }

    // Custom validation method used by @ValidateWith on the divide subcommand
    public void validateDivider(int num) {
        if (num == 0) {
            throw new IllegalArgumentException("Cannot divide by zero!");
        }
    }

    // Default action: runs when no subcommand matches
    @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));
    }

    // @Suggest("operators") pulls completions from the field named "operators"
    @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);
        }
    }

    // @Optional gives the prefix a default value; @Greedy consumes all remaining tokens
    @Subcommand(value = "print")
    public void print(Object sender, @Optional("Result:") String prefix, @Greedy String text) {
        System.out.println(prefix + " " + text);
    }

    // Enum parameter dispatched via a dynamically-registered provider (see CLIApp)
    @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;
        }
    }

    // @Resolve("resolveSender") invokes the local resolver to convert the raw sender
    @Subcommand("whoami")
    public void whoAmI(@Resolve("resolveSender") CustomSender sender) {
        System.out.println("You are: " + sender.getName());
    }

    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; }
    }

    // Nested class becomes a nested subcommand group
    @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));
        }

        // @Min(0) validation: rejects negative inputs before the method body runs
        @Subcommand("sqrt")
        public void sqrt(Object sender, @Min(0) double number) {
            System.out.println("Result: " + Math.sqrt(number));
        }
    }
}
5

Register and run

Create a StandaloneCommandManager, optionally register custom type providers, call manager.register() with your command instance, then use the helper methods below to parse and dispatch command lines or collect tab-completion suggestions.
CLIApp.java
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 so the framework can parse Java Enum arguments
        manager.registerProvider(type -> {
            if (type.isEnum()) {
                return (sender, cmdArgs, current) -> Enum.valueOf((Class<Enum>) type, current.toUpperCase());
            }
            return null;
        });

        // Register the annotated command class
        manager.register(new CalculatorCommand());

        // Execute: "calc add 40 2"  →  prints "Result: 42"
        executeLine(manager, "calc add 40 2");

        // Execute: "calc sub 100 50"  →  prints "Result: 50"
        executeLine(manager, "calc sub 100 50");

        // Execute: "calc enumop MULTIPLY 6 7"  →  prints "Result: 42"
        executeLine(manager, "calc enumop multiply 6 7");

        // Execute: "calc advanced sqrt 16"  →  prints "Result: 4.0"
        executeLine(manager, "calc advanced sqrt 16");

        // Tab-completion for "calc op "  →  suggests ["add", "subtract", "multiply", "divide"]
        suggestLine(manager, "calc op ");
    }

    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);
        }
    }
}

What happens at compile time

When Maven compiles CalculatorCommand.java, craftcommand-standalone-processor detects the @Command("calc") annotation and generates a new class called CalculatorCommand_Standalone in your build output. That class implements StandaloneCommand and contains switch-based dispatch logic that maps string subcommand tokens to direct calls to your add(), subtract(), multiply(), enumOp(), whoAmI(), etc. methods. It also wires in any @Min / @ValidateWith validation checks before the method body runs. At runtime, StandaloneCommandManager.register() performs exactly one Class.forName("...CalculatorCommand_Standalone") call to locate the generated class, constructs it, and registers it. From that point on, every cmd.execute() and cmd.tabComplete() call goes through ordinary generated Java — no reflection per invocation.
For the complete Standalone platform guide — including custom type resolvers, error handlers, and the registerProvider API — see Standalone Platform.
To install CraftCommand for Bukkit/Spigot or Paper, or to add the optional validation module to any platform, see the Installation guide.

Build docs developers (and LLMs) love