diff --git a/example/shards.js b/example/shards.js index a62ab8a5..d5023f0b 100644 --- a/example/shards.js +++ b/example/shards.js @@ -10,9 +10,9 @@ command.name('').description('The options below are passed to reciple cli shards process.chdir(cli.cwd); -loadEnv({ path: cli.options.env }); +loadEnv({ path: cli.flags.env }); -const config = (await ConfigReader.readConfigJS(cli.options.config ?? 'reciple.mjs')).config; +const config = (await ConfigReader.readConfigJS(cli.flags.config ?? 'reciple.mjs')).config; const logsFolder = process.env.LOGS_FOLDER ?? path.join(process.cwd(), ((typeof config.logger?.logToFile !== 'function' ? config.logger?.logToFile.logsFolder : null) ?? 'logs')); const logger = await (await createLogger({ diff --git a/packages/reciple/src/bin.ts b/packages/reciple/src/bin.ts index e5722b10..02f559ff 100644 --- a/packages/reciple/src/bin.ts +++ b/packages/reciple/src/bin.ts @@ -2,41 +2,28 @@ import { ContextMenuCommandBuilder, Logger, MessageCommandBuilder, SlashCommandBuilder, buildVersion } from '@reciple/core'; import { createLogger, addEventListenersToClient } from './utils/logger.js'; -import { type ProcessInformation, RecipleClient, findModules } from './index.js'; -import { command, cli, cliVersion, updateChecker } from './utils/cli.js'; +import { RecipleClient, findModules, moduleFilesFilter } from './index.js'; +import { cli, cliVersion, updateChecker } from './utils/cli.js'; import { setTimeout as setTimeoutAsync } from 'node:timers/promises'; -import { existsAsync, resolveEnvProtocol } from '@reciple/utils'; -import { parentPort, threadId } from 'node:worker_threads'; +import { existsAsync } from '@reciple/utils'; +import { parentPort } from 'node:worker_threads'; import { ConfigReader } from './classes/Config.js'; import { config as loadEnv } from 'dotenv'; import { mkdir } from 'node:fs/promises'; import { kleur } from 'fallout-utility'; import path from 'node:path'; import semver from 'semver'; +import { CLI } from './classes/CLI.js'; -command.parse(); +await cli.parse(); if (!await existsAsync(cli.cwd)) await mkdir(cli.cwd, { recursive: true }); -if (cli.cwd !== cli.nodeCwd || parentPort === null) { - process.chdir(cli.cwd); - cli.isCwdUpdated = true; -} - -loadEnv({ path: cli.options.env }); - -let configPaths = [path.resolve('./reciple.mjs'), path.resolve('./reciple.js')]; +if ((cli.cwd !== cli.processCwd && !cli.isCwdUpdated) || parentPort === null) process.chdir(cli.cwd); -const configPath = path.resolve(cli.options.config ?? 'reciple.mjs'); -const isCustomPath = !configPaths.includes(configPath) || !!cli.options.config; - -if (!isCustomPath) { - configPaths = configPaths.filter(p => p !== configPath); - configPaths.unshift(configPath); -} else { - configPaths = [configPath]; -} +loadEnv({ path: cli.flags.env }); +const configPaths = ConfigReader.resolveConfigPaths(path.resolve(cli.flags.config ?? 'reciple.mjs')) const config = await ConfigReader.readConfigJS({ paths: configPaths }).then(c => c.config); const logger = config.logger instanceof Logger ? config.logger @@ -46,30 +33,25 @@ const logger = config.logger instanceof Logger global.logger = logger ?? undefined; -if (cli.options.setup) process.exit(0); +if (cli.flags.setup) process.exit(0); if (cli.shardmode) config.applicationCommandRegister = { ...config.applicationCommandRegister, enabled: false }; -if (cli.options.token) config.token = resolveEnvProtocol(cli.options.token) ?? config.token; + +config.token = cli.token ?? config.token; const processErrorHandler = (err: any) => logger?.error(err); process.once('uncaughtException', processErrorHandler); process.once('unhandledRejection', processErrorHandler); - process.on('warning', warn => logger?.warn(warn)); -if (cli.shardmode) { - const message: ProcessInformation = { type: 'ProcessInfo', pid: process.pid, threadId, log: cli.logPath }; - - if (parentPort) parentPort.postMessage(message); - if (process.send) process.send(message); -} +if (cli.shardmode) await cli.sendShardProcessInfo(); if (config.version && !semver.satisfies(cliVersion, config.version)) { logger?.error(`Your config version doesn't support Reciple CLI v${cliVersion}`); process.exit(1); } -logger?.info(`Starting Reciple client v${buildVersion} - ${new Date()}`); +logger?.info(`Starting Reciple client v${buildVersion} - ${new Date().toISOString()}`); const client = new RecipleClient(config); global.reciple = client; @@ -78,10 +60,8 @@ client.setLogger(logger); addEventListenersToClient(client); -const moduleFilesFilter = (file: string) => file.endsWith('.js') || file.endsWith('.cjs') || file.endsWith('.mjs'); - const modules = await client.modules.resolveModuleFiles({ - files: await findModules(config.modules, (f) => moduleFilesFilter(f)), + files: await findModules(config.modules, moduleFilesFilter), disableVersionCheck: config.modules?.disableModuleVersionCheck }); @@ -125,14 +105,7 @@ client.once('ready', async () => { process.stdin.resume(); - process.once('SIGHUP', unloadModulesAndStopProcess); - process.once('SIGINT', unloadModulesAndStopProcess); - process.once('SIGQUIT', unloadModulesAndStopProcess); - process.once('SIGABRT',unloadModulesAndStopProcess); - process.once('SIGALRM', unloadModulesAndStopProcess); - process.once('SIGTERM', unloadModulesAndStopProcess); - process.once('SIGBREAK', unloadModulesAndStopProcess); - process.once('SIGUSR2', unloadModulesAndStopProcess); + CLI.addExitListener(unloadModulesAndStopProcess); client.on('interactionCreate', interaction => { if (interaction.isContextMenuCommand()) { diff --git a/packages/reciple/src/classes/CLI.ts b/packages/reciple/src/classes/CLI.ts new file mode 100644 index 00000000..8adda10f --- /dev/null +++ b/packages/reciple/src/classes/CLI.ts @@ -0,0 +1,158 @@ +import { buildVersion } from '@reciple/core'; +import { resolveEnvProtocol } from '@reciple/utils'; +import { Command } from 'commander'; +import type { PackageJson } from 'fallout-utility'; +import path from 'path'; +import { isMainThread, parentPort, threadId } from 'worker_threads'; +import type { ProcessInformation } from '../exports.js'; + +export interface CLIOptions { + packageJSON: PackageJson; + binPath: string; + logPath?: string; + cwd?: string; +} + +export interface CLIFlags { + version?: string; + token?: string; + config?: string; + debugmode?: boolean; + yes?: boolean; + env?: string; + shardmode?: boolean; + setup?: boolean; + [k: string]: any; +} + +export class CLI { + public packageJSON: PackageJson; + public processCwd: string; + public commander: Command; + public binPath: string; + public logPath?: string; + + get name() { + return this.packageJSON.name!; + } + + get description() { + return this.packageJSON.description!; + } + + get version() { + return this.packageJSON.version!; + } + + get args() { + return this.commander.args; + } + + get flags() { + return this.commander.opts(); + } + + /** + * @deprecated Use `.flags` instead + */ + get options() { + return this.flags; + } + + get cwd() { + return this.args[0] + ? path.isAbsolute(this.args[0]) ? this.args[0] : path.join(this.processCwd, this.args[0]) + : process.cwd(); + } + + get shardmode() { + return !!(this.options.shardmode ?? process.env.SHARDMODE); + } + + get threadId() { + return !isMainThread && parentPort !== undefined ? threadId : undefined; + } + + get isCwdUpdated() { + return process.cwd() !== this.processCwd; + } + + /** + * @deprecated Use `.processCwd` instead + */ + get nodeCwd() { + return this.processCwd; + } + + get token() { + return (this.flags.token && resolveEnvProtocol(this.flags.token)) || null; + } + + constructor(options: CLIOptions) { + this.packageJSON = options.packageJSON; + this.processCwd = options.cwd ?? process.cwd(); + this.commander = new Command(); + this.binPath = options.binPath; + this.logPath = options.logPath; + + this.commander + .name(this.name) + .description(this.description) + .version(`reciple: ${this.version}\n@reciple/client: ${buildVersion}`, '-v, --version') + .argument('[cwd]', 'Change the current working directory') + .option('-t, --token ', 'Replace used bot token') + .option('-c, --config ', 'Set path to a config file') + .option('-D, --debugmode', 'Enable debug mode') + .option('-y, --yes', 'Agree to all Reciple confirmation prompts') + .option('--env ', '.env file location') + .option('--shardmode', 'Modifies some functionalities to support sharding') + .option('--setup', 'Create required config without starting the bot') + .allowUnknownOption(true); + } + + public async parse(): Promise { + await this.commander.parseAsync(); + + return this; + } + + public async sendShardProcessInfo(): Promise { + const message: ProcessInformation = { type: 'ProcessInfo', pid: process.pid, threadId, log: cli.logPath }; + + if (parentPort) parentPort.postMessage(message); + if (process.send) process.send(message); + } + + public static addExitListener(listener: (signal: NodeJS.Signals) => any, once?: boolean): void { + if (!once) { + process.on('SIGHUP', listener); + process.on('SIGINT', listener); + process.on('SIGQUIT', listener); + process.on('SIGABRT', listener); + process.on('SIGALRM', listener); + process.on('SIGTERM', listener); + process.on('SIGBREAK', listener); + process.on('SIGUSR2', listener); + } else { + process.once('SIGHUP', listener); + process.once('SIGINT', listener); + process.once('SIGQUIT', listener); + process.once('SIGABRT', listener); + process.once('SIGALRM', listener); + process.once('SIGTERM', listener); + process.once('SIGBREAK', listener); + process.once('SIGUSR2', listener); + } + } + + public static removeExitListener(listener: (signal: NodeJS.Signals) => any): void { + process.removeListener('SIGHUP', listener); + process.removeListener('SIGINT', listener); + process.removeListener('SIGQUIT', listener); + process.removeListener('SIGABRT', listener); + process.removeListener('SIGALRM', listener); + process.removeListener('SIGTERM', listener); + process.removeListener('SIGBREAK', listener); + process.removeListener('SIGUSR2', listener); + } +} diff --git a/packages/reciple/src/classes/Config.ts b/packages/reciple/src/classes/Config.ts index e3bfc3ab..d864900e 100644 --- a/packages/reciple/src/classes/Config.ts +++ b/packages/reciple/src/classes/Config.ts @@ -113,4 +113,19 @@ export class ConfigReader { return file; } + + public static resolveConfigPaths(configPath: string): string[] { + let configPaths = [path.resolve('./reciple.mjs'), path.resolve('./reciple.js')]; + + const isCustomPath = !configPaths.includes(configPath) || !!cli.flags.config; + + if (!isCustomPath) { + configPaths = configPaths.filter(p => p !== configPath); + configPaths.unshift(configPath); + } else { + configPaths = [configPath]; + } + + return configPaths; + } } diff --git a/packages/reciple/src/exports.ts b/packages/reciple/src/exports.ts index 65db1b75..8762a83f 100644 --- a/packages/reciple/src/exports.ts +++ b/packages/reciple/src/exports.ts @@ -1,8 +1,9 @@ +export * from './classes/CLI.js'; export * from './classes/Config.js'; -export * from './utils/modules.js'; -export * from './utils/logger.js'; export * from './utils/cli.js'; +export * from './utils/logger.js'; +export * from './utils/modules.js'; export interface ProcessInformation { type: 'ProcessInfo'; diff --git a/packages/reciple/src/utils/cli.ts b/packages/reciple/src/utils/cli.ts index b1d70f18..0de0ab6b 100644 --- a/packages/reciple/src/utils/cli.ts +++ b/packages/reciple/src/utils/cli.ts @@ -1,93 +1,23 @@ -import { isMainThread, parentPort, threadId } from 'node:worker_threads'; import { PackageUpdateChecker } from '@reciple/utils'; import { buildVersion } from '@reciple/core'; import { fileURLToPath } from 'node:url'; -import { readFileSync } from 'node:fs'; -import { Command } from 'commander'; import { coerce } from 'semver'; import path from 'node:path'; +import { CLI } from '../classes/CLI.js'; +import { readFile } from 'node:fs/promises'; -const { version, description } = JSON.parse(readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), '../../package.json'), 'utf-8')); -const originalCwd = process.cwd(); - -export let command = new Command() - .name('reciple') - .description(description) - .version(`Reciple CLI: ${version}\nReciple Client: ${buildVersion}`, '-v, --version') - .argument('[cwd]', 'Change the current working directory') - .option('-t, --token ', 'Replace used bot token') - .option('-c, --config ', 'Set path to a config file') - .option('-D, --debugmode', 'Enable debug mode') - .option('-y, --yes', 'Agree to all Reciple confirmation prompts') - .option('--env ', '.env file location') - .option('--shardmode', 'Modifies some functionalities to support sharding') - .option('--setup', 'Create required config without starting the bot') - .allowUnknownOption(true); - -export interface CLIOptions { - version?: string; - token?: string; - config?: string; - debugmode?: boolean; - yes?: boolean; - env?: string; - shardmode?: boolean; - setup?: boolean; - [k: string]: any; -} +export const cli = new CLI({ + packageJSON: JSON.parse(await readFile(path.join(path.dirname(fileURLToPath(import.meta.url)), '../../package.json'), 'utf-8')), + binPath: path.join(path.dirname(fileURLToPath(import.meta.url)), '../bin.js'), + cwd: process.cwd(), +}); -export const cli = { - /** - * This property returns the command-line arguments passed to the CLI. It is an array of strings, where each string represents an argument. - */ - get args() { return command.args; }, - /** - * This property returns the command-line options passed to the CLI. It is an object with keys and values depending on the options specified in the command-line arguments. - */ - get options() { return command.opts(); }, - /** - * This property returns the current working directory (CWD) of the CLI. It takes into account the command-line arguments passed to the CLI. - */ - get cwd() { - return this.args[0] - ? path.isAbsolute(this.args[0]) ? this.args[0] : path.join(originalCwd, this.args[0]) - : process.cwd(); - }, - /** - * This property returns a boolean value indicating whether shard mode is enabled or not. - * It is enabled if the shardmode option is specified in the command-line arguments, or if the `SHARDMODE` environment variable is set to `1`. Otherwise, it is disabled. - */ - get shardmode() { return !!(this.options.shardmode ?? process.env.SHARDMODE) }, - /** - * This property returns the thread ID of the current thread, if it is not the main thread. - */ - threadId: !isMainThread && parentPort !== undefined ? threadId : undefined, - /** - * This property is used to store a boolean value indicating whether the CWD has been updated or not. - */ - isCwdUpdated: false, - /** - * This property returns the current working directory of the Node.js process. - */ - nodeCwd: process.cwd(), - /** - * This property returns the path of the bin.mjs file, which is the main entry point of the CLI. - */ - binPath: path.join(path.dirname(fileURLToPath(import.meta.url)), '../bin.mjs'), - /** - * Reciple package root directory - */ - reciplePackagePath: path.join(path.dirname(fileURLToPath(import.meta.url)), '../../'), - /** - * This property is used to store the path of the log file. - */ - logPath: undefined as string|undefined -}; +export const command = cli.commander; global.cli = cli; -export const cliVersion = `${coerce(version)}`; -export const cliBuildVersion = version; +export const cliVersion = `${coerce(cli.version)}`; +export const cliBuildVersion = cli.version; export const updateChecker = new PackageUpdateChecker({ packages: [ { package: 'reciple', currentVersion: cliBuildVersion }, diff --git a/packages/reciple/src/utils/modules.ts b/packages/reciple/src/utils/modules.ts index 8842dc31..e3f3635c 100644 --- a/packages/reciple/src/utils/modules.ts +++ b/packages/reciple/src/utils/modules.ts @@ -52,3 +52,5 @@ export async function findModules(config: RecipleConfig['modules'], filter?: (fi return modules; } + +export const moduleFilesFilter = (file: string) => file.endsWith('.js') || file.endsWith('.cjs') || file.endsWith('.mjs');