-### cancelled
-
-
-
-Whether the command execution was cancelled.
-### cancel
-
-
void`} />
-
-Cancels the command execution.
### setCommandRunner
RunCommand) => void`} />
diff --git a/apps/website/docs/api-reference/commandkit/functions/exit-middleware.mdx b/apps/website/docs/api-reference/commandkit/functions/exit-middleware.mdx
deleted file mode 100644
index 65882d98..00000000
--- a/apps/website/docs/api-reference/commandkit/functions/exit-middleware.mdx
+++ /dev/null
@@ -1,25 +0,0 @@
----
-title: "ExitMiddleware"
-isDefaultIndex: false
-generated: true
----
-
-import MemberInfo from '@site/src/components/MemberInfo';
-import GenerationInfo from '@site/src/components/GenerationInfo';
-import MemberDescription from '@site/src/components/MemberDescription';
-
-
-
-
-## exitMiddleware
-
-
-
-Cancel upcoming middleware execution.
-If this is called inside pre-stage middleware, the next run will be the actual command, skipping all other pre-stage middlewares.
-If this is called inside a command itself, it will skip all post-stage middlewares.
-If this is called inside post-stage middleware, it will skip all other post-stage middlewares.
-
-```ts title="Signature"
-function exitMiddleware(): never
-```
diff --git a/apps/website/docs/api-reference/commandkit/functions/flag.mdx b/apps/website/docs/api-reference/commandkit/functions/flag.mdx
index f9bc3858..e5b436d7 100644
--- a/apps/website/docs/api-reference/commandkit/functions/flag.mdx
+++ b/apps/website/docs/api-reference/commandkit/functions/flag.mdx
@@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
## flag
-
+
Create a new feature flag.
diff --git a/apps/website/docs/api-reference/commandkit/functions/get-command-kit.mdx b/apps/website/docs/api-reference/commandkit/functions/get-command-kit.mdx
index 6e147f6b..9823dad1 100644
--- a/apps/website/docs/api-reference/commandkit/functions/get-command-kit.mdx
+++ b/apps/website/docs/api-reference/commandkit/functions/get-command-kit.mdx
@@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
## getCommandKit
-
+
diff --git a/apps/website/docs/api-reference/commandkit/functions/redirect.mdx b/apps/website/docs/api-reference/commandkit/functions/redirect.mdx
index b7a91a51..64fdd3d6 100644
--- a/apps/website/docs/api-reference/commandkit/functions/redirect.mdx
+++ b/apps/website/docs/api-reference/commandkit/functions/redirect.mdx
@@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
## redirect
-
+
Stops current command assuming it has been redirected to another command.
diff --git a/apps/website/docs/api-reference/commandkit/functions/rethrow.mdx b/apps/website/docs/api-reference/commandkit/functions/rethrow.mdx
index 60ab12d1..8993ab6f 100644
--- a/apps/website/docs/api-reference/commandkit/functions/rethrow.mdx
+++ b/apps/website/docs/api-reference/commandkit/functions/rethrow.mdx
@@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
## rethrow
-
+
Rethrow the error if it is a CommandKit error.
diff --git a/apps/website/docs/api-reference/commandkit/functions/stop-events.mdx b/apps/website/docs/api-reference/commandkit/functions/stop-events.mdx
index 2e7994c8..0d0d58b9 100644
--- a/apps/website/docs/api-reference/commandkit/functions/stop-events.mdx
+++ b/apps/website/docs/api-reference/commandkit/functions/stop-events.mdx
@@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
## stopEvents
-
+
Stops event propagation. This function should be called inside an event handler
to prevent further event handling.
diff --git a/apps/website/docs/api-reference/commandkit/functions/stop-middlewares.mdx b/apps/website/docs/api-reference/commandkit/functions/stop-middlewares.mdx
new file mode 100644
index 00000000..759c5b95
--- /dev/null
+++ b/apps/website/docs/api-reference/commandkit/functions/stop-middlewares.mdx
@@ -0,0 +1,22 @@
+---
+title: "StopMiddlewares"
+isDefaultIndex: false
+generated: true
+---
+
+import MemberInfo from '@site/src/components/MemberInfo';
+import GenerationInfo from '@site/src/components/GenerationInfo';
+import MemberDescription from '@site/src/components/MemberDescription';
+
+
+
+
+## stopMiddlewares
+
+
+
+Stop upcoming middlewares and command execution.
+
+```ts title="Signature"
+function stopMiddlewares(): never
+```
diff --git a/apps/website/docs/api-reference/commandkit/functions/use-environment.mdx b/apps/website/docs/api-reference/commandkit/functions/use-environment.mdx
index ce6df1b0..c052206e 100644
--- a/apps/website/docs/api-reference/commandkit/functions/use-environment.mdx
+++ b/apps/website/docs/api-reference/commandkit/functions/use-environment.mdx
@@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
## useEnvironment
-
+
Use current commandkit context. Throws an error if no context is found.
diff --git a/apps/website/docs/api-reference/commandkit/interfaces/command-flag-context.mdx b/apps/website/docs/api-reference/commandkit/interfaces/command-flag-context.mdx
index 2fec6a23..5af9d70a 100644
--- a/apps/website/docs/api-reference/commandkit/interfaces/command-flag-context.mdx
+++ b/apps/website/docs/api-reference/commandkit/interfaces/command-flag-context.mdx
@@ -21,34 +21,34 @@ Context for evaluating command flags in CommandKit.
interface CommandFlagContext {
client: Client;
commandkit: CommandKit;
- command: {
- /**
- * The interaction object if the command was invoked via an interaction.
- * This can be a ChatInputCommandInteraction, AutocompleteInteraction, or ContextMenuCommandInteraction.
- */
- interaction?:
- | ChatInputCommandInteraction
- | AutocompleteInteraction
- | ContextMenuCommandInteraction;
- /**
- * The message object if the command was invoked via a message.
- */
- message?: Message;
- /**
- * The guild where the command was invoked, if applicable.
- * This will be null for commands invoked in DMs.
- */
- guild: Guild | null;
- /**
- * The channel where the command was invoked.
- * This can be a text channel, DM channel, or any other type of text-based channel.
- */
- channel: TextBasedChannel | null;
- /**
- * The loaded command instance that is being executed.
- * This contains the command's metadata and logic.
- */
- command: LoadedCommand;
+ command: {
+ /**
+ * The interaction object if the command was invoked via an interaction.
+ * This can be a ChatInputCommandInteraction, AutocompleteInteraction, or ContextMenuCommandInteraction.
+ */
+ interaction?:
+ | ChatInputCommandInteraction
+ | AutocompleteInteraction
+ | ContextMenuCommandInteraction;
+ /**
+ * The message object if the command was invoked via a message.
+ */
+ message?: Message;
+ /**
+ * The guild where the command was invoked, if applicable.
+ * This will be null for commands invoked in DMs.
+ */
+ guild: Guild | null;
+ /**
+ * The channel where the command was invoked.
+ * This can be a text channel, DM channel, or any other type of text-based channel.
+ */
+ channel: TextBasedChannel | null;
+ /**
+ * The loaded command instance that is being executed.
+ * This contains the command's metadata and logic.
+ */
+ command: LoadedCommand;
};
event: null;
}
@@ -60,25 +60,25 @@ interface CommandFlagContext {
-The Discord client instance.
+The Discord client instance.
This is the main entry point for interacting with the Discord API.
### commandkit
CommandKit`} />
-The CommandKit instance, which provides access to the command framework.
+The CommandKit instance, which provides access to the command framework.
This includes commands, events, and other features of CommandKit.
### command
-LoadedCommand; }`} />
+LoadedCommand;
}`} />
-The command context, which includes information about the command being executed.
+The command context, which includes information about the command being executed.
This can include the interaction, message, guild, channel, and the loaded command.
### event
-The event context is null for command flags, as they are not tied to a specific event.
+The event context is null for command flags, as they are not tied to a specific event.
This is used to differentiate between command and event flags.
diff --git a/apps/website/docs/api-reference/commandkit/interfaces/context-parameters.mdx b/apps/website/docs/api-reference/commandkit/interfaces/context-parameters.mdx
index 055f9c5a..d5c2939f 100644
--- a/apps/website/docs/api-reference/commandkit/interfaces/context-parameters.mdx
+++ b/apps/website/docs/api-reference/commandkit/interfaces/context-parameters.mdx
@@ -22,14 +22,14 @@ interface ContextParameters
+
### message
diff --git a/apps/website/docs/api-reference/commandkit/interfaces/event-flag-context.mdx b/apps/website/docs/api-reference/commandkit/interfaces/event-flag-context.mdx
index be20fb31..11b19d7e 100644
--- a/apps/website/docs/api-reference/commandkit/interfaces/event-flag-context.mdx
+++ b/apps/website/docs/api-reference/commandkit/interfaces/event-flag-context.mdx
@@ -21,35 +21,35 @@ Context for evaluating event flags in CommandKit.
interface EventFlagContext {
client: Client;
commandkit: CommandKit;
- event: {
- /**
- * The parsed event data, which contains the raw data from the event.
- * This can include information like user IDs, channel IDs, and other relevant data.
- */
- data: ParsedEvent;
- /**
- * The name of the event being processed.
- * This is the string identifier for the event, such as 'messageCreate' or 'guildMemberAdd'.
- */
- event: string;
- /**
- * The namespace of the event, if applicable.
- * This can be used to group related events or commands together.
- * It is null if the event does not belong to a specific namespace.
- */
- namespace: string | null;
- /**
- * The arguments passed to the event handler.
- * This is an array of arguments that were passed when the event was triggered.
- * It can be used to access specific data related to the event.
- */
- arguments: any[];
- /**
- * A function to retrieve the arguments for a specific event type.
- * This allows for type-safe access to the arguments based on the event name.
- * @param event - The name of the event to retrieve arguments for.
- */
- argumentsAs(event: E): ClientEvents[E];
+ event: {
+ /**
+ * The parsed event data, which contains the raw data from the event.
+ * This can include information like user IDs, channel IDs, and other relevant data.
+ */
+ data: ParsedEvent;
+ /**
+ * The name of the event being processed.
+ * This is the string identifier for the event, such as 'messageCreate' or 'guildMemberAdd'.
+ */
+ event: string;
+ /**
+ * The namespace of the event, if applicable.
+ * This can be used to group related events or commands together.
+ * It is null if the event does not belong to a specific namespace.
+ */
+ namespace: string | null;
+ /**
+ * The arguments passed to the event handler.
+ * This is an array of arguments that were passed when the event was triggered.
+ * It can be used to access specific data related to the event.
+ */
+ arguments: any[];
+ /**
+ * A function to retrieve the arguments for a specific event type.
+ * This allows for type-safe access to the arguments based on the event name.
+ * @param event - The name of the event to retrieve arguments for.
+ */
+ argumentsAs(event: E): ClientEvents[E];
};
command: null;
}
@@ -61,25 +61,25 @@ interface EventFlagContext {
-The Discord client instance.
+The Discord client instance.
This is the main entry point for interacting with the Discord API.
### commandkit
CommandKit`} />
-The CommandKit instance, which provides access to the command framework.
+The CommandKit instance, which provides access to the command framework.
This includes commands, events, and other features of CommandKit.
### event
-ParsedEvent; /** * The name of the event being processed. * This is the string identifier for the event, such as 'messageCreate' or 'guildMemberAdd'. */ event: string; /** * The namespace of the event, if applicable. * This can be used to group related events or commands together. * It is null if the event does not belong to a specific namespace. */ namespace: string | null; /** * The arguments passed to the event handler. * This is an array of arguments that were passed when the event was triggered. * It can be used to access specific data related to the event. */ arguments: any[]; /** * A function to retrieve the arguments for a specific event type. * This allows for type-safe access to the arguments based on the event name. * @param event - The name of the event to retrieve arguments for. */ argumentsAs<E extends keyof ClientEvents>(event: E): ClientEvents[E]; }`} />
+ParsedEvent;
/**
* The name of the event being processed.
* This is the string identifier for the event, such as 'messageCreate' or 'guildMemberAdd'.
*/
event: string;
/**
* The namespace of the event, if applicable.
* This can be used to group related events or commands together.
* It is null if the event does not belong to a specific namespace.
*/
namespace: string | null;
/**
* The arguments passed to the event handler.
* This is an array of arguments that were passed when the event was triggered.
* It can be used to access specific data related to the event.
*/
arguments: any[];
/**
* A function to retrieve the arguments for a specific event type.
* This allows for type-safe access to the arguments based on the event name.
* @param event - The name of the event to retrieve arguments for.
*/
argumentsAs<E extends keyof ClientEvents>(event: E): ClientEvents[E];
}`} />
-The event context, which includes information about the event being processed.
+The event context, which includes information about the event being processed.
This can include the parsed event data, the event name, and the namespace if applicable.
### command
-The command context is null for event flags, as they are not tied to a specific command.
+The command context is null for event flags, as they are not tied to a specific command.
This is used to differentiate between command and event flags.
diff --git a/apps/website/docs/api-reference/commandkit/types/filter-function.mdx b/apps/website/docs/api-reference/commandkit/types/filter-function.mdx
index 17409dc9..0b7aae9f 100644
--- a/apps/website/docs/api-reference/commandkit/types/filter-function.mdx
+++ b/apps/website/docs/api-reference/commandkit/types/filter-function.mdx
@@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
## FilterFunction
-
+
Filter function type for analytics events.
diff --git a/apps/website/docs/api-reference/commandkit/variables/middleware-id.mdx b/apps/website/docs/api-reference/commandkit/variables/middleware-id.mdx
index 90b18e61..1873c9ea 100644
--- a/apps/website/docs/api-reference/commandkit/variables/middleware-id.mdx
+++ b/apps/website/docs/api-reference/commandkit/variables/middleware-id.mdx
@@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
## middlewareId
-
+
diff --git a/apps/website/docs/api-reference/legacy/classes/legacy-handler-plugin.mdx b/apps/website/docs/api-reference/legacy/classes/legacy-handler-plugin.mdx
index c2a6f4c8..fffff894 100644
--- a/apps/website/docs/api-reference/legacy/classes/legacy-handler-plugin.mdx
+++ b/apps/website/docs/api-reference/legacy/classes/legacy-handler-plugin.mdx
@@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
## LegacyHandlerPlugin
-
+
diff --git a/apps/website/docs/api-reference/legacy/interfaces/legacy-handler-plugin-options.mdx b/apps/website/docs/api-reference/legacy/interfaces/legacy-handler-plugin-options.mdx
index f52777ac..04505ff7 100644
--- a/apps/website/docs/api-reference/legacy/interfaces/legacy-handler-plugin-options.mdx
+++ b/apps/website/docs/api-reference/legacy/interfaces/legacy-handler-plugin-options.mdx
@@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
## LegacyHandlerPluginOptions
-
+
Options for the LegacyHandlerPlugin.
diff --git a/apps/website/docs/guide/01-getting-started/02-setup-commandkit.mdx b/apps/website/docs/guide/01-getting-started/02-setup-commandkit.mdx
index 25c7a724..e5289cda 100644
--- a/apps/website/docs/guide/01-getting-started/02-setup-commandkit.mdx
+++ b/apps/website/docs/guide/01-getting-started/02-setup-commandkit.mdx
@@ -36,7 +36,7 @@ that looks something like this:
│ │ ├── commands/
│ │ │ └── ping.ts
│ │ └── events/
-│ │ └── ready/
+│ │ └── clientReady/
│ │ └── log.ts
│ └── app.ts
├── .env
diff --git a/apps/website/docs/guide/02-commands/01-chat-input-commands.mdx b/apps/website/docs/guide/02-commands/01-chat-input-commands.mdx
index 674a984b..7f4bb3ae 100644
--- a/apps/website/docs/guide/02-commands/01-chat-input-commands.mdx
+++ b/apps/website/docs/guide/02-commands/01-chat-input-commands.mdx
@@ -49,19 +49,3 @@ supports. Learn more about
[context menu commands](./03-context-menu-commands.mdx).
:::
-
-## Guild-based commands
-
-You can register your chat input command to specific guilds by using
-the `guilds` property in the `command` object which accepts an array
-of guild IDs.
-
-```ts title="src/app/commands/ping.ts" {6}
-import type { CommandData } from 'commandkit';
-
-export const command: CommandData = {
- name: 'ping',
- description: 'Replies with Pong!',
- guilds: ['1055188344188973066'],
-};
-```
diff --git a/apps/website/docs/guide/02-commands/05-command-metadata.mdx b/apps/website/docs/guide/02-commands/05-command-metadata.mdx
new file mode 100644
index 00000000..b941c5db
--- /dev/null
+++ b/apps/website/docs/guide/02-commands/05-command-metadata.mdx
@@ -0,0 +1,98 @@
+---
+title: Command Metadata
+---
+
+Command metadata is a way to add additional information to your
+commands that CommandKit will handle.
+
+To get started, you can export a `metadata` object from your command:
+
+```ts title="src/app/commands/ping.ts" {8-10}
+import type { CommandData, CommandMetadata } from 'commandkit';
+
+export const command: CommandData = {
+ name: 'ping',
+ description: 'Replies with Pong!',
+};
+
+export const metadata: CommandMetadata = {
+ // Add your metadata here
+};
+```
+
+## Metadata properties
+
+### `userPermissions`
+
+This is a string, or array of user permission strings that will be
+required by the person executing the command.
+
+```ts title="src/app/commands/ping.ts"
+export const metadata: CommandMetadata = {
+ // If the user does not have the Administrator permission, CommandKit will let them know
+ userPermissions: 'Administrator',
+};
+```
+
+### `botPermissions`
+
+This is a string, or array of bot permission strings that will be
+required by your bot to execute the command. This is useful for
+commands where your bot needs to have certain permissions in a guild
+e.g. moderation commands.
+
+```ts title="src/app/commands/ping.ts"
+export const metadata: CommandMetadata = {
+ // If the bot does not have these permissions, CommandKit will let them know
+ botPermissions: ['KickMembers', 'BanMembers'],
+};
+```
+
+### `guilds`
+
+This is an array of guild IDs that the command will be registered in,
+or be available to be executed (message commands).
+
+```ts title="src/app/commands/ping.ts"
+export const metadata: CommandMetadata = {
+ guilds: ['1234567890', '1234567891'],
+};
+```
+
+### `aliases`
+
+This is an array of alternative command names that will be available
+for users to use to execute the command.
+
+:::warning
+
+This only works for [message commands](./04-message-commands.mdx).
+
+:::
+
+```ts title="src/app/commands/ping.ts"
+export const metadata: CommandMetadata = {
+ aliases: ['p', 'pong'],
+};
+```
+
+## Generated metadata
+
+If you'd like to generate metadata dynamically, you can export a
+`generateMetadata` function from your command file that should return
+a `CommandMetadata` object.
+
+```ts title="src/app/commands/ping.ts"
+import type { CommandMetadataFunction } from 'commandkit';
+
+export const generateMetadata: CommandMetadataFunction = async () => {
+ // Dynamically determine the metadata for the command
+
+ return {
+ userPermissions: 'Administrator',
+ botPermissions: ['KickMembers', 'BanMembers'],
+ guilds: ['1234567890', '1234567891'],
+ aliases: ['p', 'pong'],
+ };
+};
+```
diff --git a/apps/website/docs/guide/02-commands/05-after-function.mdx b/apps/website/docs/guide/02-commands/06-after-function.mdx
similarity index 50%
rename from apps/website/docs/guide/02-commands/05-after-function.mdx
rename to apps/website/docs/guide/02-commands/06-after-function.mdx
index 71571101..210a872d 100644
--- a/apps/website/docs/guide/02-commands/05-after-function.mdx
+++ b/apps/website/docs/guide/02-commands/06-after-function.mdx
@@ -2,6 +2,13 @@
title: after Function
---
+:::warning UNSTABLE
+
+The `after()` function is currently marked as unstable and may change
+in the future.
+
+:::
+
The `after()` function allows you to execute a callback after a
command has been executed. This is useful for performing actions that
should occur after the command has completed, such as logging or
@@ -10,13 +17,18 @@ cleanup tasks.
## Usage
```ts title="src/app/commands/ping.ts"
-import { type CommandData, type ChatInputCommand, after } from 'commandkit';
+import {
+ type CommandData,
+ type ChatInputCommand,
+ unstable_after as after,
+} from 'commandkit';
export const command: CommandData = {};
export const chatInput: ChatInputCommand = async (ctx) => {
after(() => {
// This code will be executed after the command has been executed
+ // Perform any cleanup here
console.log('Command has been executed');
});
@@ -33,3 +45,25 @@ The `after()` function will always be called, regardless of whether
the command execution was successful or not.
:::
+
+## Cancelling the after function
+
+You can cancel the after function by calling the `cancelAfter`
+function with the ID of the after function.
+
+```ts title="src/app/commands/ping.ts"
+import {
+ unstable_after as after,
+ unstable_cancelAfter as cancelAfter,
+} from 'commandkit';
+
+export const chatInput: ChatInputCommand = async (ctx) => {
+ const id = after(() => {
+ console.log('This will run after the command has finished executing.');
+ });
+
+ if (something) {
+ cancelAfter(id);
+ }
+};
+```
diff --git a/apps/website/docs/guide/02-commands/06-category-directory.mdx b/apps/website/docs/guide/02-commands/07-category-directory.mdx
similarity index 97%
rename from apps/website/docs/guide/02-commands/06-category-directory.mdx
rename to apps/website/docs/guide/02-commands/07-category-directory.mdx
index e727384c..587f84ed 100644
--- a/apps/website/docs/guide/02-commands/06-category-directory.mdx
+++ b/apps/website/docs/guide/02-commands/07-category-directory.mdx
@@ -31,7 +31,7 @@ plugins, or even other commands through your `commandkit` instance.
│ │ │ ├── kick.ts
│ │ │ └── ban.ts
│ │ └── events/
-│ │ └── ready/
+│ │ └── clientReady/
│ │ └── log.ts
│ └── app.ts
├── .env
diff --git a/apps/website/docs/guide/02-commands/07-middlewares.mdx b/apps/website/docs/guide/02-commands/08-middlewares.mdx
similarity index 70%
rename from apps/website/docs/guide/02-commands/07-middlewares.mdx
rename to apps/website/docs/guide/02-commands/08-middlewares.mdx
index d1466807..c67bbe3a 100644
--- a/apps/website/docs/guide/02-commands/07-middlewares.mdx
+++ b/apps/website/docs/guide/02-commands/08-middlewares.mdx
@@ -34,8 +34,8 @@ export function afterExecute(ctx: MiddlewareContext) {
## Stop command execution
-You can stop a command from running by returning `ctx.cancel()` in the
-`beforeExecute` function.
+You can stop a command from running by calling `stopMiddlewares()` in
+the `beforeExecute` function.
```ts title="src/app/commands/+middleware.ts"
import type { MiddlewareContext } from 'commandkit';
@@ -44,7 +44,7 @@ export function beforeExecute(ctx: MiddlewareContext) {
if (ctx.interaction.user.id !== '1234567890') {
// Conditionally stop command execution
console.log(`${ctx.commandName} will not be executed!`);
- return ctx.cancel();
+ stopMiddlewares();
}
// Continue with command execution
@@ -52,6 +52,49 @@ export function beforeExecute(ctx: MiddlewareContext) {
}
```
+:::tip
+
+You can also use `stopMiddlewares()` inside a command function to stop
+any `afterExecute` middlewares from running.
+
+In addition, you can also use `stopMiddlewares()` inside any
+`afterExecute` middleware function to stop any remaining middlewares
+from running.
+
+:::
+
+:::warning
+
+Calling `stopMiddlewares()` in a try/catch block may lead to
+unexpected behavior.
+
+If you still want to use `stopMiddlewares()` in a try/catch block, you
+can use the `isErrorType` function to check if the error is an
+instance of the `CommandKitErrorCodes.StopMiddlewares` error.
+
+```ts title="src/app/commands/+middleware.ts"
+import type { MiddlewareContext } from 'commandkit';
+
+export function beforeExecute(ctx: MiddlewareContext) {
+ try {
+ // code that may throw an error
+
+ stopMiddlewares(); // conditionally stop the middleware chain
+ } catch (error) {
+ if (isErrorType(error, CommandKitErrorCodes.StopMiddlewares)) {
+ // if stopMiddlewares() is called in the try block, throw it so CommandKit can stop the middleware chain
+ throw error;
+ }
+
+ // this means that the code threw the error, and stopMiddlewares() was not called
+ // the rest of the middlewares will be executed as normal
+ console.error(error);
+ }
+}
+```
+
+:::
+
## Middleware types
### Directory-scoped middleware
diff --git a/apps/website/docs/guide/03-events/01-discordjs-events.mdx b/apps/website/docs/guide/03-events/01-discordjs-events.mdx
index d6c974da..5fa1f86b 100644
--- a/apps/website/docs/guide/03-events/01-discordjs-events.mdx
+++ b/apps/website/docs/guide/03-events/01-discordjs-events.mdx
@@ -19,7 +19,7 @@ Inside each event directory, you can create multiple handler files
that will all execute when that event is triggered.
Here's how your project structure might look with the
-`"messageCreate"` and `"ready"` events:
+`"messageCreate"` and `"clientReady"` events:
```title="" {4-9}
.
@@ -29,7 +29,7 @@ Here's how your project structure might look with the
│ │ ├── messageCreate/
│ │ │ ├── give-xp.ts
│ │ │ └── log-message.ts
-│ │ └── ready/
+│ │ └── clientReady/
│ │ └── log.ts
│ └── app.ts
├── .env
@@ -50,12 +50,12 @@ You can see what events are available in Discord.js by checking the
To create an event handler, create a folder inside the
`src/app/events` directory which should match the event name from
-Discord.js. This example will use the `"ready"` event.
+Discord.js. This example will use the `"clientReady"` event.
-```ts title="src/app/events/ready/log.ts"
+```ts title="src/app/events/clientReady/log.ts"
import type { EventHandler } from 'commandkit';
-const handler: EventHandler<'ready'> = (client) => {
+const handler: EventHandler<'clientReady'> = (client) => {
console.log(`🤖 ${client.user.displayName} is online!`);
};
@@ -63,11 +63,12 @@ export default handler;
```
That's it! CommandKit will automatically detect this file and register
-it as one of the handler functions for the `"ready"` event.
+it as one of the handler functions for the `"clientReady"` event.
Just like the Discord.js `client.on()` method, the parameters you
receive in your event file will be based on what event you're trying
-to handle. In the example above, the `"ready"` event should give you a
+to handle. In the example above, the `"clientReady"` event should give
+you a
[`Client`](https://discord.js.org/docs/packages/discord.js/main/Client:Class)
instance.
@@ -86,12 +87,12 @@ definition.
You may want to have some events to only get called once. For this,
you can export a variable called `once` from your event function file.
-```ts title="src/app/events/ready/log.ts"
+```ts title="src/app/events/clientReady/log.ts"
import type { EventHandler } from 'commandkit';
export const once = true;
-const handler: EventHandler<'ready'> = (client) => {
+const handler: EventHandler<'clientReady'> = (client) => {
console.log(`🤖 ${client.user.displayName} is online!`);
};
diff --git a/apps/website/docs/guide/05-official-plugins/01-commandkit-ai.mdx b/apps/website/docs/guide/05-official-plugins/01-commandkit-ai.mdx
index 90e6a814..02f7ac17 100644
--- a/apps/website/docs/guide/05-official-plugins/01-commandkit-ai.mdx
+++ b/apps/website/docs/guide/05-official-plugins/01-commandkit-ai.mdx
@@ -124,7 +124,7 @@ export const command: CommandData = {
};
export const aiConfig: AiConfig = {
- parameters: z.object({
+ inputSchema: z.object({
username: z.string().describe('The username to greet'),
message: z.string().optional().describe('Optional custom greeting message'),
}),
@@ -282,7 +282,7 @@ import { z } from 'zod';
export const getWeather = createTool({
name: 'getWeather',
description: 'Get current weather information for a location',
- parameters: z.object({
+ inputSchema: z.object({
location: z.string().describe('The city or location to get weather for'),
units: z.enum(['celsius', 'fahrenheit']).default('celsius'),
}),
diff --git a/apps/website/docs/guide/08-advanced/01-setup-commandkit-manually.mdx b/apps/website/docs/guide/08-advanced/01-setup-commandkit-manually.mdx
index 5ffa32e6..2a86920b 100644
--- a/apps/website/docs/guide/08-advanced/01-setup-commandkit-manually.mdx
+++ b/apps/website/docs/guide/08-advanced/01-setup-commandkit-manually.mdx
@@ -108,16 +108,16 @@ Learn more
To register and handle events emitted by your discord.js client,
create a folder inside the `src/app` directory called `events` and
create a folder with the name of the discord.js event you'd like to
-handle (e.g. ready, messageCreate, etc). This example will use the
-`ready` event.
+handle (e.g. clientReady, messageCreate, etc). This example will use
+the `clientReady` event.
-In the `src/app/events/ready` directory, you can create files which
-will export default functions that will be called when the respective
-event is emitted by discord.js. Following the `ready` event example
-mentioned above, you may want to log when your bot comes online. The
-function for that will look like so:
+In the `src/app/events/clientReady` directory, you can create files
+which will export default functions that will be called when the
+respective event is emitted by discord.js. Following the `clientReady`
+event example mentioned above, you may want to log when your bot comes
+online. The function for that will look like so:
-```ts title="src/app/events/log.ts"
+```ts title="src/app/events/clientReady/log.ts"
import type { Client } from 'discord.js';
export default function (client: Client) {
diff --git a/apps/website/docs/guide/08-advanced/04-migrating-from-v0.mdx b/apps/website/docs/guide/08-advanced/04-migrating-from-v0.mdx
index 425f9980..a99bb0d2 100644
--- a/apps/website/docs/guide/08-advanced/04-migrating-from-v0.mdx
+++ b/apps/website/docs/guide/08-advanced/04-migrating-from-v0.mdx
@@ -58,7 +58,7 @@ enables advanced features like automatic route discovery.
│ │ ├── commands/
│ │ │ └── ping.ts
│ │ └── events/
- │ │ └── ready/
+ │ │ └── clientReady/
│ │ └── log.ts
│ └── app.ts
├── .env
@@ -75,7 +75,7 @@ enables advanced features like automatic route discovery.
│ ├── commands/
│ │ └── ping.ts
│ ├── events/
- │ │ └── ready/
+ │ │ └── clientReady/
│ │ └── log.ts
│ └── index.ts
├── .env
@@ -241,7 +241,7 @@ control over command execution.
```ts title='src/app/commands/+global-middleware.ts'
- import type { MiddlewareContext } from 'commandkit';
+ import type { MiddlewareContext, stopMiddlewares } from 'commandkit';
export function beforeExecute(ctx: MiddlewareContext) {
// Example: Block command execution based on conditions
@@ -249,7 +249,7 @@ control over command execution.
ctx.interaction.reply('Access denied: Command execution blocked.');
}
- ctx.cancel(); // Prevents command execution
+ stopMiddlewares(); // Prevents command execution
}
```
diff --git a/apps/website/versioned_docs/version-0.1.10/guide/05-event-file-setup.mdx b/apps/website/versioned_docs/version-0.1.10/guide/05-event-file-setup.mdx
index de584991..1cd8761b 100644
--- a/apps/website/versioned_docs/version-0.1.10/guide/05-event-file-setup.mdx
+++ b/apps/website/versioned_docs/version-0.1.10/guide/05-event-file-setup.mdx
@@ -8,25 +8,25 @@ import TabItem from '@theme/TabItem';
# Events Setup
-This is a simple overview of how to set up a simple function that is called when the "ready" event is triggered. All this code does is log to the console when the bot is ready.
+This is a simple overview of how to set up a simple function that is called when the "clientReady" event is triggered. All this code does is log to the console when the bot is ready.
- ```js title="src/events/ready/console-log.js"
+ ```js title="src/events/clientReady/console-log.js"
module.exports = (c, client, handler) => {
console.log(`${c.user.username} is ready!`);
};
```
- ```js title="src/events/ready/console-log.js"
+ ```js title="src/events/clientReady/console-log.js"
export default function (c, client, handler) {
console.log(`${c.user.username} is ready!`);
};
```
- ```ts title="src/events/ready/console-log.ts"
+ ```ts title="src/events/clientReady/console-log.ts"
import type { Client } from 'discord.js';
import type { CommandKit } from 'commandkit';
@@ -41,7 +41,7 @@ This is a simple overview of how to set up a simple function that is called when
## Parameters explained
-The parameters might look a bit confusing at first, but they're actually quite simple. The first parameter `c` is the client object that was returned as a parameter when the "ready" event was triggered. The second parameter `client` is the Discord.js client that was instantiated in your main entry point file. Finally, the `handler` parameter is the current CommandKit instance.
+The parameters might look a bit confusing at first, but they're actually quite simple. The first parameter `c` is the client object that was returned as a parameter when the "clientReady" event was triggered. The second parameter `client` is the Discord.js client that was instantiated in your main entry point file. Finally, the `handler` parameter is the current CommandKit instance.
To better understand how the parameters work, here's another example but with the "messageCreate" event listener.
diff --git a/packages/ai/src/configure.ts b/packages/ai/src/configure.ts
index 4673d81b..533b1b57 100644
--- a/packages/ai/src/configure.ts
+++ b/packages/ai/src/configure.ts
@@ -168,7 +168,7 @@ const AIConfig: Required = {
}
},
onError: async (_ctx, message, error) => {
- Logger.error(`Error processing AI message: ${error}`);
+ Logger.error`Error processing AI message: ${error}`;
const channel = message.channel as TextChannel;
if (channel.isSendable()) {
@@ -177,7 +177,7 @@ const AIConfig: Required = {
content: 'An error occurred while processing your request.',
allowedMentions: { parse: [] },
})
- .catch((e) => Logger.error(`Failed to send error message: ${e}`));
+ .catch((e) => Logger.error`Failed to send error message: ${e}`);
}
},
};
diff --git a/packages/ai/src/tools/common/index.ts b/packages/ai/src/tools/common/index.ts
index e54108be..80b31db7 100644
--- a/packages/ai/src/tools/common/index.ts
+++ b/packages/ai/src/tools/common/index.ts
@@ -2,6 +2,7 @@ import { Schema, tool } from 'ai';
import { AiContext } from '../../context';
import { getAiWorkerContext } from '../../ai-context-worker';
import { z } from 'zod';
+import { z as z3 } from 'zod/v3';
/**
* Utility type that represents a value that can be either synchronous or asynchronous.
@@ -14,7 +15,7 @@ type Awaitable = T | Promise;
* Type representing the parameters schema for AI tools.
* Extracted from the first parameter of the `tool` function from the 'ai' library.
*/
-export type ToolParameterType = z.ZodType | Schema;
+export type ToolParameterType = z.ZodType | z3.ZodType | Schema;
/**
* Utility type that infers the TypeScript type from a tool parameter schema.
@@ -26,8 +27,9 @@ export type InferParameters =
? T['_type']
: T extends z.ZodTypeAny
? z.infer
- : never;
-
+ : T extends z3.ZodTypeAny
+ ? z3.infer
+ : never;
/**
* Configuration options for creating an AI tool.
* @template T - The parameter schema type for the tool
diff --git a/packages/cache/src/use-cache-directive.ts b/packages/cache/src/use-cache-directive.ts
index c0910148..328795ca 100644
--- a/packages/cache/src/use-cache-directive.ts
+++ b/packages/cache/src/use-cache-directive.ts
@@ -2,7 +2,6 @@ import {
CommonDirectiveTransformer,
CommonDirectiveTransformerOptions,
CompilerPluginRuntime,
- Logger,
} from 'commandkit';
/**
@@ -26,6 +25,5 @@ export class UseCacheDirectivePlugin extends CommonDirectiveTransformer {
public async activate(ctx: CompilerPluginRuntime): Promise {
super.activate(ctx);
- Logger.info('"use cache" directive compiler plugin activated');
}
}
diff --git a/packages/commandkit/package.json b/packages/commandkit/package.json
index c113f02e..fb06a395 100644
--- a/packages/commandkit/package.json
+++ b/packages/commandkit/package.json
@@ -54,75 +54,85 @@
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
},
- "./jsx-runtime": {
- "require": "./jsx-runtime.cjs",
- "import": "./jsx-runtime.cjs",
- "types": "./jsx-runtime.d.ts"
+ "./ai": {
+ "require": "./ai.cjs",
+ "import": "./ai.cjs",
+ "types": "./ai.d.ts"
},
- "./hooks": {
- "require": "./hooks.cjs",
- "import": "./hooks.cjs",
- "types": "./hooks.d.ts"
+ "./analytics": {
+ "require": "./analytics.cjs",
+ "import": "./analytics.cjs",
+ "types": "./analytics.d.ts"
},
- "./plugin": {
- "require": "./plugin.cjs",
- "import": "./plugin.cjs",
- "types": "./plugin.d.ts"
+ "./async-queue": {
+ "require": "./async-queue.cjs",
+ "import": "./async-queue.cjs",
+ "types": "./async-queue.d.ts"
},
- "./config": {
- "require": "./config.cjs",
- "import": "./config.cjs",
- "types": "./config.d.ts"
+ "./cache": {
+ "require": "./cache.cjs",
+ "import": "./cache.cjs",
+ "types": "./cache.d.ts"
},
"./components": {
"require": "./components.cjs",
"import": "./components.cjs",
"types": "./components.d.ts"
},
+ "./config": {
+ "require": "./config.cjs",
+ "import": "./config.cjs",
+ "types": "./config.d.ts"
+ },
+ "./env": {
+ "require": "./env.cjs",
+ "import": "./env.cjs",
+ "types": "./env.d.ts"
+ },
+ "./events": {
+ "require": "./events.cjs",
+ "import": "./events.cjs",
+ "types": "./events.d.ts"
+ },
"./flag": {
"require": "./flag.cjs",
"import": "./flag.cjs",
"types": "./flag.d.ts"
},
- "./cache": {
- "require": "./cache.cjs",
- "import": "./cache.cjs",
- "types": "./cache.d.ts"
+ "./hooks": {
+ "require": "./hooks.cjs",
+ "import": "./hooks.cjs",
+ "types": "./hooks.d.ts"
},
"./i18n": {
"require": "./i18n.cjs",
"import": "./i18n.cjs",
"types": "./i18n.d.ts"
},
+ "./jsx-runtime": {
+ "require": "./jsx-runtime.cjs",
+ "import": "./jsx-runtime.cjs",
+ "types": "./jsx-runtime.d.ts"
+ },
+ "./kv": {
+ "require": "./kv.cjs",
+ "import": "./kv.cjs",
+ "types": "./kv.d.ts"
+ },
"./logger": {
"require": "./logger.cjs",
"import": "./logger.cjs",
"types": "./logger.d.ts"
},
- "./events": {
- "require": "./events.cjs",
- "import": "./events.cjs",
- "types": "./events.d.ts"
- },
- "./analytics": {
- "require": "./analytics.cjs",
- "import": "./analytics.cjs",
- "types": "./analytics.d.ts"
- },
- "./ai": {
- "require": "./ai.cjs",
- "import": "./ai.cjs",
- "types": "./ai.d.ts"
- },
- "./env": {
- "require": "./env.cjs",
- "import": "./env.cjs",
- "types": "./env.d.ts"
+ "./mutex": {
+ "require": "./mutex.cjs",
+ "import": "./mutex.cjs",
+ "types": "./mutex.d.ts"
},
- "./async-queue": {
- "require": "./async-queue.cjs",
- "import": "./async-queue.cjs",
- "types": "./async-queue.d.ts"
+ "./plugin": {
+ "require": "./plugin.cjs",
+ "import": "./plugin.cjs",
+ "types": "./plugin.d.ts"
},
"./ratelimit": {
"require": "./ratelimit.cjs",
@@ -133,21 +143,10 @@
"require": "./semaphore.cjs",
"import": "./semaphore.cjs",
"types": "./semaphore.d.ts"
- },
- "./mutex": {
- "require": "./mutex.cjs",
- "import": "./mutex.cjs",
- "types": "./mutex.d.ts"
- },
- "./kv": {
- "require": "./kv.cjs",
- "import": "./kv.cjs",
- "types": "./kv.d.ts"
}
},
"scripts": {
"check-types": "tsc --noEmit",
- "dev": "pnpm run --filter=test-bot dev",
"build": "tsdown",
"test": "vitest"
},
@@ -160,7 +159,9 @@
"keywords": [
"discord.js",
"command handler",
- "event handler"
+ "event handler",
+ "framework",
+ "discord.js 14"
],
"dependencies": {
"@rollup/plugin-json": "^6.1.0",
@@ -173,7 +174,7 @@
"picocolors": "^1.1.1",
"rfdc": "^1.3.1",
"rimraf": "^6.0.0",
- "tsdown": "^0.14.0",
+ "tsdown": "^0.14.2",
"use-macro": "^1.1.0"
},
"devDependencies": {
diff --git a/packages/commandkit/src/app/commands/AppCommandRunner.ts b/packages/commandkit/src/app/commands/AppCommandRunner.ts
index 784566f7..4490ae04 100644
--- a/packages/commandkit/src/app/commands/AppCommandRunner.ts
+++ b/packages/commandkit/src/app/commands/AppCommandRunner.ts
@@ -1,22 +1,22 @@
import { ChatInputCommandInteraction, Interaction, Message } from 'discord.js';
+import { AnalyticsEvents } from '../../analytics/constants';
+import {
+ makeContextAwareFunction,
+ provideContext,
+ useEnvironment,
+} from '../../context/async-context';
import {
CommandKitEnvironment,
CommandKitEnvironmentType,
} from '../../context/environment';
import { Logger } from '../../logger/Logger';
+import { CommandKitErrorCodes, isErrorType } from '../../utils/error-codes';
import {
AppCommandHandler,
PreparedAppCommandExecution,
RunCommand,
} from '../handlers/AppCommandHandler';
import { CommandExecutionMode, MiddlewareContext } from './Context';
-import {
- makeContextAwareFunction,
- provideContext,
- useEnvironment,
-} from '../../context/async-context';
-import { CommandKitErrorCodes, isErrorType } from '../../utils/error-codes';
-import { AnalyticsEvents } from '../../analytics/constants';
/**
* Options for running a command in CommandKit.
@@ -53,6 +53,7 @@ export class AppCommandRunner {
* Handles the complete command lifecycle including before/after middleware execution.
* @param prepared - The prepared command execution data
* @param source - The source interaction or message that triggered the command
+ * @param options - The options for running the command
*/
public async runCommand(
prepared: PreparedAppCommandExecution,
@@ -74,82 +75,111 @@ export class AppCommandRunner {
env.variables.set('execHandlerKind', executionMode);
env.variables.set('customHandler', options?.handler ?? null);
- const ctx = new MiddlewareContext(commandkit, {
- command: prepared.command,
- environment: env,
- executionMode,
- interaction: !(source instanceof Message)
- ? (source as ChatInputCommandInteraction)
- : (null as never),
- message: source instanceof Message ? source : (null as never),
- forwarded: false,
- customArgs: {
- setCommandRunner: (fn: RunCommand) => {
- runCommand = fn;
+ try {
+ const middlewareCtx = new MiddlewareContext(commandkit, {
+ command: prepared.command,
+ environment: env,
+ executionMode,
+ interaction: !(source instanceof Message)
+ ? (source as ChatInputCommandInteraction)
+ : (null as never),
+ message: source instanceof Message ? source : (null as never),
+ forwarded: false,
+ customArgs: {
+ setCommandRunner: (fn: RunCommand) => {
+ runCommand = fn;
+ },
},
- },
- messageCommandParser: prepared.messageCommandParser,
- });
+ messageCommandParser: prepared.messageCommandParser,
+ });
- let middlewaresCanceled = false;
+ const beforeMiddlewares = prepared.middlewares.filter(
+ (m) => m.data.beforeExecute,
+ );
+
+ let beforeMiddlewaresStopped = false;
+
+ // Run middleware before command execution
+ if (beforeMiddlewares.length) {
+ await provideContext(env, async () => {
+ for (const middleware of beforeMiddlewares) {
+ try {
+ await middleware.data.beforeExecute(middlewareCtx);
+ } catch (e) {
+ if (isErrorType(e, CommandKitErrorCodes.StopMiddlewares)) {
+ beforeMiddlewaresStopped = true;
+ Logger.debug(
+ `Middleware propagation stopped for command "${middlewareCtx.commandName}". stopMiddlewares() was called inside a beforeExecute function at "${middleware.middleware.relativePath}"`,
+ );
+ break; // Stop the middleware loop if `stopMiddlewares()` is called.
+ }
+
+ if (
+ isErrorType(e, [
+ CommandKitErrorCodes.ForwardedCommand,
+ CommandKitErrorCodes.InvalidCommandPrefix,
+ ])
+ ) {
+ continue;
+ }
- // Run middleware before command execution
- if (prepared.middlewares.length) {
- await provideContext(env, async () => {
- for (const middleware of prepared.middlewares) {
- if (!middleware.data.beforeExecute) continue;
- try {
- await middleware.data.beforeExecute(ctx);
- } catch (e) {
- if (isErrorType(e, CommandKitErrorCodes.ExitMiddleware)) {
- middlewaresCanceled = true;
- return;
+ throw e;
}
+ }
+ });
+ }
- if (
- isErrorType(e, [
- CommandKitErrorCodes.ForwardedCommand,
- CommandKitErrorCodes.InvalidCommandPrefix,
- ])
- ) {
- continue;
- }
+ let result: any;
- throw e;
- }
- }
- });
- }
+ let stopMiddlewaresCalledInCmd = false;
- let result: any;
+ // If no `stopMiddlewares()` was called in a `beforeExecute` middleware, try to run the command
+ if (!beforeMiddlewaresStopped) {
+ const targetData = prepared.command.data;
+ const fn = targetData[options?.handler || executionMode];
- if (!ctx.cancelled) {
- // Determine which function to run based on whether we're executing a command or subcommand
- const targetData = prepared.command.data;
- const fn = targetData[options?.handler || executionMode];
+ if (!fn) {
+ Logger.warn(
+ `Command ${prepared.command.command.name} has no handler for ${executionMode}`,
+ );
+ }
- if (!fn) {
- Logger.warn(
- `Command ${prepared.command.command.name} has no handler for ${executionMode}`,
- );
- }
+ const analytics = commandkit.analytics;
- const analytics = commandkit.analytics;
-
- if (fn) {
- try {
- const _executeCommand = makeContextAwareFunction(
- env,
- async () => {
- env.registerDeferredFunction(async (env) => {
- env.markEnd();
- const error = env.getExecutionError();
- const marker = env.getMarker();
- const time = `${env.getExecutionTime().toFixed(2)}ms`;
-
- if (error) {
- Logger.error(
- `[${marker} - ${time}] Error executing command: ${error.stack || error}`,
+ if (fn) {
+ try {
+ const _executeCommand = makeContextAwareFunction(
+ env,
+ async () => {
+ env.registerDeferredFunction(async (env) => {
+ env.markEnd();
+ const error = env.getExecutionError();
+ const marker = env.getMarker();
+ const time = `${env.getExecutionTime().toFixed(2)}ms`;
+
+ if (error) {
+ Logger.error`[${marker} - ${time}] Error executing command: ${error}`;
+
+ const commandName =
+ prepared.command?.data?.command?.name ??
+ prepared.command.command.name;
+
+ await analytics.track({
+ name: AnalyticsEvents.COMMAND_EXECUTION,
+ id: commandName,
+ data: {
+ error: true,
+ executionTime: env.getExecutionTime().toFixed(2),
+ type: executionMode,
+ command: commandName,
+ },
+ });
+
+ return;
+ }
+
+ Logger.info(
+ `[${marker} - ${time}] Command executed successfully`,
);
const commandName =
@@ -160,106 +190,95 @@ export class AppCommandRunner {
name: AnalyticsEvents.COMMAND_EXECUTION,
id: commandName,
data: {
- error: true,
+ error: false,
executionTime: env.getExecutionTime().toFixed(2),
type: executionMode,
command: commandName,
},
});
-
- return;
- }
-
- Logger.info(
- `[${marker} - ${time}] Command executed successfully`,
- );
-
- const commandName =
- prepared.command?.data?.command?.name ??
- prepared.command.command.name;
-
- await analytics.track({
- name: AnalyticsEvents.COMMAND_EXECUTION,
- id: commandName,
- data: {
- error: false,
- executionTime: env.getExecutionTime().toFixed(2),
- type: executionMode,
- command: commandName,
- },
});
- });
-
- return fn(ctx.clone());
- },
- this.#finalizer.bind(this),
- );
-
- const executeCommand =
- runCommand != null
- ? (runCommand as RunCommand)(_executeCommand)
- : _executeCommand;
-
- env.markStart(prepared.command.data.command.name);
- const res = await commandkit.plugins.execute(async (ctx, plugin) => {
- return plugin.executeCommand(
- ctx,
- env,
- source,
- prepared,
- executeCommand,
+ return fn(middlewareCtx.clone());
+ },
+ this.#finalizer.bind(this),
);
- });
- if (!res) {
- result = await executeCommand();
- }
- } catch (e) {
- if (isErrorType(e, CommandKitErrorCodes.ExitMiddleware)) {
- middlewaresCanceled = true;
- }
+ const executeCommand =
+ runCommand != null
+ ? (runCommand as RunCommand)(_executeCommand)
+ : _executeCommand;
+
+ env.markStart(prepared.command.data.command.name);
+
+ const res = await commandkit.plugins.execute(
+ async (ctx, plugin) => {
+ return plugin.executeCommand(
+ ctx,
+ env,
+ source,
+ prepared,
+ executeCommand,
+ );
+ },
+ );
- if (
- !isErrorType(e, [
- CommandKitErrorCodes.ForwardedCommand,
- CommandKitErrorCodes.ExitMiddleware,
- ])
- ) {
- if (shouldThrowOnError) {
- throw e;
+ if (!res) {
+ result = await executeCommand();
+ }
+ } catch (e) {
+ if (isErrorType(e, CommandKitErrorCodes.StopMiddlewares)) {
+ stopMiddlewaresCalledInCmd = true;
+ Logger.debug(
+ `Middleware propagation stopped for command "${middlewareCtx.commandName}". stopMiddlewares() was called by the command itself`,
+ );
+ } else if (!isErrorType(e, CommandKitErrorCodes.ForwardedCommand)) {
+ if (shouldThrowOnError) {
+ throw e;
+ }
+ Logger.error`${e}`;
}
-
- Logger.error(e);
}
}
+ } else {
+ result = {
+ error: true,
+ message:
+ 'Command execution was cancelled by a beforeExecute middleware.',
+ };
}
- } else {
- result = {
- error: true,
- message: 'Command execution was cancelled by the middleware.',
- };
- }
- // Run middleware after command execution
- if (!middlewaresCanceled && prepared.middlewares.length) {
- await provideContext(env, async () => {
- for (const middleware of prepared.middlewares) {
- if (!middleware.data.afterExecute) continue;
- try {
- await middleware.data.afterExecute(ctx);
- } catch (e) {
- if (isErrorType(e, CommandKitErrorCodes.ExitMiddleware)) {
- return;
+ const afterMiddlewares = prepared.middlewares.filter(
+ (m) => m.data.afterExecute,
+ );
+
+ // Run middleware after command execution only if `stopMiddlewares()` wasn't
+ // called in either `beforeExecute` middleware or in the command itself.
+ if (
+ !beforeMiddlewaresStopped &&
+ !stopMiddlewaresCalledInCmd &&
+ afterMiddlewares.length
+ ) {
+ await provideContext(env, async () => {
+ for (const middleware of afterMiddlewares) {
+ try {
+ await middleware.data.afterExecute(middlewareCtx);
+ } catch (e) {
+ if (isErrorType(e, CommandKitErrorCodes.StopMiddlewares)) {
+ Logger.debug(
+ `Middleware propagation stopped for command "${middlewareCtx.commandName}". stopMiddlewares() was called inside an afterExecute function at "${middleware.middleware.relativePath}"`,
+ );
+ break; // Stop the afterExecute middleware loop if `stopMiddlewares()` is called.
+ }
+ throw e;
}
-
- throw e;
}
- }
- });
- }
+ });
+ }
- return result;
+ return result;
+ } finally {
+ await this.#finalizer(env, false);
+ }
}
/**
@@ -267,17 +286,19 @@ export class AppCommandRunner {
* @internal
* Finalizes command execution by running deferred functions and plugin cleanup.
*/
- async #finalizer() {
- const env = useEnvironment();
+ async #finalizer(env?: CommandKitEnvironment, runPlugins = true) {
+ env ??= useEnvironment();
await env.runDeferredFunctions();
env.clearAllDeferredFunctions();
// plugins may have their own deferred function, useful for cleanup or post-command analytics
- await this.handler.commandkit.plugins.execute(async (ctx, plugin) => {
- await plugin.onAfterCommand(ctx, env);
- });
+ if (runPlugins) {
+ await this.handler.commandkit.plugins.execute(async (ctx, plugin) => {
+ await plugin.onAfterCommand(ctx, env);
+ });
+ }
}
/**
diff --git a/packages/commandkit/src/app/commands/Context.ts b/packages/commandkit/src/app/commands/Context.ts
index 6d75744a..df5b1be0 100644
--- a/packages/commandkit/src/app/commands/Context.ts
+++ b/packages/commandkit/src/app/commands/Context.ts
@@ -1,29 +1,29 @@
import {
AutocompleteInteraction,
+ Awaitable,
ChatInputCommandInteraction,
- MessageContextMenuCommandInteraction,
- Message,
- Locale,
- Interaction,
- UserContextMenuCommandInteraction,
Client,
- Awaitable,
Guild,
+ Interaction,
+ Locale,
+ Message,
+ MessageContextMenuCommandInteraction,
TextBasedChannel,
+ UserContextMenuCommandInteraction,
} from 'discord.js';
import { CommandKit } from '../../commandkit';
-import {
- MessageCommandOptions,
- MessageCommandParser,
-} from './MessageCommandParser';
-import { CommandKitEnvironment } from '../../context/environment';
import { GenericFunction, getContext } from '../../context/async-context';
-import { exitMiddleware, redirect } from '../interrupt/signals';
+import { CommandKitEnvironment } from '../../context/environment';
import {
LoadedCommand,
ResolvableCommand,
RunCommand,
} from '../handlers/AppCommandHandler';
+import { redirect } from '../interrupt/signals';
+import {
+ MessageCommandOptions,
+ MessageCommandParser,
+} from './MessageCommandParser';
/**
* Enumeration of different command execution modes supported by CommandKit.
@@ -551,15 +551,15 @@ export class Context<
return [];
}
- /**
- * Stops upcoming middleware or current command execution.
- * If this is called inside pre-stage middleware, the next run will be the actual command, skipping all other pre-stage middlewares.
- * If this is called inside a command itself, it will skip all post-stage middlewares.
- * If this is called inside post-stage middleware, it will skip all other post-stage middlewares.
- */
- public exit(): never {
- exitMiddleware();
- }
+ // /**
+ // * Stops upcoming middleware or current command execution.
+ // * If this is called inside pre-stage middleware, the next run will be the actual command, skipping all other pre-stage middlewares.
+ // * If this is called inside a command itself, it will skip all post-stage middlewares.
+ // * If this is called inside post-stage middleware, it will skip all other post-stage middlewares.
+ // */
+ // public exit(): never {
+ // stopMiddlewares();
+ // }
}
/**
@@ -568,26 +568,6 @@ export class Context<
export class MiddlewareContext<
T extends CommandExecutionMode = CommandExecutionMode,
> extends Context {
- /**
- * @private
- * @internal
- */
- #cancel = false;
-
- /**
- * Whether the command execution was cancelled.
- */
- public get cancelled(): boolean {
- return this.#cancel;
- }
-
- /**
- * Cancels the command execution.
- */
- public cancel(): void {
- this.#cancel = true;
- }
-
/**
* Sets command runner function to wrap the command execution.
* @param fn The function to set.
diff --git a/packages/commandkit/src/app/handlers/AppCommandHandler.ts b/packages/commandkit/src/app/handlers/AppCommandHandler.ts
index fed22235..bc63d713 100644
--- a/packages/commandkit/src/app/handlers/AppCommandHandler.ts
+++ b/packages/commandkit/src/app/handlers/AppCommandHandler.ts
@@ -462,9 +462,7 @@ export class AppCommandHandler {
COMMANDKIT_IS_DEV &&
this.commandkit.config.showUnknownPrefixCommandsWarning
) {
- Logger.error(
- `Prefix command "${command}" was not found.\nNote: This warning is only shown in development mode as an alert to help you find the command. If you wish to remove this warning, set \`showUnknownPrefixCommandsWarning\` to \`false\` in your commandkit config.`,
- );
+ Logger.error`Prefix command "${command}" was not found.\nNote: This warning is only shown in development mode as an alert to help you find the command. If you wish to remove this warning, set \`showUnknownPrefixCommandsWarning\` to \`false\` in your commandkit config.`;
}
return null;
}
@@ -502,7 +500,7 @@ export class AppCommandHandler {
if (isErrorType(e, CommandKitErrorCodes.InvalidCommandPrefix)) {
return null;
}
- Logger.error(e);
+ Logger.error`${e}`;
return null;
}
} else {
diff --git a/packages/commandkit/src/app/handlers/AppEventsHandler.ts b/packages/commandkit/src/app/handlers/AppEventsHandler.ts
index cc35a7d2..0f42fddd 100644
--- a/packages/commandkit/src/app/handlers/AppEventsHandler.ts
+++ b/packages/commandkit/src/app/handlers/AppEventsHandler.ts
@@ -105,9 +105,7 @@ export class AppEventsHandler {
const handler = await import(toFileURL(listener, true));
if (!handler.default || typeof handler.default !== 'function') {
- Logger.error(
- `Event handler for ${event.event}${event.namespace ? ` of namespace ${event.namespace}` : ''} does not have a default export or is not a function`,
- );
+ Logger.error`Event handler for ${event.event}${event.namespace ? ` of namespace ${event.namespace}` : ''} does not have a default export or is not a function`;
}
listeners.push({
diff --git a/packages/commandkit/src/app/interrupt/signals.ts b/packages/commandkit/src/app/interrupt/signals.ts
index f12200ab..eba195c5 100644
--- a/packages/commandkit/src/app/interrupt/signals.ts
+++ b/packages/commandkit/src/app/interrupt/signals.ts
@@ -6,13 +6,10 @@ import {
import { eventWorkerContext } from '../events/EventWorkerContext';
/**
- * Cancel upcoming middleware execution.
- * If this is called inside pre-stage middleware, the next run will be the actual command, skipping all other pre-stage middlewares.
- * If this is called inside a command itself, it will skip all post-stage middlewares.
- * If this is called inside post-stage middleware, it will skip all other post-stage middlewares.
+ * Stop upcoming middlewares and command execution.
*/
-export function exitMiddleware(): never {
- throw createCommandKitError(CommandKitErrorCodes.ExitMiddleware);
+export function stopMiddlewares(): never {
+ throw createCommandKitError(CommandKitErrorCodes.StopMiddlewares);
}
/**
diff --git a/packages/commandkit/src/app/middlewares/permissions.ts b/packages/commandkit/src/app/middlewares/permissions.ts
index 99e768e5..e7aac3f6 100644
--- a/packages/commandkit/src/app/middlewares/permissions.ts
+++ b/packages/commandkit/src/app/middlewares/permissions.ts
@@ -2,6 +2,7 @@ import { EmbedBuilder, MessageFlags } from 'discord.js';
import { getConfig } from '../../config/config';
import { Logger } from '../../logger/Logger';
import { MiddlewareContext } from '../commands/Context';
+import { stopMiddlewares } from '../interrupt/signals';
export const middlewareId = crypto.randomUUID();
@@ -46,7 +47,7 @@ export async function beforeExecute(ctx: MiddlewareContext) {
Logger.error`Could not send 'Server-only command' DM to user ${interaction?.user.id ?? message?.author.id} for command ${command.command.name}: ${error}`;
}
- return ctx.cancel(); // Stop the command from executing
+ stopMiddlewares(); // Stop the command from executing
}
const userPermissions =
@@ -153,5 +154,5 @@ export async function beforeExecute(ctx: MiddlewareContext) {
Logger.error`Could not send 'Not enough permissions' reply to user ${interaction?.user.id ?? message?.author.id} for command ${command.command.name}: ${error}`;
}
- return ctx.cancel(); // Stop the command from executing
+ stopMiddlewares(); // Stop the command from executing
}
diff --git a/packages/commandkit/src/cli/build.ts b/packages/commandkit/src/cli/build.ts
index ac61b774..eeffad9f 100644
--- a/packages/commandkit/src/cli/build.ts
+++ b/packages/commandkit/src/cli/build.ts
@@ -1,16 +1,17 @@
-import { build, Options } from 'tsdown';
-import { CompilerPlugin, CompilerPluginRuntime } from '../plugins';
-import { loadConfigFile } from '../config/loader';
+import { existsSync } from 'node:fs';
import { writeFile } from 'node:fs/promises';
import { join } from 'node:path';
-import { DevEnv, devEnvFileArgs, ProdEnv, prodEnvFileArgs } from './env';
import { rimraf } from 'rimraf';
-import { performTypeCheck } from './type-checker';
-import { copyLocaleFiles } from './common';
+import { build, Options } from 'tsdown';
+
import { MaybeArray } from '../components';
-import { COMMANDKIT_CWD } from '../utils/constants';
+import { loadConfigFile } from '../config/loader';
import { mergeDeep } from '../config/utils';
-import { existsSync } from 'node:fs';
+import { CompilerPlugin, CompilerPluginRuntime } from '../plugins';
+import { COMMANDKIT_CWD } from '../utils/constants';
+import { copyLocaleFiles } from './common';
+import { devEnvFileArgs, prodEnvFileArgs } from './env';
+import { performTypeCheck } from './type-checker';
/**
* @private
diff --git a/packages/commandkit/src/cli/development.ts b/packages/commandkit/src/cli/development.ts
index c273f658..1c27060e 100644
--- a/packages/commandkit/src/cli/development.ts
+++ b/packages/commandkit/src/cli/development.ts
@@ -164,7 +164,7 @@ export async function bootstrapDevelopmentServer(configPath?: string) {
}
return false;
- }, 300);
+ }, 700);
const isConfigUpdate = (path: string) => {
const isConfig = configPaths.some((configPath) => path === configPath);
diff --git a/packages/commandkit/src/commandkit.ts b/packages/commandkit/src/commandkit.ts
index 1d4de3b5..a4a699ff 100644
--- a/packages/commandkit/src/commandkit.ts
+++ b/packages/commandkit/src/commandkit.ts
@@ -254,7 +254,7 @@ export class CommandKit extends EventEmitter {
} catch (e) {
// ignore
if (process.env.COMMANDKIT_DEBUG_TYPEGEN) {
- Logger.error(e);
+ Logger.error`${e}`;
}
}
}
diff --git a/packages/commandkit/src/context/async-context.ts b/packages/commandkit/src/context/async-context.ts
index 3d8d113f..a7cc915a 100644
--- a/packages/commandkit/src/context/async-context.ts
+++ b/packages/commandkit/src/context/async-context.ts
@@ -70,6 +70,9 @@ export function makeContextAwareFunction<
} catch (e) {
if (!isCommandKitError(e)) {
env.setExecutionError(e as Error);
+ } else {
+ // rethrow commandkit errors so they can be handled by the caller
+ throw e;
}
} finally {
if (typeof finalizer === 'function') {
diff --git a/packages/commandkit/src/flags/feature-flags.ts b/packages/commandkit/src/flags/feature-flags.ts
index 3291fee5..a94f1f13 100644
--- a/packages/commandkit/src/flags/feature-flags.ts
+++ b/packages/commandkit/src/flags/feature-flags.ts
@@ -352,9 +352,7 @@ export class FeatureFlag {
}
}
} catch (error) {
- Logger.error(
- `Error fetching flag provider configuration for "${this.options.key}": ${error}`,
- );
+ Logger.error`Error fetching flag provider configuration for "${this.options.key}": ${error}`;
// continue with local decision if provider fails
}
}
diff --git a/packages/commandkit/src/index.ts b/packages/commandkit/src/index.ts
index b7165166..2a672f8d 100644
--- a/packages/commandkit/src/index.ts
+++ b/packages/commandkit/src/index.ts
@@ -6,7 +6,13 @@ export * from './commandkit';
export * from './components';
export * from './config/config';
export * from './context/async-context';
-export * from './context/environment';
+export {
+ type CommandKitEnvironmentInternalData,
+ CommandKitEnvironment,
+ CommandKitEnvironmentType,
+ cancelAfter as unstable_cancelAfter,
+ after as unstable_after,
+} from './context/environment';
export * from './app/index';
export * from './logger/DefaultLogger';
export * from './logger/ILogger';
diff --git a/packages/commandkit/src/logger/DefaultLogger.ts b/packages/commandkit/src/logger/DefaultLogger.ts
index c791573d..8ce74a74 100644
--- a/packages/commandkit/src/logger/DefaultLogger.ts
+++ b/packages/commandkit/src/logger/DefaultLogger.ts
@@ -139,15 +139,20 @@ export class DefaultLogger implements ILogger {
const context = this._getContext();
const colorFn = TextColorMap[level];
+ let processedMessage = message;
+ if (message instanceof Error) {
+ processedMessage = `${message.message}\n${message.stack}`;
+ }
+
if (context) {
this.logger.log(
`${prefix}\n${context} ${colors.dim(BoxChars.corner)}`,
- colorFn(message),
+ colorFn(processedMessage),
);
} else {
this.logger.log(
`${prefix} ${colors.dim(BoxChars.corner)}`,
- colorFn(message),
+ colorFn(processedMessage),
);
}
}
@@ -165,10 +170,14 @@ export class DefaultLogger implements ILogger {
for (let i = 0; i < strings.length; i++) {
result += strings[i];
if (i < values.length) {
- result += inspect(values[i], {
- colors: COMMANDKIT_IS_DEV,
- depth: 2,
- });
+ const value = values[i];
+ if (value instanceof Error) {
+ result += `${value.message}\n${value.stack}`;
+ } else if (value !== null && typeof value === 'object') {
+ result += inspect(value, { colors: true, depth: 2 });
+ } else {
+ result += value;
+ }
}
}
diff --git a/packages/commandkit/src/plugins/plugin-runtime/CompilerPluginRuntime.ts b/packages/commandkit/src/plugins/plugin-runtime/CompilerPluginRuntime.ts
index a24a34af..b6a25b53 100644
--- a/packages/commandkit/src/plugins/plugin-runtime/CompilerPluginRuntime.ts
+++ b/packages/commandkit/src/plugins/plugin-runtime/CompilerPluginRuntime.ts
@@ -209,9 +209,7 @@ export class CompilerPluginRuntime {
},
);
} catch (e: any) {
- console.error(
- `Plugin ${plugin.name} failed to activate with ${e?.stack || e}`,
- );
+ Logger.error`Plugin ${plugin.name} failed to activate with ${e}`;
}
}
@@ -237,9 +235,7 @@ export class CompilerPluginRuntime {
},
);
} catch (e: any) {
- console.error(
- `Plugin ${plugin.name} failed to deactivate with ${e?.stack || e}`,
- );
+ Logger.error`Plugin ${plugin.name} failed to deactivate with ${e}`;
}
}
diff --git a/packages/commandkit/src/utils/error-codes.ts b/packages/commandkit/src/utils/error-codes.ts
index 1c823273..a3ffcaff 100644
--- a/packages/commandkit/src/utils/error-codes.ts
+++ b/packages/commandkit/src/utils/error-codes.ts
@@ -5,7 +5,7 @@ export const CommandKitErrorCodes = {
/**
* Error code for exiting middleware.
*/
- ExitMiddleware: Symbol('kExitMiddleware'),
+ StopMiddlewares: Symbol('kStopMiddlewares'),
/**
* Error code for forwarded commands.
*/
diff --git a/packages/create-commandkit/templates/JavaScript/src/app/events/clientReady/log.js b/packages/create-commandkit/templates/JavaScript/src/app/events/clientReady/log.js
index 44f2cf81..abb2fe51 100644
--- a/packages/create-commandkit/templates/JavaScript/src/app/events/clientReady/log.js
+++ b/packages/create-commandkit/templates/JavaScript/src/app/events/clientReady/log.js
@@ -1,7 +1,7 @@
import { Logger } from 'commandkit/logger';
/**
- * @type {import('commandkit').EventHandler<'ready'>}
+ * @type {import('commandkit').EventHandler<'clientReady'>}
*/
const handler = async (client) => {
Logger.info(`Logged in as ${client.user.username}!`);
diff --git a/packages/create-commandkit/templates/TypeScript/src/app/events/clientReady/log.ts b/packages/create-commandkit/templates/TypeScript/src/app/events/clientReady/log.ts
index cde182ab..e1ff76c0 100644
--- a/packages/create-commandkit/templates/TypeScript/src/app/events/clientReady/log.ts
+++ b/packages/create-commandkit/templates/TypeScript/src/app/events/clientReady/log.ts
@@ -1,7 +1,7 @@
import type { EventHandler } from 'commandkit';
import { Logger } from 'commandkit/logger';
-const handler: EventHandler<'ready'> = async (client) => {
+const handler: EventHandler<'clientReady'> = async (client) => {
Logger.info(`Logged in as ${client.user.username}!`);
};
diff --git a/packages/legacy/src/plugin.ts b/packages/legacy/src/plugin.ts
index 1ec13e00..b92ecdbb 100644
--- a/packages/legacy/src/plugin.ts
+++ b/packages/legacy/src/plugin.ts
@@ -13,6 +13,7 @@ import {
HMREventType,
getSourceDirectories,
CommandKitEventDispatch,
+ stopMiddlewares,
} from 'commandkit';
import { join, resolve } from 'node:path';
import { loadLegacyValidations } from './loadLegacyValidations.js';
@@ -254,7 +255,7 @@ export class LegacyHandlerPlugin extends RuntimePlugin