Skip to content

Hot Reload & Plugins

The commands framework can watch your command files for changes and reload them automatically. Plugins let you package reusable middleware, hooks, and commands into a single installable unit.

When watch is enabled (the default), calling commands:load() also starts watching the loaded directory. Saving a command file triggers an automatic reload - no restart required.

local commandsManager = commands.new(bot) -- watch: true by default
bot.onAllShardsReady:listenOnce(function()
commandsManager:load("src/commands") -- loads and watches
end)

Disable watching if you don’t need it (e.g. in production):

local commandsManager = commands.new(bot, { watch = false })

Call commands:watch() separately to watch a directory without loading from it, or to watch additional directories after the initial load.

local stopWatching = commandsManager:watch("src/extra-commands")
stopWatching()

commands:destroy() stops every active watcher registered through commands:watch() or auto-started by commands:load().

commandsManager:destroy()

When a file changes, the loader writes the new file content to a temporary file with a randomised name, require()s that file to bypass the module cache, then deletes the temporary file. The new CommandDefinition replaces the old one in the registry. In-flight interactions using the previous definition complete normally.

A plugin is a table with a name and a setup function. setup receives the Commands instance so it can register middleware, after hooks, commands, or anything else. It can optionally return a teardown function.

local commands = require("../../luau_packages/commands")
local loggingPlugin = {
name = "logging",
setup = function(commandsManager: commands.Commands)
commandsManager:use(function(interaction: any, next: () -> ())
local anyInteraction = interaction :: any
local name = anyInteraction.data and anyInteraction.data.name or "interaction"
print(`[log] {name}`)
next()
end)
-- return a teardown function (optional)
return function()
print("[log] plugin unloaded")
end
end,
}

Install a plugin with commands:usePlugin():

commandsManager:usePlugin(loggingPlugin)

Installing the same plugin twice is a no-op - the framework warns and skips.

commandsManager:unloadPlugin("logging")

This calls the teardown function returned by setup, if one was provided.

The context option lets you pass a shared value (config, database handle, etc.) into every command factory function and into plugins.

Command files can receive it by exporting a factory function instead of a plain table:

-- src/commands/example.luau
local classes = require("../../luau_packages/classes")
return function(context)
return {
command = ...,
execute = function(interaction: classes.TypesCommand)
-- context.db, context.config, etc.
end,
}
end

Pass the context when creating the Commands instance:

local commandsManager = commands.new(bot, {
context = {
db = myDatabase,
config = myConfig,
},
})