From 14667ad69f692512e2cae85932a237dd173cd708 Mon Sep 17 00:00:00 2001 From: Ahmad <103906421+ahmadk953@users.noreply.github.com> Date: Wed, 16 Apr 2025 19:20:17 -0400 Subject: [PATCH] chore: add option to undeploy commands and not deploy on start --- package.json | 3 ++ src/structures/ExtendedClient.ts | 69 +++++++++++++++++++++++++++----- src/util/deployCommand.ts | 2 +- src/util/undeployCommands.ts | 36 +++++++++++++++++ yarn.lock | 15 ++++++- 5 files changed, 113 insertions(+), 12 deletions(-) create mode 100644 src/util/undeployCommands.ts diff --git a/package.json b/package.json index f46ba47..e6a3a6a 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,10 @@ "compile": "npx tsc", "target": "node ./target/discord-bot.js", "start:dev": "yarn run compile && yarn run target", + "start:dev:no-deploy": "cross-env SKIP_COMMAND_DEPLOY=true yarn run start:dev", "start:prod": "yarn compile && pm2 start ./target/discord-bot.js --name poixpixel-discord-bot", "restart": "pm2 restart poixpixel-discord-bot", + "undeploy-commands": "yarn compile && node --experimental-specifier-resolution=node ./target/util/undeployCommands.js", "lint": "npx eslint ./src && npx tsc --noEmit", "format": "prettier --check --ignore-path .prettierignore .", "format:fix": "prettier --write --ignore-path .prettierignore .", @@ -34,6 +36,7 @@ "@types/pg": "^8.11.13", "@typescript-eslint/eslint-plugin": "^8.30.1", "@typescript-eslint/parser": "^8.30.1", + "cross-env": "^7.0.3", "drizzle-kit": "^0.31.0", "eslint": "^9.24.0", "eslint-config-prettier": "^10.1.2", diff --git a/src/structures/ExtendedClient.ts b/src/structures/ExtendedClient.ts index 6cf3d9d..aa28e65 100644 --- a/src/structures/ExtendedClient.ts +++ b/src/structures/ExtendedClient.ts @@ -1,7 +1,7 @@ import { Client, ClientOptions, Collection } from 'discord.js'; import { Command } from '@/types/CommandTypes.js'; import { Config } from '@/types/ConfigTypes.js'; -import { deployCommands } from '@/util/deployCommand.js'; +import { deployCommands, getFilesRecursively } from '@/util/deployCommand.js'; import { registerEvents } from '@/util/eventLoader.js'; /** @@ -29,20 +29,69 @@ export class ExtendedClient extends Client { private async loadModules() { try { - const commands = await deployCommands(); - if (!commands?.length) { - throw new Error('No commands found'); - } + if (process.env.SKIP_COMMAND_DEPLOY === 'true') { + console.log('Skipping command deployment (SKIP_COMMAND_DEPLOY=true)'); + const commandFiles = await this.loadCommandsWithoutDeploying(); - for (const command of commands) { - this.commands.set(command.data.name, command); - } + await registerEvents(this); + console.log( + `Loaded ${commandFiles.length} commands and registered events (without deployment)`, + ); + } else { + const commands = await deployCommands(); + if (!commands?.length) { + throw new Error('No commands found'); + } - await registerEvents(this); - console.log(`Loaded ${commands.length} commands and registered events`); + for (const command of commands) { + this.commands.set(command.data.name, command); + } + + await registerEvents(this); + console.log(`Loaded ${commands.length} commands and registered events`); + } } catch (error) { console.error('Error loading modules:', error); process.exit(1); } } + + /** + * Loads commands without deploying them to Discord + * @returns Array of command objects + */ + private async loadCommandsWithoutDeploying(): Promise { + try { + const path = await import('path'); + + const __dirname = path.resolve(); + const commandsPath = path.join(__dirname, 'target', 'commands'); + + const commandFiles = getFilesRecursively(commandsPath); + + const commands: Command[] = []; + for (const file of commandFiles) { + const commandModule = await import(`file://${file}`); + const command = commandModule.default; + + if ( + command instanceof Object && + 'data' in command && + 'execute' in command + ) { + commands.push(command); + this.commands.set(command.data.name, command); + } else { + console.warn( + `[WARNING] The command at ${file} is missing a required "data" or "execute" property.`, + ); + } + } + + return commands; + } catch (error) { + console.error('Error loading commands:', error); + throw error; + } + } } diff --git a/src/util/deployCommand.ts b/src/util/deployCommand.ts index c7e4486..31d9f88 100644 --- a/src/util/deployCommand.ts +++ b/src/util/deployCommand.ts @@ -17,7 +17,7 @@ const rest = new REST({ version: '10' }).setToken(token); * @param directory - The directory to get files from * @returns - An array of file paths */ -const getFilesRecursively = (directory: string): string[] => { +export const getFilesRecursively = (directory: string): string[] => { const files: string[] = []; const filesInDirectory = fs.readdirSync(directory); diff --git a/src/util/undeployCommands.ts b/src/util/undeployCommands.ts new file mode 100644 index 0000000..a80cf3b --- /dev/null +++ b/src/util/undeployCommands.ts @@ -0,0 +1,36 @@ +import { REST, Routes } from 'discord.js'; +import { loadConfig } from './configLoader.js'; + +const config = loadConfig(); +const { token, clientId, guildId } = config; + +const rest = new REST({ version: '10' }).setToken(token); + +/** + * Undeploys all commands from the Discord API + */ +export const undeployCommands = async () => { + try { + console.log('Undeploying all commands from the Discord API...'); + + await rest.put(Routes.applicationGuildCommands(clientId, guildId), { + body: [], + }); + + console.log('Successfully undeployed all commands'); + } catch (error) { + console.error('Error undeploying commands:', error); + throw error; + } +}; + +if (import.meta.url.endsWith(process.argv[1].replace(/\\/g, '/'))) { + undeployCommands() + .then(() => { + console.log('Undeploy process completed successfully'); + }) + .catch((err) => { + console.error('Undeploy process failed:', err); + process.exitCode = 1; + }); +} diff --git a/yarn.lock b/yarn.lock index 4328c0d..2fb7898 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1882,7 +1882,19 @@ __metadata: languageName: node linkType: hard -"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.6": +"cross-env@npm:^7.0.3": + version: 7.0.3 + resolution: "cross-env@npm:7.0.3" + dependencies: + cross-spawn: "npm:^7.0.1" + bin: + cross-env: src/bin/cross-env.js + cross-env-shell: src/bin/cross-env-shell.js + checksum: 10c0/f3765c25746c69fcca369655c442c6c886e54ccf3ab8c16847d5ad0e91e2f337d36eedc6599c1227904bf2a228d721e690324446876115bc8e7b32a866735ecf + languageName: node + linkType: hard + +"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.1, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.6": version: 7.0.6 resolution: "cross-spawn@npm:7.0.6" dependencies: @@ -4201,6 +4213,7 @@ __metadata: "@types/pg": "npm:^8.11.13" "@typescript-eslint/eslint-plugin": "npm:^8.30.1" "@typescript-eslint/parser": "npm:^8.30.1" + cross-env: "npm:^7.0.3" discord.js: "npm:^14.18.0" drizzle-kit: "npm:^0.31.0" drizzle-orm: "npm:^0.42.0"