Skip to content

Command

luau
local Operator = require(game:GetService("ServerStorage").Operator)
local Command = Operator.Command

Builds a command. Every method returns the builder, so calls chain; :run returns the finished, frozen Command.

Functions

Command.new

luau
Command.new<A>(name: string): Builder<A>

Starts a builder. name must begin with a letter and contain only letters, digits, underscores and hyphens. Anything else raises immediately.

luau
export type KickArgs = { target: Player, reason: string }

local kick = Command.new("kick") :: Command.Builder<KickArgs>

The cast is required

local kick = Command.new("kick") :: Builder<KickArgs> works; the annotation form local kick: Builder<KickArgs> = Command.new("kick") fails on the new Luau solver.

The link between :arg(Types.Player, "target") and KickArgs.target: Player is not checked. Types are erased at runtime. You get a typed handler, not verified end-to-end type safety.

Builder<A>

:description

luau
:description(text: string): Builder<A>

Help text, shown by help and in the console's suggestion card.

:group

luau
:group(...: string): Builder<A>

Nests the command. :group("admin") on a command named ban gives the path admin ban.

:arg

luau
:arg(argType: AnyArgType, name: string, description: string?, options: ArgOptions?): Builder<A>

Adds a positional argument. Order of declaration is order on the command line.

luau
:arg(Types.Duration, "duration", "How long", { optional = true })
:arg(Types.String, "message", "What to say", { rest = true })
:arg(Types.Players, "targets", "Who", { variadic = true })

:flag

luau
:flag(name: string, description: string?, argType: AnyArgType?): Builder<A>

Adds a named flag. Passing argType makes it take a value (--reason=spam); omitting it makes it a presence flag (--silent).

:guard

luau
:guard(fn: GuardFn): Builder<A>

Adds a predicate run before the handler. Guards declared here run after any global guards on the dispatcher, in declaration order.

:permission

luau
:permission(role: string): Builder<A>

Gates the command behind a role. A command with permissions runs for a player holding any one of them, directly or inherited. Call it more than once to accept several roles.

The method is permission; the argument is a role name

That inconsistency is in the API itself, not just the docs. :permission("moderator") takes the name of a role, and everywhere else the package calls that a role.

:destructive

luau
:destructive(): Builder<A>

Forces a confirmation round-trip before running. With no confirm handler installed the command is cancelled rather than executed. It fails closed.

:noAudit

luau
:noAudit(): Builder<A>

Excludes the command from audit records.

:run

luau
:run(handler: (context: Context, args: A) -> ()): Command

Sets the handler and returns the finished command. Everything is validated here, not on first use: an argument named badly, a required argument after an optional one, a rest or variadic argument that is not last, or a duplicate flag all raise immediately. The returned command is frozen.

:runClient

luau
:runClient(handler: (context: ClientContext, args: A) -> ()): Command

Sets the handler and marks the command client-run: it executes on the executor's machine with no server round trip. The handler receives a ClientContext, which is narrower than the server one.

:run and :runClient are mutually exclusive. Declaring both on one builder is an error naming the command.

:destructive() and :runClient() are also mutually exclusive, in either order. A client cannot enforce a confirmation, and the same spelling must not mean two different guarantees.

Types

ArgOptions

luau
type ArgOptions = {
	optional: boolean?,
	variadic: boolean?,
	rest: boolean?,
}
OptionEffect
optionalabsence is not an error; must not precede a required argument
variadiccollects every remaining word into a list; must be last
restjoins every remaining word into one string; must be last

Argument

luau
type Argument = {
	name: string,
	description: string?,
	argType: AnyArgType,
	optional: boolean,
	variadic: boolean,
	rest: boolean,
}

Flag

luau
type Flag = {
	name: string,
	description: string?,
	argType: AnyArgType?,
	takesValue: boolean,
}

Command

The finished command, as the registry and dispatcher see it.

luau
type Command = {
	name: string,
	path: { string },
	description: string?,
	arguments: { Argument },
	flags: { Flag },
	argSpecs: { Binder.ArgSpec },
	flagSpecs: { Binder.FlagSpec },
	guards: { GuardFn },
	permissions: { string },
	destructive: boolean,
	audit: boolean,
	clientRun: boolean,
	run: (context: Context, args: any) -> (),
}

argSpecs and flagSpecs are the binder shapes, built once at :run() so the dispatcher does not rebuild them per call.

GuardFn

luau
type GuardFn = (context: Context) -> (boolean, string?)

Returning false denies, and the second value becomes the message the caller sees. Guards may yield and are each wrapped in a timeout. A guard that raises or times out denies, because guards fail closed.

Example

luau
local Operator = require(game:GetService("ServerStorage").Operator)
local Command = Operator.Command
local Types = Operator.Types

export type BanArgs = { target: Player, duration: number?, reason: string? }

return (Command.new("ban") :: Command.Builder<BanArgs>)
	:description("Ban a player")
	:arg(Types.Player, "target", "Who to ban")
	:arg(Types.Duration, "duration", "How long", { optional = true })
	:arg(Types.String, "reason", "Why", { optional = true, rest = true })
	:flag("silent", "Do not announce it")
	:permission("moderator")
	:destructive()
	:run(function(context, args)
		context:reply(`banned {args.target.Name}`)
	end)

Released under the MIT Licence.