Skip to content

Conversation

@vrdons
Copy link
Member

@vrdons vrdons commented Feb 3, 2026

Summary by CodeRabbit

  • New Features

    • Added localization support for command error messages, enabling multilingual error handling.
    • Enhanced command configuration with explicit support flags and alias management capabilities.
  • Refactor

    • Restructured internal command building system for improved flexibility and maintainability.
    • Reorganized module imports and project structure.
    • Streamlined build process and dependency management.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 3, 2026

📝 Walkthrough

Walkthrough

This PR replaces module path aliases with relative paths, refactors the command builder system to use a new ApplicationCommandBuilder, removes the Argument class, updates event handlers with private ES fields, and adjusts Context's i18n API. Dependencies are reorganized, build output is modified, and example code is updated.

Changes

Cohort / File(s) Summary
Module Path Migration
src/index.ts, src/context.ts, src/events/ready.ts
Replaced short module aliases (#structures, #ctx) with relative paths (./structures, ./context, ../structures/index) across exports and imports.
Command Builder Refactoring
src/structures/builder/Command.ts, src/structures/builder/Builder.ts, src/structures/builder/index.ts
Replaced CommandOptions interface with ApplicationCommandBuilder constructor; removed Argument class export; introduced new ApplicationCommandBuilder with setters for name, description, aliases, and prefix/slash support flags; updated Command.ts to use data.toJSON() and command.data.name.
Event Handler Refactoring
src/structures/builder/Event.ts
Migrated handler, bound, listener, and register from public/private to ES private fields (#handler, #bound, #listener, #register); updated import paths and initialization logic.
Context and Interaction Updates
src/structures/builder/Context.ts, src/events/interaction.ts, src/events/message.ts
Updated Context.t() signature to accept TOptions with defaultValue; added author getter support for Message; split command existence/capability checks into separate branches with localized error messages in interaction and message handlers; updated module imports to relative paths.
Client Structure Updates
src/structures/core/Client.ts
Removed explicit public modifiers from logger, i18n, and prefix fields; converted loadDir/loadFile to private fields (#loadDir, #loadFile); replaced lodash merge with spread syntax; updated command serialization to use cmd.data.toClientJSON(this); adjusted event module loading paths and updated context/utility imports.
Utility Additions
src/utils/normalizeArray.ts
New module exports normalizeArray utility function and RestOrArray type to normalize variadic or array inputs.
Build and Package Configuration
.github/workflows/release.yml, scripts/utils/build.js, package.json, .npmignore
Replaced npm script invocations with direct Node.js executions in CI; modified build script to exclude scripts and devDependencies from output and minify JSON; removed internal path aliases from package.json imports; dropped i18next and lodash from dependencies; moved i18next-fs-backend to devDependencies; added cliff.toml to .npmignore.
Example Updates
examples/basic_client/index.js, examples/basic_client/locales/en-US/error.json
Updated CommandBuilder to use ApplicationCommandBuilder chain; changed intents from numeric value to IntentsBitField.Flags array; added "error" namespace to i18n config; added new error.json locale file with command.notfound and command.disabled translations.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Paths now relative, aliases gone,
Commands rebuilt from config reborn,
Private fields hide what needs not show,
Builders chain, translations flow,
A structure renewed, the codebase aglow!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Refactor: command builder' accurately captures the main architectural change, which is a comprehensive refactoring of the command builder system including CommandBuilder, ApplicationCommandBuilder, and related structures.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/command_builder

Important

Action Needed: IP Allowlist Update

If your organization protects your Git platform with IP whitelisting, please add the new CodeRabbit IP address to your allowlist:

  • 136.113.208.247/32 (new)
  • 34.170.211.100/32
  • 35.222.179.152/32

Failure to add the new IP will result in interrupted reviews.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/events/interaction.ts (1)

40-50: 🧹 Nitpick | 🔵 Trivial

Error messages in catch block are not localized.

The error handling uses hardcoded English strings while other error messages use ctx.t(). Consider using localized messages for consistency.

♻️ Proposed fix for consistency
 			if (interaction.replied || interaction.deferred) {
 				await interaction.followUp({
-					content: "There was an error while executing this command!",
+					content: ctx.t("error.command.execution", {
+						defaultValue: "There was an error while executing this command!",
+					}),
 					flags: MessageFlags.Ephemeral,
 				});
 			} else {
 				await interaction.reply({
-					content: "There was an error while executing this command!",
+					content: ctx.t("error.command.execution", {
+						defaultValue: "There was an error while executing this command!",
+					}),
 					flags: MessageFlags.Ephemeral,
 				});
 			}
src/structures/core/Client.ts (1)

61-67: ⚠️ Potential issue | 🟠 Major

Fire-and-forget async call may cause race condition.

#loadDir is called with .catch() but without await, meaning login() proceeds to initialize i18n and call super.login() before directory loading completes. Commands and events may not be loaded when the client becomes ready, causing handlers to be missing for early events.

🐛 Proposed fix to await directory loading
 	override async login(token?: string) {
 		if (this.options.includePaths) {
 			for (const p of this.options.includePaths) {
-				this.#loadDir(path.join(getProjectRoot(), p)).catch((error) =>
-					this.logger.error("Error loading events:", error)
-				);
+				try {
+					await this.#loadDir(path.join(getProjectRoot(), p));
+				} catch (error) {
+					this.logger.error("Error loading events:", error);
+				}
 			}
 		}
 		if (this.i18n && !this.i18n.isInitialized) {
🤖 Fix all issues with AI agents
In `@examples/basic_client/locales/en-US/error.json`:
- Around line 1-4: The two locale entries "command.notfound" and
"command.disabled" contain accidental trailing digits; update their values to
user-facing text without placeholders (e.g., change "Command not found or
disabled1" and "Command not found or disabled2" to a clean message such as
"Command not found or disabled") so the UI shows the correct string for both
keys.

In `@src/events/interaction.ts`:
- Around line 11-28: Move the locale initialization (set ctx.locale) to run
before the command existence and supportsSlash checks, and update the two ctx.t
calls inside the interaction handler so their defaultValue strings are distinct
(e.g., "Command not found." for error.command.notfound and "Slash command
disabled." for error.command.disabled) so users see different fallback messages;
modify the interaction.reply calls that reference ctx.t for these keys and
ensure they execute after ctx.locale is set.

In `@src/events/message.ts`:
- Line 25: The Context created in message handler (new Context(context.client, {
message, args })) never has ctx.locale set, so ctx.t() falls back to default;
after creating ctx assign ctx.locale using available sources such as
message.guild?.preferredLocale or a stored user preference, e.g., ctx.locale =
message.guild?.preferredLocale ?? getUserLocale(message.author.id) ??
context.client.config.defaultLocale; update or add a helper (e.g.,
getUserLocale) if you rely on persisted user settings and ensure you reference
the Context variable name (ctx) and the message object when implementing this
assignment.
- Around line 29-51: The two error branches use identical defaultValue text so
missing translations can't distinguish them; update the first ctx.t call (the
!command branch) to use a distinct defaultValue like "Command not found." and
update the second ctx.t call (the !command.supportsPrefix branch) to use a
distinct defaultValue like "Command is disabled." while keeping the same
message.reply usage, allowedMentions, and .then(deleteMessageAfterSent)
behavior; locate the checks for !command and !command.supportsPrefix in the
message handler to apply the change.

In `@src/structures/builder/Builder.ts`:
- Around line 45-47: The toJSON() override in Builder currently returns
super.toJSON() cast to ApplicationJSONBody but omits the custom fields; update
Builder.toJSON() to take the object from super.toJSON(), add the custom
properties prefix_support, slash_support, and aliases from the Builder instance
(or delegate to toClientJSON() if that produces the complete shape), and return
the merged object as ApplicationJSONBody so consumers like
Client.registerCommands() receive the full serialized data.
- Around line 13-16: Remove the redundant empty constructor from the
ApplicationCommandBuilder class: delete the constructor() { super(); } block so
the class simply extends SlashCommandBuilder without defining a constructor
(TypeScript will call the parent constructor automatically); update any
references only if they rely on an explicit constructor (none should).
- Around line 49-55: The toClientJSON method declares a unused parameter client;
either remove the parameter from Builder.toClientJSON and update all call sites
that pass a Client (e.g., places that call cmd.data.toClientJSON(this)) to call
it without an argument, or keep the parameter but mark it as intentionally
unused (rename to _client or add a concise comment above toClientJSON stating
"client parameter reserved for future use / required by registerCommands call")
so linting/CI no longer complains; update the signature for Builder.toClientJSON
and matching call sites (or add the comment) accordingly.
- Around line 21-44: The setters/getters (setAliases, addAliases,
setPrefixSupport, setSlashSupport) are using Reflect.get/Reflect.set
unnecessarily; replace those with direct property access (e.g., this.aliases,
this.prefix_support, this.slash_support) and use the existing
normalizeArray(...) call in addAliases to merge current aliases (default to []
when undefined) with new ones; ensure you keep the same return-this chaining and
type expectations for normalizeArray and existing class properties.

In `@src/structures/builder/Command.ts`:
- Around line 37-41: The constructor currently validates
`#supportsPrefix/`#supportsSlash before handlers are attached, so add a runtime
warning in registerCommands(): for each Command instance, if the configuration
flag (the private flags or the data-derived flag tested by
supportsSlash/supportsPrefix) is true but no handler has been attached via
onInteraction() (for slash) or onMessage() (for prefix), emit a warning (e.g.,
logger.warn or console.warn) naming the Command (data.name) and which handler is
missing; keep existing constructor validation but use registerCommands() to
detect and warn about mismatches between the declared support flags
(`#supportsSlash/`#supportsPrefix or supportsSlash/supportsPrefix) and the
presence of handlers (onInteraction/onMessage).

In `@src/structures/builder/Context.ts`:
- Around line 62-64: The current fallback uses "result === key" which can be a
false positive; instead, call i18n.exists(key, { lng: locale }) to determine
whether the key is resolvable and only return options.defaultValue when
i18n.exists(...) is false (i.e., key missing for the given locale); update the
branch around result, key, options?.defaultValue to use !i18n.exists(key, { lng:
locale }) and ensure i18n and locale are in scope before returning the default.

In `@src/utils/normalizeArray.ts`:
- Around line 7-11: The normalizeArray function incorrectly unwraps when
ItemType itself is an array because it only checks Array.isArray(arr[0]); change
the unwrap guard in normalizeArray (exported function
normalizeArray<ItemType>(arr: RestOrArray<ItemType>)) to require that arr has
exactly one element and that element is an array (e.g., check arr.length === 1
&& Array.isArray(arr[0])) before returning the unwrapped value, so rest-style
calls like [["a"], ["b"]] are not collapsed.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 6835b09 and 74cefb3.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (19)
  • .github/workflows/release.yml
  • .npmignore
  • examples/basic_client/index.js
  • examples/basic_client/locales/en-US/error.json
  • package.json
  • scripts/utils/build.js
  • src/context.ts
  • src/events/interaction.ts
  • src/events/message.ts
  • src/events/ready.ts
  • src/index.ts
  • src/structures/builder/Argument.ts
  • src/structures/builder/Builder.ts
  • src/structures/builder/Command.ts
  • src/structures/builder/Context.ts
  • src/structures/builder/Event.ts
  • src/structures/builder/index.ts
  • src/structures/core/Client.ts
  • src/utils/normalizeArray.ts
💤 Files with no reviewable changes (1)
  • src/structures/builder/Argument.ts
🧰 Additional context used
🧬 Code graph analysis (6)
src/events/interaction.ts (2)
src/structures/builder/Event.ts (1)
  • EventBuilder (13-64)
src/structures/builder/Context.ts (1)
  • Context (10-89)
src/structures/builder/Builder.ts (2)
src/utils/normalizeArray.ts (1)
  • normalizeArray (7-12)
src/structures/core/Client.ts (1)
  • Client (27-129)
src/structures/builder/Command.ts (5)
src/structures/builder/Context.ts (1)
  • Context (10-89)
src/structures/core/Client.ts (1)
  • Client (27-129)
types/extra.d.ts (1)
  • MaybePromise (1-1)
src/structures/builder/Builder.ts (1)
  • ApplicationCommandBuilder (13-56)
src/context.ts (1)
  • currentClient (4-4)
src/events/message.ts (2)
src/structures/builder/Context.ts (1)
  • Context (10-89)
src/utils/util.ts (1)
  • deleteMessageAfterSent (4-14)
src/structures/builder/Event.ts (2)
src/structures/core/Client.ts (1)
  • Client (27-129)
src/utils/logger/Logger.ts (1)
  • Logger (11-200)
src/structures/core/Client.ts (4)
src/structures/builder/Command.ts (1)
  • CommandBuilder (13-87)
types/client.d.ts (1)
  • FrameworkOptions (16-22)
src/context.ts (1)
  • clearClient (10-12)
src/utils/Files.ts (2)
  • getProjectRoot (21-27)
  • getFiles (4-20)
🔇 Additional comments (32)
.npmignore (1)

6-6: LGTM — packaging ignore update is clear.

src/events/ready.ts (1)

2-2: Import path update looks safe.

scripts/utils/build.js (1)

19-25: LGTM — package.json pruning and minified write are consistent.

package.json (3)

22-24: Imports mapping simplification looks fine.


30-32: Build script update is straightforward.


39-46: i18next-fs-backend placement in devDependencies is correct.

This package is only used in examples/basic_client/index.js and does not appear in any production source code (src/). It is appropriately placed in devDependencies and will not cause MODULE_NOT_FOUND errors in production installs since the main application code does not import it.

Likely an incorrect or invalid review comment.

.github/workflows/release.yml (1)

68-74: No action needed. The scripts do not depend on npm-provided environment variables and will function correctly with direct node execution. Both github.js and npm.js access only explicitly-passed environment variables (GITHUB_TOKEN, NPM_ORG, GITHUB_SHA) and read package.json via Node's require() mechanism. The npm publish commands use explicit --registry arguments, so npm's environment configuration is not relied upon.

src/structures/builder/index.ts (1)

1-4: LGTM!

The barrel export properly includes the new Builder module while maintaining clean re-exports from the builder directory.

src/context.ts (1)

1-1: LGTM!

Import path correctly updated to relative path, consistent with the PR's refactoring goals.

src/index.ts (1)

1-3: LGTM!

Export paths correctly updated to relative paths, aligning with the PR's refactoring goals. The module structure remains clean.

examples/basic_client/index.js (2)

37-42: LGTM!

The example correctly demonstrates the new ApplicationCommandBuilder pattern with fluent API chaining. This aligns well with the builder pattern introduced in Builder.ts.


12-12: Good addition of "error" namespace.

Adding the "error" namespace aligns with the new error.command.notfound and error.command.disabled translation keys used in the event handlers.

src/structures/builder/Context.ts (5)

2-3: LGTM!

Import paths updated to use relative paths, aligning with the refactor.


11-13: LGTM!

Using readonly for args and data improves immutability guarantees while preserving public access.


28-34: LGTM!

Type guard methods correctly narrow the generic type using instanceof checks.


36-44: LGTM!

The author getter now correctly handles both Message (via data.author) and ChatInputCommandInteraction (via data.user), with a safe null fallback.


69-88: LGTM!

The toJSON method correctly binds this.t to preserve context when the translation function is extracted.

src/structures/builder/Event.ts (5)

3-5: LGTM!

Import paths updated to use relative paths consistent with the refactor.


14-17: LGTM!

ES private fields (#handler, #bound) provide true encapsulation, preventing external access and modification.


19-29: LGTM!

The #listener correctly guards against missing handler and properly propagates errors to the logger without crashing the event loop.


46-57: LGTM!

The #register method correctly guards against double registration via #bound and chooses between once and on based on the event configuration.


59-63: LGTM!

The fluent API pattern with onExecute returning this allows method chaining.

src/structures/builder/Command.ts (4)

1-6: LGTM!

Imports updated to use the new ApplicationCommandBuilder and relative paths.


16-26: LGTM!

ES private fields for support flags with getters that require both the flag AND the handler to be set. This ensures a command is only considered to support slash/prefix if it's both configured for it and has the corresponding handler.


43-69: LGTM!

Comprehensive validation for command name and alias conflicts, preventing duplicate registrations and alias collisions.


71-75: LGTM!

Command and aliases are registered correctly, with debug logging for observability.

src/structures/core/Client.ts (6)

8-18: LGTM!

Import paths updated to use relative paths and re-exports from index files.


30-34: LGTM!

Field declarations updated with appropriate visibility. The readonly modifier on logger and prefix ensures immutability.


40-41: LGTM!

Changed from lodash.merge to spread operator. Since defaultOpts contains only primitives and a flat array, shallow merging is sufficient here.


74-83: LGTM!

Private method #loadDir properly checks directory existence and iterates through files.


85-95: LGTM!

Private method #loadFile properly clears require cache, sets/clears the client context, and handles errors gracefully.


109-111: LGTM!

Command serialization now uses the builder's toClientJSON method, aligning with the new ApplicationCommandBuilder pattern.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +1 to +4
{
"command.notfound": "Command not found or disabled1",
"command.disabled": "Command not found or disabled2"
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix placeholder digits in user-facing strings.
The trailing “1/2” looks like accidental placeholders and will surface in UI.

✅ Suggested correction
 {
-	"command.notfound": "Command not found or disabled1",
-	"command.disabled": "Command not found or disabled2"
+	"command.notfound": "Command not found.",
+	"command.disabled": "Command is disabled."
 }
🤖 Prompt for AI Agents
In `@examples/basic_client/locales/en-US/error.json` around lines 1 - 4, The two
locale entries "command.notfound" and "command.disabled" contain accidental
trailing digits; update their values to user-facing text without placeholders
(e.g., change "Command not found or disabled1" and "Command not found or
disabled2" to a clean message such as "Command not found or disabled") so the UI
shows the correct string for both keys.

Comment on lines +11 to 28
if (!command) {
await interaction.reply({
content: "Command not found or disabled.",
content: ctx.t("error.command.notfound", {
defaultValue: "Command not found or disabled.",
}),
flags: MessageFlags.Ephemeral,
});
return;
}
if (!command.supportsSlash) {
await interaction.reply({
content: ctx.t("error.command.disabled", {
defaultValue: "Command not found or disabled.",
}),
flags: MessageFlags.Ephemeral,
});
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Default values should be distinct for different error states.

Same issue as in message.ts: both error.command.notfound and error.command.disabled use identical defaultValue. Additionally, ctx.locale is set at line 30, after these checks, so error messages won't use the user's locale.

💡 Proposed fix
 		const ctx = new Context(context.client, { interaction });
+		ctx.locale = interaction.locale;

 		if (!command) {
 			await interaction.reply({
 				content: ctx.t("error.command.notfound", {
-					defaultValue: "Command not found or disabled.",
+					defaultValue: "Command not found.",
 				}),
 				flags: MessageFlags.Ephemeral,
 			});
 			return;
 		}
 		if (!command.supportsSlash) {
 			await interaction.reply({
 				content: ctx.t("error.command.disabled", {
-					defaultValue: "Command not found or disabled.",
+					defaultValue: "This command is not available as a slash command.",
 				}),
 				flags: MessageFlags.Ephemeral,
 			});
 			return;
 		}
 		try {
-			ctx.locale = interaction.locale;
 			context.logger.debug(
🤖 Prompt for AI Agents
In `@src/events/interaction.ts` around lines 11 - 28, Move the locale
initialization (set ctx.locale) to run before the command existence and
supportsSlash checks, and update the two ctx.t calls inside the interaction
handler so their defaultValue strings are distinct (e.g., "Command not found."
for error.command.notfound and "Slash command disabled." for
error.command.disabled) so users see different fallback messages; modify the
interaction.reply calls that reference ctx.t for these keys and ensure they
execute after ctx.locale is set.

const commandAlias = context.client.aliases.findKey((cmd) =>
cmd.has(commandName)
);
const ctx = new Context(context.client, { message, args });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing locale assignment for message context.

In interaction.ts (line 30), ctx.locale is set to interaction.locale before using the context. Here, ctx.locale remains undefined, causing ctx.t() to always fall back to the default language. Consider deriving locale from the guild or user preferences if available.

🤖 Prompt for AI Agents
In `@src/events/message.ts` at line 25, The Context created in message handler
(new Context(context.client, { message, args })) never has ctx.locale set, so
ctx.t() falls back to default; after creating ctx assign ctx.locale using
available sources such as message.guild?.preferredLocale or a stored user
preference, e.g., ctx.locale = message.guild?.preferredLocale ??
getUserLocale(message.author.id) ?? context.client.config.defaultLocale; update
or add a helper (e.g., getUserLocale) if you rely on persisted user settings and
ensure you reference the Context variable name (ctx) and the message object when
implementing this assignment.

Comment on lines +29 to 51
if (!command) {
await message
.reply({
content: ctx.t("error.command.notfound", {
defaultValue: "Command not found or disabled.",
}),
allowedMentions: { repliedUser: false },
})
.then(deleteMessageAfterSent);
return;
}

if (!command.supportsPrefix) {
await message
.reply({
content: "Command not found or disabled.",
content: ctx.t("error.command.disabled", {
defaultValue: "Command not found or disabled.",
}),
allowedMentions: { repliedUser: false },
})
.then(deleteMessageAfterSent);
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Default values should be distinct for different error states.

Both error.command.notfound and error.command.disabled use the same defaultValue: "Command not found or disabled.". This makes it impossible to distinguish between the two error conditions when translations are missing. Consider using distinct messages.

💡 Proposed fix with distinct messages
 		if (!command) {
 			await message
 				.reply({
 					content: ctx.t("error.command.notfound", {
-						defaultValue: "Command not found or disabled.",
+						defaultValue: "Command not found.",
 					}),
 					allowedMentions: { repliedUser: false },
 				})
 				.then(deleteMessageAfterSent);
 			return;
 		}

 		if (!command.supportsPrefix) {
 			await message
 				.reply({
 					content: ctx.t("error.command.disabled", {
-						defaultValue: "Command not found or disabled.",
+						defaultValue: "This command is not available for prefix usage.",
 					}),
 					allowedMentions: { repliedUser: false },
 				})
 				.then(deleteMessageAfterSent);
 			return;
 		}
🤖 Prompt for AI Agents
In `@src/events/message.ts` around lines 29 - 51, The two error branches use
identical defaultValue text so missing translations can't distinguish them;
update the first ctx.t call (the !command branch) to use a distinct defaultValue
like "Command not found." and update the second ctx.t call (the
!command.supportsPrefix branch) to use a distinct defaultValue like "Command is
disabled." while keeping the same message.reply usage, allowedMentions, and
.then(deleteMessageAfterSent) behavior; locate the checks for !command and
!command.supportsPrefix in the message handler to apply the change.

Comment on lines +13 to +16
export class ApplicationCommandBuilder extends SlashCommandBuilder {
constructor() {
super();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Redundant constructor can be removed.

The constructor only calls super() with no additional logic. TypeScript automatically calls the parent constructor when none is defined.

🔧 Proposed fix
 export class ApplicationCommandBuilder extends SlashCommandBuilder {
-	constructor() {
-		super();
-	}
 	protected prefix_support: boolean = true;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export class ApplicationCommandBuilder extends SlashCommandBuilder {
constructor() {
super();
}
export class ApplicationCommandBuilder extends SlashCommandBuilder {
protected prefix_support: boolean = true;
🤖 Prompt for AI Agents
In `@src/structures/builder/Builder.ts` around lines 13 - 16, Remove the redundant
empty constructor from the ApplicationCommandBuilder class: delete the
constructor() { super(); } block so the class simply extends SlashCommandBuilder
without defining a constructor (TypeScript will call the parent constructor
automatically); update any references only if they rely on an explicit
constructor (none should).

Comment on lines +45 to +47
override toJSON(): ApplicationJSONBody {
return super.toJSON() as ApplicationJSONBody;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: toJSON() does not include custom fields.

The override casts super.toJSON() to ApplicationJSONBody, but the custom fields (prefix_support, slash_support, aliases) are never added to the output. This means the serialized JSON will not contain these properties, breaking downstream consumers (e.g., Client.registerCommands() which expects toClientJSON() to return complete data).

🐛 Proposed fix to include custom fields
 	override toJSON(): ApplicationJSONBody {
-		return super.toJSON() as ApplicationJSONBody;
+		return {
+			...super.toJSON(),
+			prefix_support: this.prefix_support,
+			slash_support: this.slash_support,
+			aliases: this.aliases,
+		};
 	}
🤖 Prompt for AI Agents
In `@src/structures/builder/Builder.ts` around lines 45 - 47, The toJSON()
override in Builder currently returns super.toJSON() cast to ApplicationJSONBody
but omits the custom fields; update Builder.toJSON() to take the object from
super.toJSON(), add the custom properties prefix_support, slash_support, and
aliases from the Builder instance (or delegate to toClientJSON() if that
produces the complete shape), and return the merged object as
ApplicationJSONBody so consumers like Client.registerCommands() receive the full
serialized data.

Comment on lines +37 to 41
if (!this.#supportsPrefix && !this.#supportsSlash) {
throw new Error(
`Command ${this.name} must support either slash or prefix commands.`
`Command ${data.name} must support either slash or prefix commands.`
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Validation occurs before handlers are attached.

The validation checks #supportsPrefix and #supportsSlash flags, but the actual supportsSlash/supportsPrefix getters also require handlers to be set. A command configured with slash_support: true but where onInteraction() is never called will pass this validation but won't actually register as a slash command. Consider adding a warning in registerCommands() if a command has the flag set but no handler.

🤖 Prompt for AI Agents
In `@src/structures/builder/Command.ts` around lines 37 - 41, The constructor
currently validates `#supportsPrefix/`#supportsSlash before handlers are attached,
so add a runtime warning in registerCommands(): for each Command instance, if
the configuration flag (the private flags or the data-derived flag tested by
supportsSlash/supportsPrefix) is true but no handler has been attached via
onInteraction() (for slash) or onMessage() (for prefix), emit a warning (e.g.,
logger.warn or console.warn) naming the Command (data.name) and which handler is
missing; keep existing constructor validation but use registerCommands() to
detect and warn about mismatches between the declared support flags
(`#supportsSlash/`#supportsPrefix or supportsSlash/supportsPrefix) and the
presence of handlers (onInteraction/onMessage).

Comment on lines +62 to +64
if (result === key && options?.defaultValue) {
return options.defaultValue;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

❓ Verification inconclusive

Script executed:

# First, let's examine the Context.ts file around lines 62-64
cat -n src/structures/builder/Context.ts | sed -n '50,75p'

Repository: AroxBot/framework


Repository: AroxBot/framework
Exit code: 0

stdout:

    50	
    51			const locale =
    52				this.locale ??
    53				(Array.isArray(this.client.i18n.options.fallbackLng)
    54					? this.client.i18n.options.fallbackLng[0]
    55					: this.client.i18n.options.fallbackLng) ??
    56				"en";
    57	
    58			const t = this.client.i18n.getFixedT(locale);
    59	
    60			const result = t(key, options);
    61	
    62			if (result === key && options?.defaultValue) {
    63				return options.defaultValue;
    64			}
    65	
    66			return result;
    67		}
    68	
    69		toJSON() {
    70			const { data, args, author } = this;
    71	
    72			if (this.isInteraction()) {
    73				return {
    74					kind: "interaction" as const,
    75					interaction: data,

i18next check if translation key exists i18n.exists method


Use i18next.exists(key, options?) (aka i18n.exists)—it uses the same key resolution logic as t() and returns a boolean indicating whether the key can be resolved. [1][2]

Basic (i18next)

import i18next from "i18next";

if (i18next.exists("common.values.active")) {
  // key exists
}

[1]

With react-i18next

import { useTranslation } from "react-i18next";

function Comp() {
  const { i18n } = useTranslation();
  const ok = i18n.exists("common.values.active");
}

[3][4]

Notes

  • options are the same as t() (e.g., lng, ns, context, count, etc.), so existence checks respect namespaces/language overrides the same way translations do. [1][2]

i18next returnedObjectHandler configuration


returnedObjectHandler is an i18next option that is invoked when a translation resolves to an object/array but returnObjects is false (the default). It lets you decide what to do in that situation (e.g., warn, throw, or convert the object to a string). By default it’s a no-op. [1]

Configure globally

i18next.init({
  returnObjects: false, // default
  returnedObjectHandler: (key, value, options) => {
    // key: the resolved key
    // value: the resolved object/array
    // options: t() options
    console.warn(`i18next: "${key}" returned an object; enable returnObjects to use it.`);
    return ''; // or JSON.stringify(value), or throw new Error(...)
  }
});

(Handler signature: function(key, value, options) {}) [1]

Recommended alternative (if you actually want the object)

Enable object returns instead of handling the fallback:

i18next.t('tree', { returnObjects: true });

or globally:

i18next.init({ returnObjects: true });

[1][2]

Sources: i18next configuration options (includes returnedObjectHandler, returnObjects) [1], Objects & Arrays docs (shows returnObjects: true usage) [2].


Use i18n.exists() to reliably check if a translation key has a value.

The fallback check result === key is unreliable because a translation file could legitimately map a key to itself, producing a false positive and skipping the default value when one should be used. Instead, use i18n.exists(key, { lng: locale }) to explicitly verify whether the translation key is resolvable before deciding whether to apply the default value.

🤖 Prompt for AI Agents
In `@src/structures/builder/Context.ts` around lines 62 - 64, The current fallback
uses "result === key" which can be a false positive; instead, call
i18n.exists(key, { lng: locale }) to determine whether the key is resolvable and
only return options.defaultValue when i18n.exists(...) is false (i.e., key
missing for the given locale); update the branch around result, key,
options?.defaultValue to use !i18n.exists(key, { lng: locale }) and ensure i18n
and locale are in scope before returning the default.

Comment on lines +7 to +11
export function normalizeArray<ItemType>(
arr: RestOrArray<ItemType>
): ItemType[] {
if (Array.isArray(arr[0])) return [...arr[0]];
return arr as ItemType[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's see the complete normalizeArray implementation
cat -n src/utils/normalizeArray.ts

Repository: AroxBot/framework

Length of output: 897


🏁 Script executed:

# Search for RestOrArray type definition
rg -n "RestOrArray" src --type ts

Repository: AroxBot/framework

Length of output: 303


🏁 Script executed:

# Find all usages of normalizeArray
rg -n "normalizeArray" src --type ts -B 2 -A 2

Repository: AroxBot/framework

Length of output: 1675


🏁 Script executed:

# Check for any actual usage patterns with array ItemTypes
rg -n "normalizeArray" src --type ts -B 5 -A 5

Repository: AroxBot/framework

Length of output: 3269


🏁 Script executed:

# Search for any complex types being used with normalizeArray
ast-grep --pattern 'normalizeArray($_)'

Repository: AroxBot/framework

Length of output: 254


🏁 Script executed:

# Search for other uses of RestOrArray or normalizeArray beyond what we've seen
rg -n "RestOrArray|normalizeArray" src --type ts | grep -v "normalizeArray.ts"

Repository: AroxBot/framework

Length of output: 351


🏁 Script executed:

# Check if there are any type definitions or interfaces that might use array types with normalizeArray
rg -n "RestOrArray" src --type ts -B 3 -A 3

Repository: AroxBot/framework

Length of output: 1116


Guard against unintended unwrapping when ItemType is an array type.
The RestOrArray<Type> type permits both rest parameters (Type[]) and wrapped arrays ([Type[]]). When ItemType itself is an array, the unwrap condition can drop a nesting level: if ItemType = string[] and arr = [["a"], ["b"]] (rest params), arr[0] is an array so the code returns [["a"]] instead of [["a"], ["b"]]. The suggested check prevents this:

🔧 Safer unwrap condition
 export function normalizeArray<ItemType>(
 	arr: RestOrArray<ItemType>
 ): ItemType[] {
-	if (Array.isArray(arr[0])) return [...arr[0]];
+	if (arr.length === 1 && Array.isArray(arr[0])) return [...arr[0]];
 	return arr as ItemType[];
 }

Current usage is only with string types, so this does not affect existing code. Apply the fix to prevent future edge cases.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function normalizeArray<ItemType>(
arr: RestOrArray<ItemType>
): ItemType[] {
if (Array.isArray(arr[0])) return [...arr[0]];
return arr as ItemType[];
export function normalizeArray<ItemType>(
arr: RestOrArray<ItemType>
): ItemType[] {
if (arr.length === 1 && Array.isArray(arr[0])) return [...arr[0]];
return arr as ItemType[];
}
🤖 Prompt for AI Agents
In `@src/utils/normalizeArray.ts` around lines 7 - 11, The normalizeArray function
incorrectly unwraps when ItemType itself is an array because it only checks
Array.isArray(arr[0]); change the unwrap guard in normalizeArray (exported
function normalizeArray<ItemType>(arr: RestOrArray<ItemType>)) to require that
arr has exactly one element and that element is an array (e.g., check arr.length
=== 1 && Array.isArray(arr[0])) before returning the unwrapped value, so
rest-style calls like [["a"], ["b"]] are not collapsed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant