Skip to content

fix: command handler bugs #274

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/test-bot/src/app/commands/(developer)/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const command: CommandData = {
name: 'server',
description: 'server command',
guilds: [process.env.DEV_GUILD_ID!],
aliases: ['s', 'serv'],
};

export const chatInput: ChatInputCommand = async (ctx) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import {
ChatInputCommand,
CommandData,
Container,
MessageCommand,
TextDisplay,
} from 'commandkit';
import { Colors, MessageFlags } from 'discord.js';

export const command: CommandData = {
name: 'components-test',
description: 'Test components v2 again',
aliases: ['ct'],
};

export const chatInput: ChatInputCommand = async (ctx) => {
Expand All @@ -20,6 +22,19 @@ export const chatInput: ChatInputCommand = async (ctx) => {

await ctx.interaction.reply({
components: [container],
flags: MessageFlags.IsComponentsV2,
});
};

export const message: MessageCommand = async (ctx) => {
const container = (
<Container accentColor={Colors.Fuchsia}>
<TextDisplay content="# CommandKit Components v2 test" />
</Container>
);

await ctx.message.reply({
components: [container],

flags: MessageFlags.IsComponentsV2,
});
Expand Down
2 changes: 2 additions & 0 deletions packages/commandkit/logger.cjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
const { DefaultLogger } = require('./dist/logger/DefaultLogger.js');
const { Logger, createLogger } = require('./dist/logger/Logger.js');
const { NoopLogger } = require('./dist/logger/NoopLogger.js');

module.exports = {
DefaultLogger,
Logger,
createLogger,
NoopLogger,
useLogger(logger) {
Logger.configure({ provider: logger });
},
Expand Down
3 changes: 3 additions & 0 deletions packages/commandkit/logger.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
export * from './dist/logger/DefaultLogger';
export * from './dist/logger/Logger';
export * from './dist/logger/ILogger';
export * from './dist/logger/NoopLogger';

import type { ILogger } from './dist/logger/ILogger';

/**
* Use a custom logger provider.
Expand Down
16 changes: 13 additions & 3 deletions packages/commandkit/src/CommandKit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { Logger } from './logger/Logger';
import { AsyncFunction, GenericFunction } from './context/async-context';
import { FlagStore } from './flags/store';
import { AnalyticsEngine } from './analytics/analytics-engine';
import { ResolvedCommandKitConfig } from './config/utils';
import { getConfig } from './config/config';

/**
* Configurations for the CommandKit instance.
Expand Down Expand Up @@ -110,11 +112,16 @@ export class CommandKit extends EventEmitter {
/**
* The configuration for the CommandKit instance.
*/
public readonly config: CommandKitConfiguration = {
public readonly appConfig: CommandKitConfiguration = {
defaultLocale: Locale.EnglishUS,
getMessageCommandPrefix: () => '!',
};

/**
* The configuration for the CommandKit environment.
*/
public config: ResolvedCommandKitConfig = getConfig();

/**
* A key-value store for storing arbitrary data.
*/
Expand Down Expand Up @@ -178,6 +185,9 @@ export class CommandKit extends EventEmitter {

super();

// lazily load the actual config file
loadConfigFile().then((config) => (this.config = config));

if (!CommandKit.instance) {
CommandKit.instance = this;
}
Expand Down Expand Up @@ -313,7 +323,7 @@ export class CommandKit extends EventEmitter {
setPrefixResolver(
resolver: (message: Message) => Awaitable<string | string[]>,
) {
this.config.getMessageCommandPrefix = resolver;
this.appConfig.getMessageCommandPrefix = resolver;
return this;
}

Expand All @@ -322,7 +332,7 @@ export class CommandKit extends EventEmitter {
* @param locale The default locale.
*/
setDefaultLocale(locale: Locale) {
this.config.defaultLocale = locale;
this.appConfig.defaultLocale = locale;
return this;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/commandkit/src/app/commands/Context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,7 @@ export class Context<
const locale = preferUser ? this.getUserLocale() : this.getGuildLocale();

if (!locale) {
return this.commandkit.config.defaultLocale;
return this.commandkit.appConfig.defaultLocale;
}

return locale;
Expand Down
107 changes: 62 additions & 45 deletions packages/commandkit/src/app/handlers/AppCommandHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ export interface AppCommandNative {
* Custom properties that can be added to an AppCommand.
* This allows for additional metadata or configuration to be associated with a command.
*/
// export type CustomAppCommandProps = Record<string, any>;
export interface CustomAppCommandProps {
[key: string]: any;
}
Expand Down Expand Up @@ -160,19 +159,6 @@ export class AppCommandHandler {
*/
private loadedMiddlewares = new Collection<string, LoadedMiddleware>();

// Name-to-ID mapping for easier lookup
/**
* @private
* @internal
*/
private commandNameToId = new Map<string, string>();

/**
* @private
* @internal
*/
private subcommandPathToId = new Map<string, string>();

/**
* Command registrar for handling Discord API registration.
*/
Expand Down Expand Up @@ -434,7 +420,7 @@ export class AppCommandHandler {
if (source.author.bot) return null;

const prefix =
await this.commandkit.config.getMessageCommandPrefix(source);
await this.commandkit.appConfig.getMessageCommandPrefix(source);

if (!prefix || !prefix.length) return null;

Expand All @@ -443,11 +429,18 @@ export class AppCommandHandler {
Array.isArray(prefix) ? prefix : [prefix],
(command: string) => {
// Find the command by name
const commandId = this.commandNameToId.get(command);
if (!commandId) return null;

const loadedCommand = this.loadedCommands.get(commandId);
if (!loadedCommand) return null;
const loadedCommand = this.findCommandByName(command);
if (!loadedCommand) {
if (
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.`,
);
}
return null;
}

if (
source.guildId &&
Expand Down Expand Up @@ -498,10 +491,7 @@ export class AppCommandHandler {
}

// Find the command by name
const commandId = this.commandNameToId.get(cmdName);
if (!commandId) return null;

const loadedCommand = this.loadedCommands.get(commandId);
const loadedCommand = this.findCommandByName(cmdName);
if (!loadedCommand) return null;

// If this is a guild specific command, check if we're in the right guild
Expand Down Expand Up @@ -534,14 +524,32 @@ export class AppCommandHandler {
};
}

/**
* Finds a command by name.
* @param name - The command name to search for
* @returns The loaded command or null if not found
*/
private findCommandByName(name: string): LoadedCommand | null {
for (const [, loadedCommand] of this.loadedCommands) {
if (loadedCommand.data.command.name === name) {
return loadedCommand;
}

// Check aliases for prefix commands
const aliases = loadedCommand.data.command.aliases;
if (aliases && Array.isArray(aliases) && aliases.includes(name)) {
return loadedCommand;
}
}
return null;
}

/**
* Reloads all commands and middleware from scratch.
*/
public async reloadCommands() {
this.loadedCommands.clear();
this.loadedMiddlewares.clear();
this.commandNameToId.clear();
this.subcommandPathToId.clear();
this.externalCommandData.clear();
this.externalMiddlewareData.clear();

Expand Down Expand Up @@ -589,7 +597,6 @@ export class AppCommandHandler {
public async registerExternalLoadedCommands(data: LoadedCommand[]) {
for (const command of data) {
this.loadedCommands.set(command.command.id, command);
this.commandNameToId.set(command.command.name, command.command.id);
}
}

Expand Down Expand Up @@ -629,12 +636,17 @@ export class AppCommandHandler {

// generate types
if (COMMANDKIT_IS_DEV) {
const commandNames = Array.from(this.loadedCommands.values()).map(
(v) => v.data.command.name,
);
const aliases = Array.from(this.loadedCommands.values()).flatMap(
(v) => v.data.command.aliases || [],
);

const allNames = [...commandNames, ...aliases];

await rewriteCommandDeclaration(
`type CommandTypeData = ${Array.from(
this.loadedCommands
.mapValues((v) => JSON.stringify(v.command.name))
.values(),
).join(' | ')}`,
`type CommandTypeData = ${allNames.map((name) => JSON.stringify(name)).join(' | ')}`,
);
}

Expand Down Expand Up @@ -707,13 +719,18 @@ export class AppCommandHandler {
);
}

if (
(!commandFileData.command.type ||
commandFileData.command.type === ApplicationCommandType.ChatInput) &&
!commandFileData.command.description
) {
commandFileData.command.description = `${command.name} command`;
}
// Apply the specified logic for name and description
const commandName = commandFileData.command.name || command.name;
const commandDescription =
commandFileData.command.description || `${commandName} command`;

// Update the command data with resolved name and description
const updatedCommandData = {
...commandFileData.command,
name: commandName,
description: commandDescription,
aliases: commandFileData.command.aliases,
} as CommandData;

let handlerCount = 0;

Expand Down Expand Up @@ -743,13 +760,13 @@ export class AppCommandHandler {
);
}

let lastUpdated = commandFileData.command;
let lastUpdated = updatedCommandData;

await this.commandkit.plugins.execute(async (ctx, plugin) => {
const res = await plugin.prepareCommand(ctx, lastUpdated);

if (res) {
lastUpdated = res;
lastUpdated = res as CommandData;
}
});

Expand All @@ -758,12 +775,12 @@ export class AppCommandHandler {
guilds: commandFileData.command.guilds,
data: {
...commandFileData,
command: 'toJSON' in lastUpdated ? lastUpdated.toJSON() : lastUpdated,
command:
'toJSON' in lastUpdated && typeof lastUpdated.toJSON === 'function'
? lastUpdated.toJSON()
: lastUpdated,
},
});

// Map command name to ID for easier lookup
this.commandNameToId.set(command.name, id);
} catch (error) {
Logger.error(`Failed to load command ${command.name} (${id})`, error);
}
Expand Down
18 changes: 11 additions & 7 deletions packages/commandkit/src/cli/development.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,17 +223,21 @@ export async function bootstrapDevelopmentServer(configPath?: string) {
console.error(e);
});

ps = await buildAndStart(cwd);

const end = performance.now();

console.log(
`${colors.greenBright('Development server started in')} ${colors.yellowBright(`${(end - start).toFixed(2)}ms`)}
console.log(`${colors.greenBright('Bootstrapped CommandKit Development Environment in')} ${colors.yellowBright(`${(performance.now() - start).toFixed(2)}ms`)}
${colors.greenBright('Watching for changes in')} ${colors.yellowBright('src')} ${colors.greenBright('directory')}

${colors.greenBright('Commands:')}
${colors.yellowBright('r')} - Restart the server
${colors.yellowBright('rc')} - Reload all commands
${colors.yellowBright('re')} - Reload all events`,
${colors.yellowBright('re')} - Reload all events`);

const buildStart = performance.now();

ps = await buildAndStart(cwd);

const buildEnd = performance.now();

console.log(
`\n${colors.greenBright('Development mode compilation took')} ${colors.yellowBright(`${(buildEnd - buildStart).toFixed(2)}ms`)}\n`,
);
}
3 changes: 3 additions & 0 deletions packages/commandkit/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ export function defineConfig(
},
disablePrefixCommands:
config.disablePrefixCommands ?? defaultConfig.disablePrefixCommands,
showUnknownPrefixCommandsWarning:
config.showUnknownPrefixCommandsWarning ??
defaultConfig.showUnknownPrefixCommandsWarning,
};

return defined;
Expand Down
1 change: 1 addition & 0 deletions packages/commandkit/src/config/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ export const defaultConfig: ResolvedCommandKitConfig = {
},
typedCommands: true,
disablePrefixCommands: false,
showUnknownPrefixCommandsWarning: true,
};
5 changes: 5 additions & 0 deletions packages/commandkit/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,9 @@ export interface CommandKitConfig {
* @default false
*/
disablePrefixCommands?: boolean;
/**
* Whether or not to show a warning when a prefix command is not found. This only affects development mode.
* @default true
*/
showUnknownPrefixCommandsWarning?: boolean;
}
Loading
Loading