Skip to content
Open
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
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ jobs:
uses: taiki-e/install-action@git-cliff

- name: Run github release script
run: npm run release:git
run: node scripts/actions/github.js
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
Expand Down Expand Up @@ -95,7 +95,7 @@ jobs:
run: npm install

- name: Run npm release script
run: npm run release:npm
run: node scripts/actions/npm.js
env:
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_SHA: ${{ github.sha }}
Expand Down
1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
CHANGELOG.md
cargo.toml
.husky
cliff.toml

# Test
examples
Expand Down
21 changes: 13 additions & 8 deletions examples/basic_client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ const arox = require("../../dist/index");
const backend = require("i18next-fs-backend");
const path = require("node:path");
const { LogLevel } = require("../../dist/utils/logger/ILogger");
const { IntentsBitField } = require("discord.js");

const myinstance = i18next.createInstance({
supportedLngs: ["en-US", "tr"],
fallbackLng: "en-US",
defaultNS: "translation",
ns: ["translation", "test"],
ns: ["translation", "test", "error"],
backend: {
loadPath: path.join(__dirname, "locales/{{lng}}/{{ns}}.json"),
},
Expand All @@ -19,7 +20,11 @@ const myinstance = i18next.createInstance({
myinstance.use(backend);

const client = new arox.Client({
intents: 37376,
intents: [
IntentsBitField.Flags.Guilds,
IntentsBitField.Flags.GuildMessages,
IntentsBitField.Flags.MessageContent,
],
prefix: { enabled: true, prefix: "a!" },
logger: {
level: LogLevel.Trace,
Expand All @@ -29,12 +34,12 @@ const client = new arox.Client({
});

arox.setClient(client);
const command = new arox.CommandBuilder({
name: "arox",
description: "Arox test command",
slash: true,
prefix: true,
});
const command = new arox.CommandBuilder(
new arox.ApplicationCommandBuilder()
.setName("arox")
.setDescription("Arox Test Command")
.addAliases("a")
);
arox.clearClient();

command
Expand Down
4 changes: 4 additions & 0 deletions examples/basic_client/locales/en-US/error.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"command.notfound": "Command not found or disabled1",
"command.disabled": "Command not found or disabled2"
}
Comment on lines +1 to +4
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.

9 changes: 5 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 4 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,7 @@
"main": "dist/index.js",
"types": "dist/index.d.ts",
"imports": {
"#types/*": "./types/*",
"#utils": "./src/utils/index.ts",
"#structures": "./src/structures/index.ts",
"#ctx": "./src/context.ts"
"#types/*": "./types/*"
},
"scripts": {
"prepare": "node scripts/prepareHusky.js",
Expand All @@ -32,24 +29,21 @@
"format": "oxfmt",
"build:js": "swc src -d dist --strip-leading-paths --copy-files",
"build:dts": "tsc --emitDeclarationOnly",
"build": "npm run build:js && node ./scripts/actions/patch.js && npm run build:dts",
"release:git": "node scripts/actions/github.js",
"release:npm": "node scripts/actions/npm.js"
"build": "npm run build:js && node ./scripts/actions/patch.js && npm run build:dts"
},
"dependencies": {
"@sapphire/timestamp": "^1.0.5",
"@swc/helpers": "^0.5.18",
"colorette": "^2.0.20",
"fast-glob": "^3.3.3",
"i18next": "^25.8.0",
"i18next-fs-backend": "^2.6.1",
"lodash": "^4.17.21"
"i18next": "^25.8.0"
},
"devDependencies": {
"@swc/cli": "^0.7.10",
"@types/lodash": "^4.17.23",
"@types/node": "^25.0.9",
"husky": "^9.1.7",
"i18next-fs-backend": "^2.6.1",
"libnpmpack": "^9.0.12",
"oxfmt": "^0.27.0",
"oxlint": "^1.39.0",
Expand Down
5 changes: 4 additions & 1 deletion scripts/utils/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@ module.exports = async function buildPackage(tempjson) {
console.error("Build failed, aborting release");
throw err;
}

delete tempjson.scripts;
delete tempjson.devDependencies;
await writeFile(
path.join(process.cwd(), "package.json"),
JSON.stringify(tempjson, null, 2)
JSON.stringify(tempjson)
);
console.log("Packing npm...");
const tarballBuffer = await pack(process.cwd());
Expand Down
2 changes: 1 addition & 1 deletion src/context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Client } from "#structures";
import { Client } from "./structures";

// Birden fazla client olursa hata çıkartabilir ama aklıma gelen tek şey bu
export let currentClient: Client | null = null;
Expand Down
25 changes: 18 additions & 7 deletions src/events/interaction.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,40 @@
import { Events, MessageFlags } from "discord.js";
import { EventBuilder, Context } from "#structures";
import { EventBuilder, Context } from "../structures/index";

new EventBuilder(Events.InteractionCreate, false).onExecute(
async function (context, interaction) {
if (!interaction.isChatInputCommand()) return;

const command = context.client.commands.get(interaction.commandName);
if (!command || !command.supportsSlash) {
const ctx = new Context(context.client, { interaction });

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;
}
Comment on lines +11 to 28
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.


try {
const ctx = new Context(context.client, { interaction });
ctx.locale = interaction.locale;
context.logger.debug(
`${ctx.author?.tag ?? "Unknown"} used ${command.name}(interaction)`
`${ctx.author?.tag ?? "Unknown"} used ${command.data.name}(interaction)`
);
if (command._onInteraction) await command._onInteraction(ctx.toJSON());
} catch (error) {
context.client.logger.error(
`Error executing command ${command.name}:`,
`Error executing command ${command.data.name}:`,
error
);
if (interaction.replied || interaction.deferred) {
Expand Down
29 changes: 22 additions & 7 deletions src/events/message.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Events } from "discord.js";
import { EventBuilder, Context } from "#structures";
import { deleteMessageAfterSent } from "#utils";
import { EventBuilder, Context } from "../structures/index";
import { deleteMessageAfterSent } from "../utils/index";

new EventBuilder(
Events.MessageCreate,
Expand All @@ -22,27 +22,42 @@ new EventBuilder(
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.


let command = context.client.commands.get(commandAlias ?? commandName);
if (!command || !command.supportsPrefix) {

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;
}
Comment on lines +29 to 51
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.


try {
const ctx = new Context(context.client, { message, args });
context.logger.debug(
`${ctx.author?.tag ?? "Unknown"} used ${command.name}(message)`
`${ctx.author?.tag ?? "Unknown"} used ${command.data.name}(message)`
);
if (command._onMessage) await command._onMessage(ctx.toJSON());
} catch (error) {
context.client.logger.error(
`Error executing command ${command.name}:`,
`Error executing command ${command.data.name}:`,
error
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/events/ready.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Events } from "discord.js";
import { EventBuilder } from "#structures";
import { EventBuilder } from "../structures/index";

new EventBuilder(Events.ClientReady).onExecute(async function (context) {
if (context.client.options.autoRegisterCommands) {
Expand Down
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export * from "#structures";
export * from "./structures";
export * from "./utils/logger/Logger";
export * from "#ctx";
export * from "./context";

export const version = "[VI]{{version}}[/VI]";
46 changes: 0 additions & 46 deletions src/structures/builder/Argument.ts

This file was deleted.

56 changes: 56 additions & 0 deletions src/structures/builder/Builder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {
SlashCommandBuilder,
RESTPostAPIChatInputApplicationCommandsJSONBody,
} from "discord.js";
import { normalizeArray } from "../../utils/normalizeArray";
import { Client } from "../core";

export interface ApplicationJSONBody extends RESTPostAPIChatInputApplicationCommandsJSONBody {
prefix_support: boolean;
slash_support: boolean;
aliases: string[];
}
export class ApplicationCommandBuilder extends SlashCommandBuilder {
constructor() {
super();
}
Comment on lines +13 to +16
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).

protected prefix_support: boolean = true;
protected slash_support: boolean = true;
protected aliases: string[] = [];

setAliases(...alias: string[]) {
Reflect.set(this, "aliases", normalizeArray(alias));
return this;
}

addAliases(...alias: string[]) {
const currentAliases = Reflect.get(this, "aliases") || [];
Reflect.set(
this,
"aliases",
normalizeArray([...currentAliases, ...normalizeArray(alias)])
);
return this;
}

setPrefixSupport(support: boolean) {
Reflect.set(this, "prefix_support", support);
return this;
}

setSlashSupport(support: boolean) {
Reflect.set(this, "slash_support", support);
return this;
}
Comment on lines +21 to +44
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

Unnecessary use of Reflect.set/Reflect.get for own properties.

These properties are defined directly on the class. Using Reflect.set(this, "aliases", ...) instead of this.aliases = ... adds indirection without benefit. Direct property access is clearer and maintains type safety.

♻️ Proposed refactor for direct property access
 	setAliases(...alias: string[]) {
-		Reflect.set(this, "aliases", normalizeArray(alias));
+		this.aliases = normalizeArray(alias);
 		return this;
 	}

 	addAliases(...alias: string[]) {
-		const currentAliases = Reflect.get(this, "aliases") || [];
-		Reflect.set(
-			this,
-			"aliases",
-			normalizeArray([...currentAliases, ...normalizeArray(alias)])
-		);
+		this.aliases = [...this.aliases, ...normalizeArray(alias)];
 		return this;
 	}

 	setPrefixSupport(support: boolean) {
-		Reflect.set(this, "prefix_support", support);
+		this.prefix_support = support;
 		return this;
 	}

 	setSlashSupport(support: boolean) {
-		Reflect.set(this, "slash_support", support);
+		this.slash_support = support;
 		return this;
 	}
📝 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
setAliases(...alias: string[]) {
Reflect.set(this, "aliases", normalizeArray(alias));
return this;
}
addAliases(...alias: string[]) {
const currentAliases = Reflect.get(this, "aliases") || [];
Reflect.set(
this,
"aliases",
normalizeArray([...currentAliases, ...normalizeArray(alias)])
);
return this;
}
setPrefixSupport(support: boolean) {
Reflect.set(this, "prefix_support", support);
return this;
}
setSlashSupport(support: boolean) {
Reflect.set(this, "slash_support", support);
return this;
}
this.aliases = normalizeArray([...this.aliases, ...normalizeArray(alias)]);
🤖 Prompt for AI Agents
In `@src/structures/builder/Builder.ts` around lines 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.

override toJSON(): ApplicationJSONBody {
return super.toJSON() as ApplicationJSONBody;
}
Comment on lines +45 to +47
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.


toClientJSON(
client: Client
): ReturnType<ApplicationCommandBuilder["toJSON"]> {
return {
...this.toJSON(),
};
}
}
Loading