vimcord 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # vimcord
2
+
3
+ **Vimcord** (pronounced as in _vhem-cord_) is a lightweight, opinionated framework for **Discord.js**. Built for developers who want to reduce the distance between an idea and a working command without fighting with the library.
4
+
5
+ ## 🚀 Installation
6
+
7
+ ```bash
8
+ npm install vimcord discord.js
9
+ # or
10
+ pnpm add vimcord discord.js
11
+ # or
12
+ yarn add vimcord discord.js
13
+ ```
14
+
15
+ ## 🛠 Quick Start
16
+
17
+ Vimcord uses a `createClient` factory to initialize a pre-configured environment with optional feature flags.
18
+
19
+ ```ts
20
+ import { ActivityType, ClientOptions, GatewayIntentBits } from "discord.js";
21
+ import { createClient, GatewayIntentBits, MongoDatabase, StatusType } from "vimcord";
22
+
23
+ const client = createClient(
24
+ {
25
+ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages]
26
+ },
27
+ {
28
+ useGlobalErrorHandlers: true,
29
+ useDefaultSlashCommandHandler: true,
30
+ useDefaultPrefixCommandHandler: true,
31
+ useDefaultContextCommandHandler: true,
32
+
33
+ // Automatically import the .env using dotEnv
34
+ useEnv: true,
35
+
36
+ // Automatically import event and command modules
37
+ importModules: {
38
+ events: "./events",
39
+ slashCommands: "./commands/slash",
40
+ prefixCommands: "./commands/prefix"
41
+ contextCommands: "./commands/context"
42
+ }
43
+ }
44
+ );
45
+
46
+ client.start(async client => {
47
+ // NOTE: This runs *BEFORE* the client logs in
48
+
49
+ // Use Mongo as our database
50
+ await client.useDatabase(new MongoDatabase(client));
51
+ })
52
+ ```
53
+
54
+ ## 🛠 Features
55
+
56
+ - **CommandManager**: Advanced managers for **Slash**, **Prefix**, and **Context** commands. Includes deep inference for permissions and automated error boundaries.
57
+ - **DatabaseManager**: Seamless **MongoDB/Mongoose** integration via `createMongoSchema` and `useMongoDatabase`.
58
+ - **DiscordTools**: High-level interaction helpers including the **Paginator**, **Prompt**, and **BetterModal** for stateful UI.
59
+ - **DiscordUtils**: Essential Discord-first utilities like `dynaSend`, `fetchMember`, and `isMentionOrSnowflake`.
60
+ - **Logger**: A fun, retro style console logger for clear, actionable terminal output.
61
+
62
+ ## 💎 Useful Utilities
63
+
64
+ ### **The BetterEmbed & ACF**
65
+
66
+ Stop writing boilerplate for user mentions and avatars. `BetterEmbed` supports **Shorthand Context Formatting (ACF)**, allowing you to use tokens like `$USER` and `$BOT_AVATAR` directly in strings. Vimcord resolves them at runtime.
67
+
68
+ ### **The Paginator (Chapters & Nested Pages)**
69
+
70
+ Multi-page embeds shouldn't be a chore. The `Paginator` supports **Chapters**, enabling complex, nested page structures with built-in navigation logic and select menu support.
71
+
72
+ ### **The DynaSend: Smart Dispatch**
73
+
74
+ `DynaSend` is an intelligent message delivery system. It automatically detects the interaction state to decide whether to `.reply()`, `.editReply()`, or `.followUp()`. You write one line; Vimcord handles the logic.
75
+
76
+ ### **MongoSchemaBuilder: Resilient DB Ops**
77
+
78
+ The `execute` method on your schemas includes built-in **exponential backoff**. If the database is under load or reconnecting, Vimcord retries the operation instead of crashing your command.
79
+
80
+ ## 👀 Mentionables
81
+
82
+ - **Strictly Typed**: Full TypeScript support with deep inference for command permissions, events, and database documents.
83
+ - **Feature-Rich**: Includes a built-in CLI, automated status rotation, and sophisticated error boundaries.
84
+ - **Developer-Centric**: Focused on performance and ergonomics.
85
+
86
+ ## 👋 About
87
+
88
+ _Vimcord was created by **xsqu1znt**,_ and designed for developers who value performance and aesthetic code.
89
+
90
+ It also takes advantage of my lightweight library [qznt](https://github.com/xsqu1znt/qznt) for a smooth experience.
91
+
92
+ ## **_Vimcord is still a work in progress. If you find any bugs, please let me know._**
93
+
94
+ License: MIT
package/dist/index.cjs CHANGED
@@ -525,8 +525,8 @@ var ContextCommandBuilder = class extends BaseCommandBuilder {
525
525
  };
526
526
 
527
527
  // src/utils/dir.ts
528
- var import_jstools = __toESM(require("jstools"));
529
528
  var import_node_path = __toESM(require("path"));
529
+ var import_qznt = require("qznt");
530
530
  function getProcessDir() {
531
531
  const mainPath = process.argv[1];
532
532
  if (!mainPath) return "";
@@ -536,7 +536,7 @@ async function importModulesFromDir(dir, fnPrefix) {
536
536
  const cwd = getProcessDir();
537
537
  const MODULE_RELATIVE_PATH = import_node_path.default.join(cwd, dir);
538
538
  const MODULE_LOG_PATH = dir;
539
- const files = import_jstools.default.readDir(MODULE_RELATIVE_PATH, { recursive: true }).filter(
539
+ const files = import_qznt.$.fs.readDir(MODULE_RELATIVE_PATH).filter(
540
540
  (fn) => fn.endsWith(`${fnPrefix ? `.${fnPrefix}` : ""}.js`) || fn.endsWith(`${fnPrefix ? `.${fnPrefix}` : ""}.ts`)
541
541
  );
542
542
  if (!files.length) {
@@ -1341,28 +1341,9 @@ function formatThousands(num, sep = ",") {
1341
1341
  return `${num}`.replace(/\B(?<!\.\d*)(?=(\d{3})+(?!\d))/g, sep);
1342
1342
  }
1343
1343
 
1344
- // src/utils/random.ts
1345
- function pickRandom(arr, options) {
1346
- const _rnd = () => {
1347
- return arr[Math.floor(Math.random() * arr.length)];
1348
- };
1349
- let att = 0;
1350
- let candidate = _rnd();
1351
- if (options?.notEqualTo !== void 0 && arr.length > 1) {
1352
- while (candidate === options.notEqualTo) {
1353
- if (att < (options?.maxRerollAttempts ?? 100)) {
1354
- throw new Error(`pickRandom reached max reroll attempts (${options?.maxRerollAttempts ?? 100})`);
1355
- }
1356
- candidate = _rnd();
1357
- att++;
1358
- }
1359
- }
1360
- return options?.clone ? structuredClone(candidate) : candidate;
1361
- }
1362
-
1363
1344
  // src/modules/status.manager.ts
1364
- var import_node_cron = __toESM(require("node-cron"));
1365
1345
  var import_node_events = __toESM(require("events"));
1346
+ var import_qznt2 = require("qznt");
1366
1347
  var VimcordStatusManager = class {
1367
1348
  client;
1368
1349
  logger;
@@ -1383,24 +1364,9 @@ var VimcordStatusManager = class {
1383
1364
  this.logger.debug("Status cleared");
1384
1365
  }
1385
1366
  });
1386
- this.emitter.on("rotation", () => {
1387
- if (this.client.config.app.verbose) {
1388
- this.logger.debug("Status rotated");
1389
- }
1390
- });
1391
- this.emitter.on("rotationStarted", () => {
1392
- if (this.client.config.app.verbose) {
1393
- this.logger.debug("Status rotation resumed \u25B6\uFE0F");
1394
- }
1395
- });
1396
- this.emitter.on("rotationPaused", () => {
1397
- if (this.client.config.app.verbose) {
1398
- this.logger.debug("Status rotation paused \u23F8\uFE0F");
1399
- }
1400
- });
1401
1367
  }
1402
1368
  clearData() {
1403
- this.task?.destroy();
1369
+ this.task?.stop();
1404
1370
  this.task = null;
1405
1371
  this.lastActivity = null;
1406
1372
  this.lastActivityIndex = 0;
@@ -1433,11 +1399,8 @@ var VimcordStatusManager = class {
1433
1399
  }
1434
1400
  async statusRotationTask(clientStatus) {
1435
1401
  let activity;
1436
- if (clientStatus.randomize) {
1437
- activity = pickRandom(clientStatus.activity, {
1438
- notEqualTo: this.lastActivity,
1439
- clone: true
1440
- });
1402
+ if (clientStatus.randomize && Array.isArray(clientStatus.activity)) {
1403
+ activity = import_qznt2.$.rnd.choice(clientStatus.activity, { not: this.lastActivity });
1441
1404
  this.lastActivity = activity;
1442
1405
  } else {
1443
1406
  const activityIndex = (this.lastActivityIndex + 1) % clientStatus.activity.length;
@@ -1449,34 +1412,22 @@ var VimcordStatusManager = class {
1449
1412
  }
1450
1413
  async scheduleStatusRotation(clientStatus) {
1451
1414
  if (!clientStatus.interval) throw new Error("Cannot create client activity interval without interval time");
1452
- this.task?.destroy();
1415
+ this.task?.stop();
1453
1416
  this.task = null;
1454
- await this.statusRotationTask(clientStatus);
1455
- this.task = import_node_cron.default.createTask(
1456
- `*/${clientStatus.interval} * * * * *`,
1457
- async () => await this.statusRotationTask(clientStatus),
1458
- { noOverlap: true }
1459
- );
1417
+ this.task = new import_qznt2.$.Loop(() => this.statusRotationTask(clientStatus), import_qznt2.$.math.ms(clientStatus.interval), true);
1460
1418
  this.start();
1461
- this.emitter.emit("rotationStarted", this.task);
1462
1419
  }
1463
1420
  start() {
1464
1421
  if (this.task) {
1465
1422
  this.task.start();
1466
- this.emitter.emit("rotationStarted", this.task);
1467
- if (this.client.config.app.verbose) {
1468
- this.logger.debug("Status rotation started");
1469
- }
1423
+ this.emitter.emit("started", this.task);
1470
1424
  }
1471
1425
  return this;
1472
1426
  }
1473
1427
  pause() {
1474
1428
  if (this.task) {
1475
1429
  this.task.stop();
1476
- this.emitter.emit("rotationPaused", this.task);
1477
- if (this.client.config.app.verbose) {
1478
- this.logger.debug("Status rotation paused");
1479
- }
1430
+ this.emitter.emit("paused", this.task);
1480
1431
  }
1481
1432
  return this;
1482
1433
  }
@@ -1490,21 +1441,16 @@ var VimcordStatusManager = class {
1490
1441
  }
1491
1442
  if (!clientStatus.interval) {
1492
1443
  await this.setActivity(Array.isArray(clientStatus.activity) ? clientStatus.activity[0] : clientStatus.activity);
1493
- return this;
1494
- }
1495
- if (clientStatus.interval) {
1444
+ } else {
1496
1445
  await this.scheduleStatusRotation(clientStatus);
1497
1446
  }
1498
1447
  return this;
1499
1448
  }
1500
1449
  async destroy() {
1501
1450
  if (this.task) {
1502
- this.task.destroy();
1451
+ this.task.stop();
1503
1452
  this.task = null;
1504
- this.emitter.emit("rotationDestroyed");
1505
- if (this.client.config.app.verbose) {
1506
- this.logger.debug("Status rotation destroyed");
1507
- }
1453
+ this.emitter.emit("destroyed");
1508
1454
  await this.clear();
1509
1455
  }
1510
1456
  return this;
@@ -1514,9 +1460,6 @@ var VimcordStatusManager = class {
1514
1460
  this.clearData();
1515
1461
  client.user.setActivity({ name: "" });
1516
1462
  this.emitter.emit("cleared");
1517
- if (this.client.config.app.verbose) {
1518
- this.logger.debug("Status cleared");
1519
- }
1520
1463
  return this;
1521
1464
  }
1522
1465
  };
@@ -3046,7 +2989,7 @@ var BetterCollector = class _BetterCollector {
3046
2989
  if (shouldBeDeferred.update) {
3047
2990
  await interaction.deferUpdate().catch(Boolean);
3048
2991
  } else {
3049
- await interaction.deferReply({ ephemeral: shouldBeDeferred.ephemeral }).catch(Boolean);
2992
+ await interaction.deferReply({ flags: shouldBeDeferred.ephemeral ? "Ephemeral" : void 0 }).catch(Boolean);
3050
2993
  }
3051
2994
  } else {
3052
2995
  await interaction.deferReply().catch(Boolean);
@@ -3057,7 +3000,7 @@ var BetterCollector = class _BetterCollector {
3057
3000
  try {
3058
3001
  const isAllowed = await this.validateParticipant(interaction, listener.options?.participants);
3059
3002
  if (!isAllowed) return;
3060
- await listener.fn(interaction);
3003
+ await listener.fn(interaction).finally(() => listener.options?.finally?.(interaction));
3061
3004
  } catch (err) {
3062
3005
  this.handleListenerError(err);
3063
3006
  }
@@ -3067,7 +3010,7 @@ var BetterCollector = class _BetterCollector {
3067
3010
  allListeners.map((l) => {
3068
3011
  const isAllowed = this.validateParticipant(interaction, l.options?.participants);
3069
3012
  if (!isAllowed) return;
3070
- return l.fn(interaction).catch(this.handleListenerError);
3013
+ return l.fn(interaction).catch(this.handleListenerError).finally(() => l.options?.finally?.(interaction));
3071
3014
  })
3072
3015
  );
3073
3016
  }
@@ -3917,6 +3860,7 @@ var Prompt = class {
3917
3860
  content;
3918
3861
  embed;
3919
3862
  container;
3863
+ textOnly;
3920
3864
  buttons;
3921
3865
  customButtons;
3922
3866
  onResolve;
@@ -3930,6 +3874,7 @@ var Prompt = class {
3930
3874
  this.content = options.content;
3931
3875
  this.embed = options.embed ?? this.createDefaultForm();
3932
3876
  this.container = options?.container;
3877
+ this.textOnly = options.textOnly;
3933
3878
  this.buttons = this.createButtons(options.buttons);
3934
3879
  this.customButtons = this.createCustomButtons(options.customButtons);
3935
3880
  this.onResolve = options.onResolve ?? [3 /* DeleteOnConfirm */, 4 /* DeleteOnReject */];
@@ -4009,7 +3954,7 @@ var Prompt = class {
4009
3954
  }
4010
3955
  buildSendOptions(options) {
4011
3956
  const sendData = { ...options };
4012
- if (this.container) {
3957
+ if (!this.textOnly && this.container) {
4013
3958
  sendData.components = Array.isArray(sendData.components) ? [...sendData.components, this.container] : [this.container];
4014
3959
  const existingFlags = sendData.flags ? Array.isArray(sendData.flags) ? sendData.flags : [sendData.flags] : [];
4015
3960
  if (!existingFlags.includes("IsComponentsV2")) {
@@ -4018,7 +3963,9 @@ var Prompt = class {
4018
3963
  sendData.flags = existingFlags;
4019
3964
  }
4020
3965
  } else {
4021
- sendData.embeds = Array.isArray(sendData.embeds) ? [this.embed, ...sendData.embeds] : [this.embed];
3966
+ if (!this.textOnly) {
3967
+ sendData.embeds = Array.isArray(sendData.embeds) ? [this.embed, ...sendData.embeds] : [this.embed];
3968
+ }
4022
3969
  }
4023
3970
  if (this.content) {
4024
3971
  sendData.content = this.content;
@@ -4104,7 +4051,7 @@ async function prompt(handler, options, sendOptions) {
4104
4051
 
4105
4052
  // src/utils/VimcordCLI.ts
4106
4053
  var import_node_readline = require("readline");
4107
- var import_jstools2 = __toESM(require("jstools"));
4054
+ var import_qznt3 = require("qznt");
4108
4055
  var VimcordCLI = class {
4109
4056
  rl;
4110
4057
  options;
@@ -4143,7 +4090,7 @@ var VimcordCLI = class {
4143
4090
  return { isCommand: true, commandName, content: args.join(" "), args };
4144
4091
  }
4145
4092
  getClientInstance(line) {
4146
- const clientIndex = import_jstools2.default.getFlagSubstring(line, "--client", 1) || import_jstools2.default.getFlagSubstring(line, "-c", 1);
4093
+ const clientIndex = import_qznt3.$.str.getFlag(line, "--client", 1) || import_qznt3.$.str.getFlag(line, "-c", 1);
4147
4094
  if (clientIndex) {
4148
4095
  const idx = Number(clientIndex);
4149
4096
  if (isNaN(idx)) {
@@ -4193,7 +4140,7 @@ CLI.addCommand("register", "Register app commands (slash & context) globally, or
4193
4140
  if (!["guild", "global"].includes(mode)) {
4194
4141
  return CLI.logger.error(`'${mode}' is not a valid option. Your options are [guild|global]`);
4195
4142
  }
4196
- let guildIds = (import_jstools2.default.getFlagSubstring(content, "--guilds", 1) || import_jstools2.default.getFlagSubstring(content, "-g", 1) || "").replaceAll(/["']/g, "").split(" ").filter(Boolean).map((s) => s.replaceAll(",", "").trim());
4143
+ let guildIds = (import_qznt3.$.str.getFlag(content, "--guilds", 1) || import_qznt3.$.str.getFlag(content, "-g", 1) || "").replaceAll(/["']/g, "").split(" ").filter(Boolean).map((s) => s.replaceAll(",", "").trim());
4197
4144
  if (!guildIds.length) guildIds = client.guilds.cache.map((g) => g.id);
4198
4145
  switch (mode) {
4199
4146
  case "guild":
@@ -4215,7 +4162,7 @@ CLI.addCommand("unregister", "Unregister app commands globally, or per guild", a
4215
4162
  if (!["guild", "global"].includes(mode)) {
4216
4163
  return CLI.logger.error(`'${mode}' is not a valid option. Your options are [guild|global]`);
4217
4164
  }
4218
- let guildIds = (import_jstools2.default.getFlagSubstring(content, "--guilds", 1) || import_jstools2.default.getFlagSubstring(content, "-g", 1) || "").replaceAll(/["']/g, "").split(" ").filter(Boolean).map((s) => s.replaceAll(",", "").trim());
4165
+ let guildIds = (import_qznt3.$.str.getFlag(content, "--guilds", 1) || import_qznt3.$.str.getFlag(content, "-g", 1) || "").replaceAll(/["']/g, "").split(" ").filter(Boolean).map((s) => s.replaceAll(",", "").trim());
4219
4166
  if (!guildIds.length) guildIds = client.guilds.cache.map((g) => g.id);
4220
4167
  switch (mode) {
4221
4168
  case "guild":
@@ -4234,9 +4181,9 @@ CLI.addCommand("stats", "View statistics about a client instance", (args, conten
4234
4181
  const client = CLI.getClientInstance(content);
4235
4182
  if (!client) return;
4236
4183
  CLI.logger.table(`(stats) ~ ${client.config.app.name}`, {
4237
- "Guilds:": import_jstools2.default.formatThousands(client.guilds.cache.size),
4184
+ "Guilds:": import_qznt3.$.format.number(client.guilds.cache.size),
4238
4185
  "Ping:": `${client.ws.ping || 0}ms`,
4239
- "Uptime:": `${import_jstools2.default.msToSec(client.uptime || 0)}s`,
4186
+ "Uptime:": `${import_qznt3.$.math.secs(client.uptime || 0)}s`,
4240
4187
  "Process Uptime:": `${Math.floor(process.uptime())}s`,
4241
4188
  "Memory Usage:": `${(process.memoryUsage().rss / 1024 / 1024).toFixed(2)} MB`
4242
4189
  });
@@ -4253,7 +4200,7 @@ CLI.addCommand("cmds", "List the loaded commands", async (args, content) => {
4253
4200
  for (const cmd of commands) {
4254
4201
  tableData[`/${cmd.builder.name}`] = `~ ${cmd.builder.description || "No description"}`;
4255
4202
  }
4256
- return CLI.logger.table(`(cmds) ~ slash (${import_jstools2.default.formatThousands(commands.length)})`, tableData);
4203
+ return CLI.logger.table(`(cmds) ~ slash (${import_qznt3.$.format.number(commands.length)})`, tableData);
4257
4204
  }
4258
4205
  case "prefix": {
4259
4206
  const commands = Array.from(client.commands.prefix.commands.values());
@@ -4269,7 +4216,7 @@ CLI.addCommand("cmds", "List the loaded commands", async (args, content) => {
4269
4216
  const aliasIndicator = config.aliases?.length ? ` [${config.aliases.join(", ")}]` : "";
4270
4217
  tableData[`${defaultPrefix}${config.name}${aliasIndicator}`] = `~ ${config.description || "No description"}`;
4271
4218
  }
4272
- return CLI.logger.table(`(cmds) ~ prefix (${import_jstools2.default.formatThousands(commands.length)})`, tableData);
4219
+ return CLI.logger.table(`(cmds) ~ prefix (${import_qznt3.$.format.number(commands.length)})`, tableData);
4273
4220
  }
4274
4221
  case "ctx": {
4275
4222
  const commands = Array.from(client.commands.context.commands.values());
@@ -4279,7 +4226,7 @@ CLI.addCommand("cmds", "List the loaded commands", async (args, content) => {
4279
4226
  const type = cmd.builder.type === 2 ? "User" : "Msg";
4280
4227
  tableData[`[${type}] ${cmd.builder.name}`] = "";
4281
4228
  }
4282
- return CLI.logger.table(`(cmds) ~ ctx (${import_jstools2.default.formatThousands(commands.length)})`, tableData);
4229
+ return CLI.logger.table(`(cmds) ~ ctx (${import_qznt3.$.format.number(commands.length)})`, tableData);
4283
4230
  }
4284
4231
  default:
4285
4232
  return CLI.logger.error(`'${mode}' is not a valid option. Valid options: [slash|prefix|ctx]`);
@@ -4306,6 +4253,25 @@ function createClient(options, features = {}, config = {}) {
4306
4253
  function getClientInstances() {
4307
4254
  return clientInstances;
4308
4255
  }
4256
+
4257
+ // src/utils/random.ts
4258
+ function pickRandom(arr, options) {
4259
+ const _rnd = () => {
4260
+ return arr[Math.floor(Math.random() * arr.length)];
4261
+ };
4262
+ let att = 0;
4263
+ let candidate = _rnd();
4264
+ if (options?.notEqualTo !== void 0 && arr.length > 1) {
4265
+ while (candidate === options.notEqualTo) {
4266
+ if (att < (options?.maxRerollAttempts ?? 100)) {
4267
+ throw new Error(`pickRandom reached max reroll attempts (${options?.maxRerollAttempts ?? 100})`);
4268
+ }
4269
+ candidate = _rnd();
4270
+ att++;
4271
+ }
4272
+ }
4273
+ return options?.clone ? structuredClone(candidate) : candidate;
4274
+ }
4309
4275
  // Annotate the CommonJS export names for ESM import in node:
4310
4276
  0 && (module.exports = {
4311
4277
  BaseCommandBuilder,