rune

docs · authoring

Commands.

A command in Rune is a class. The class is decorated with @Command and a space-separated path; its fields are decorated with @Arg and become the command's typed positional arguments; one method is decorated with @Run and becomes the executor. The host wires the result into Paper's Brigadier tree.

The smallest command

Three decorators, one method, you're done. The class is constructed fresh per invocation, so this.arg is always the parsed value for the current call.

@Command("hello")
export class Hello {
  @Arg("target", { type: "player" })
  target!: Player;

  @Run
  run(ctx: CommandCtx) {
    ctx.sender.sendMessage(`hello, ${this.target.getName()}`);
  }
}

ctx.sender is the CommandSender who ran the command (a player, the console, or a command block).ctx.args is a typed record of every @Arg on the class — handy when you want to fan out to a helper rather than read this.

Argument types

The type on @Arg picks a Brigadier parser. The built-ins:

  • string — quoted string. word — single token. greedy — rest of the line.
  • int, long, double, bool — primitive numerics + boolean.
  • player, players — resolves to a single online Player or an array of them.
  • entity, entities — broader entity selectors.
  • world — a World by name.
  • block_pos — an x y z coordinate tuple, supporting ~ relative form.

Suggesters

Pass suggest on the argument options to attach an autocomplete provider. The function is called with the partial input and the context, and returns an array of strings synchronously (or a promise of one for async sources).

const groupSuggester = (partial: string) => {
  return cachedGroupNames()
    .filter((name) => name.startsWith(partial.toLowerCase()));
};

@Command("ward group")
export class WardGroup {
  @Arg("group", { type: "string", suggest: groupSuggester })
  group!: string;

  @Run
  async run(ctx: CommandCtx) {
    if (!requirePerm(ctx, "ward.group.info")) return;
    await renderGroupCard(ctx, this.group);
  }
}

Subcommand trees

@Command takes a space-separated path. Each segment becomes a literal in the Brigadier tree; each class is one leaf. The host stitches the tree together by matching prefixes, so ward group create and ward group prefix set can coexist as siblings of ward group.

@Command("ward group create")
export class WardGroupCreate {
  @Arg("name", { type: "string" })
  name!: string;

  @Run
  async run(ctx: CommandCtx) {
    if (!requirePerm(ctx, PERM.groupEdit)) return;
    if (!/^[a-z0-9_-]{1,32}$/i.test(this.name)) {
      err(ctx.sender, "group name must be 1-32 chars of [a-zA-Z0-9_-]");
      return;
    }
    const created = await createGroup(this.name);
    await refreshGroups();
    ok(ctx.sender, `created group ${created.name}`);
  }
}

The parent path (ward group on its own) is a valid leaf if you also declare it; without an explicit @Command("ward group") class the parent tab-completes to its children but won't run.

Aliases, descriptions, permissions

Pass an options object to @Command for everything Paper expects to know about your command at registration time.

@Command("ward", {
  description: "Ward permissions management",
  aliases: ["perms", "permissions"],
  permission: "ward.command.ward",
})
export class WardRoot {
  @Run
  run(ctx: CommandCtx) {
    ctx.sender.sendMessage("see /ward help");
  }
}

permission gates command discovery at the Bukkit level — players who don't have the node won't see the command in tab-complete. For finer-grained per-method gating, check a permission inside @Run and return early.

Optional and greedy arguments

Mark an argument optional with optional: true; the field will be undefined if the player didn't provide it. Use greedy: true on a string to slurp the rest of the input — useful for messages, descriptions, and free-form prefixes.

@Command("ward group prefix set")
export class WardGroupPrefixSet {
  @Arg("group", { type: "string", suggest: groupSuggester })
  group!: string;

  @Arg("text", { type: "greedy", greedy: true })
  text!: string;

  @Run
  async run(ctx: CommandCtx) {
    const parsed = splitPriorityText(this.text, 100);
    await setPrefix(this.group, parsed.text, parsed.priority);
  }
}

Imperative builder

If you'd rather construct commands at runtime — say, registering commands from a config file — use the builder. rune.command("ward") returns a fluent surface that mirrors the decorators.

rune.command("kick")
  .argument("target", "player")
  .argument("reason", "greedy", { optional: true })
  .run((ctx) => {
    const target = ctx.args.target as Player;
    const reason = (ctx.args.reason as string | undefined) ?? "no reason";
    target.kick(reason);
  })
  .register();