@lunibee/builders 0.1.6 → 0.1.7

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/src/commands.ts CHANGED
@@ -1,519 +1,621 @@
1
- /** Discord application command option types. */
2
- export const ApplicationCommandOptionType = {
3
- Subcommand: 1,
4
- SubcommandGroup: 2,
5
- String: 3,
6
- Integer: 4,
7
- Boolean: 5,
8
- User: 6,
9
- Channel: 7,
10
- Role: 8,
11
- Mentionable: 9,
12
- Number: 10,
13
- Attachment: 11,
14
- } as const;
15
-
16
- /** Builds Discord application command payloads. */
17
- export class SlashCommandBuilder {
18
- readonly #data: Record<string, unknown> = { type: 1 };
19
- /** Sets the command name. */ public setName(name: string): this {
20
- validateName(name, "Command name");
21
- this.#data.name = name;
22
- return this;
23
- }
24
- /** Sets the command description. */ public setDescription(
25
- description: string,
26
- ): this {
27
- validateText(description, "Command description", 100);
28
- this.#data.description = description;
29
- return this;
30
- }
31
- /** Sets whether the command is available in direct messages. */ public setDMPermission(
32
- enabled: boolean,
33
- ): this {
34
- this.#data.dm_permission = enabled;
35
- return this;
36
- }
37
- /** Sets whether the command is NSFW. */
38
- public setNSFW(nsfw = true): this {
39
- this.#data.nsfw = nsfw;
40
- return this;
41
- }
42
- /** Sets whether the command is NSFW. */
43
- public setNsfw(nsfw = true): this {
44
- return this.setNSFW(nsfw);
45
- }
46
- /** Sets command default member permissions. */ public setDefaultMemberPermissions(
47
- permissions: bigint | number | string | null,
48
- ): this {
49
- this.#data.default_member_permissions =
50
- permissions === null ? null : BigInt(permissions).toString();
51
- return this;
52
- }
53
- /** Sets command integration types. */ public setIntegrationTypes(
54
- ...types: number[]
55
- ): this {
56
- this.#data.integration_types = [...types];
57
- return this;
58
- }
59
- /** Adds a string option. */ public addStringOption(
60
- configure: (option: StringOptionBuilder) => StringOptionBuilder,
61
- ): this {
62
- return this.addOption(configure(new StringOptionBuilder()));
63
- }
64
- /** Adds an integer option. */ public addIntegerOption(
65
- configure: (option: IntegerOptionBuilder) => IntegerOptionBuilder,
66
- ): this {
67
- return this.addOption(configure(new IntegerOptionBuilder()));
68
- }
69
- /** Adds a number option. */ public addNumberOption(
70
- configure: (option: NumberOptionBuilder) => NumberOptionBuilder,
71
- ): this {
72
- return this.addOption(configure(new NumberOptionBuilder()));
73
- }
74
- /** Adds a boolean option. */ public addBooleanOption(
75
- configure: (option: BooleanOptionBuilder) => BooleanOptionBuilder,
76
- ): this {
77
- return this.addOption(configure(new BooleanOptionBuilder()));
78
- }
79
- /** Adds a user option. */ public addUserOption(
80
- configure: (option: UserOptionBuilder) => UserOptionBuilder,
81
- ): this {
82
- return this.addOption(configure(new UserOptionBuilder()));
83
- }
84
- /** Adds a channel option. */ public addChannelOption(
85
- configure: (option: ChannelOptionBuilder) => ChannelOptionBuilder,
86
- ): this {
87
- return this.addOption(configure(new ChannelOptionBuilder()));
88
- }
89
- /** Adds a role option. */ public addRoleOption(
90
- configure: (option: RoleOptionBuilder) => RoleOptionBuilder,
91
- ): this {
92
- return this.addOption(configure(new RoleOptionBuilder()));
93
- }
94
- /** Adds a mentionable option. */ public addMentionableOption(
95
- configure: (option: MentionableOptionBuilder) => MentionableOptionBuilder,
96
- ): this {
97
- return this.addOption(configure(new MentionableOptionBuilder()));
98
- }
99
- /** Adds an attachment option. */ public addAttachmentOption(
100
- configure: (option: AttachmentOptionBuilder) => AttachmentOptionBuilder,
101
- ): this {
102
- return this.addOption(configure(new AttachmentOptionBuilder()));
103
- }
104
- /** Adds a subcommand. */ public addSubcommand(
105
- configure: (option: SubcommandBuilder) => SubcommandBuilder,
106
- ): this {
107
- return this.addOption(configure(new SubcommandBuilder()));
108
- }
109
- /** Adds a subcommand group. */ public addSubcommandGroup(
110
- configure: (option: SubcommandGroupBuilder) => SubcommandGroupBuilder,
111
- ): this {
112
- return this.addOption(configure(new SubcommandGroupBuilder()));
113
- }
114
- /** Serializes the command payload. */ public toJSON(): Record<
115
- string,
116
- unknown
117
- > {
118
- return structuredClone(this.#data);
119
- }
120
- /** Adds a validated top-level command option. */ protected addOption(
121
- option: CommandOptionBuilder,
122
- ): this {
123
- const options = (this.#data.options as unknown[] | undefined) ?? [];
124
- if (options.length >= 25)
125
- throw new RangeError(
126
- "An application command cannot contain more than 25 options.",
127
- );
128
- const payload = option.toJSON();
129
- if (
130
- options.some(
131
- (existing) =>
132
- (existing as Record<string, unknown>).name === payload.name,
133
- )
134
- )
135
- throw new RangeError(`Duplicate option name: ${String(payload.name)}.`);
136
- if (
137
- payload.required === true &&
138
- options.some(
139
- (existing) => (existing as Record<string, unknown>).required !== true,
140
- )
141
- )
142
- throw new RangeError(
143
- "Required application command options must be placed before optional options.",
144
- );
145
- options.push(payload);
146
- this.#data.options = options;
147
- return this;
148
- }
149
- }
150
-
151
- /** Base builder for command options. */
152
- export class CommandOptionBuilder {
153
- protected readonly data: Record<string, unknown>;
154
- /** Creates an option builder. */ public constructor(type: number) {
155
- this.data = { type };
156
- }
157
- /** Sets the option name. */ public setName(name: string): this {
158
- validateName(name, "Option name");
159
- this.data.name = name;
160
- return this;
161
- }
162
- /** Sets the option description. */ public setDescription(
163
- description: string,
164
- ): this {
165
- validateText(description, "Option description", 100);
166
- this.data.description = description;
167
- return this;
168
- }
169
- /** Makes the option required or optional. */ public setRequired(
170
- required = true,
171
- ): this {
172
- this.data.required = required;
173
- return this;
174
- }
175
- /** Sets autocomplete. */ public setAutocomplete(enabled = true): this {
176
- this.data.autocomplete = enabled;
177
- return this;
178
- }
179
- /** Serializes the option payload. */ public toJSON(): Record<
180
- string,
181
- unknown
182
- > {
183
- return structuredClone(this.data);
184
- }
185
- }
186
-
187
- /** Builds a string command option. */
188
- export class StringOptionBuilder extends CommandOptionBuilder {
189
- /** Creates a string option. */ public constructor() {
190
- super(ApplicationCommandOptionType.String);
191
- }
192
- /** Adds string choices. */ public addChoices(
193
- ...choices: Array<{ name: string; value: string }>
194
- ): this {
195
- const current = (this.data.choices as unknown[] | undefined) ?? [];
196
- if (!choices.length)
197
- throw new TypeError("At least one choice is required.");
198
- if (this.data.autocomplete === true)
199
- throw new RangeError("Autocomplete options cannot define choices.");
200
- if (current.length + choices.length > 25)
201
- throw new RangeError("An option cannot contain more than 25 choices.");
202
- for (const choice of choices) {
203
- validateText(choice.name, "Choice name", 100);
204
- validateText(choice.value, "Choice value", 100);
205
- }
206
- this.data.choices = [
207
- ...current,
208
- ...choices.map((choice) => ({ ...choice })),
209
- ];
210
- return this;
211
- }
212
- /** Sets the minimum string length. */ public setMinLength(
213
- value: number,
214
- ): this {
215
- validateIntegerRange(value, 0, 6000, "min_length");
216
- this.data.min_length = value;
217
- return this;
218
- }
219
- /** Sets the maximum string length. */ public setMaxLength(
220
- value: number,
221
- ): this {
222
- validateIntegerRange(value, 1, 6000, "max_length");
223
- if (
224
- (this.data.min_length as number | undefined) !== undefined &&
225
- (this.data.min_length as number) > value
226
- )
227
- throw new RangeError("min_length cannot exceed max_length.");
228
- this.data.max_length = value;
229
- return this;
230
- }
231
- }
232
- /** Builds an integer command option. */
233
- export class IntegerOptionBuilder extends CommandOptionBuilder {
234
- public constructor() {
235
- super(ApplicationCommandOptionType.Integer);
236
- }
237
- public setMinValue(value: number): this {
238
- validateNumberRange(
239
- value,
240
- -9_007_199_254_740_991,
241
- 9_007_199_254_740_991,
242
- "min_value",
243
- true,
244
- );
245
- this.data.min_value = value;
246
- return this;
247
- }
248
- public setMaxValue(value: number): this {
249
- validateNumberRange(
250
- value,
251
- -9_007_199_254_740_991,
252
- 9_007_199_254_740_991,
253
- "max_value",
254
- true,
255
- );
256
- if (
257
- (this.data.min_value as number | undefined) !== undefined &&
258
- (this.data.min_value as number) > value
259
- )
260
- throw new RangeError("min_value cannot exceed max_value.");
261
- this.data.max_value = value;
262
- return this;
263
- }
264
- public setAutocomplete(enabled = true): this {
265
- if (enabled && Array.isArray(this.data.choices) && this.data.choices.length)
266
- throw new RangeError("Autocomplete options cannot define choices.");
267
- this.data.autocomplete = enabled;
268
- return this;
269
- }
270
- public addChoices(...choices: { name: string; value: number }[]): this {
271
- const current = (this.data.choices as unknown[] | undefined) ?? [];
272
- if (!choices.length)
273
- throw new TypeError("At least one choice is required.");
274
- if (this.data.autocomplete === true)
275
- throw new RangeError("Autocomplete options cannot define choices.");
276
- if (current.length + choices.length > 25)
277
- throw new RangeError("An option cannot contain more than 25 choices.");
278
- this.data.choices = [...current, ...choices];
279
- return this;
280
- }
281
- }
282
- /** Builds a number command option. */
283
- export class NumberOptionBuilder extends CommandOptionBuilder {
284
- public constructor() {
285
- super(ApplicationCommandOptionType.Number);
286
- }
287
- public setMinValue(value: number): this {
288
- validateNumberRange(
289
- value,
290
- -9_007_199_254_740_991,
291
- 9_007_199_254_740_991,
292
- "min_value",
293
- false,
294
- );
295
- this.data.min_value = value;
296
- return this;
297
- }
298
- public setMaxValue(value: number): this {
299
- validateNumberRange(
300
- value,
301
- -9_007_199_254_740_991,
302
- 9_007_199_254_740_991,
303
- "max_value",
304
- false,
305
- );
306
- if (
307
- (this.data.min_value as number | undefined) !== undefined &&
308
- (this.data.min_value as number) > value
309
- )
310
- throw new RangeError("min_value cannot exceed max_value.");
311
- this.data.max_value = value;
312
- return this;
313
- }
314
- public setAutocomplete(enabled = true): this {
315
- if (enabled && Array.isArray(this.data.choices) && this.data.choices.length)
316
- throw new RangeError("Autocomplete options cannot define choices.");
317
- this.data.autocomplete = enabled;
318
- return this;
319
- }
320
- public addChoices(...choices: { name: string; value: number }[]): this {
321
- const current = (this.data.choices as unknown[] | undefined) ?? [];
322
- if (!choices.length)
323
- throw new TypeError("At least one choice is required.");
324
- if (this.data.autocomplete === true)
325
- throw new RangeError("Autocomplete options cannot define choices.");
326
- if (current.length + choices.length > 25)
327
- throw new RangeError("An option cannot contain more than 25 choices.");
328
- this.data.choices = [...current, ...choices];
329
- return this;
330
- }
331
- }
332
- /** Builds a boolean command option. */ export class BooleanOptionBuilder extends CommandOptionBuilder {
333
- /** Creates a boolean option. */ public constructor() {
334
- super(ApplicationCommandOptionType.Boolean);
335
- }
336
- }
337
- /** Builds a user command option. */ export class UserOptionBuilder extends CommandOptionBuilder {
338
- /** Creates a user option. */ public constructor() {
339
- super(ApplicationCommandOptionType.User);
340
- }
341
- }
342
- /** Builds a role command option. */ export class RoleOptionBuilder extends CommandOptionBuilder {
343
- /** Creates a role option. */ public constructor() {
344
- super(ApplicationCommandOptionType.Role);
345
- }
346
- }
347
- /** Builds a mentionable command option. */ export class MentionableOptionBuilder extends CommandOptionBuilder {
348
- /** Creates a mentionable option. */ public constructor() {
349
- super(ApplicationCommandOptionType.Mentionable);
350
- }
351
- }
352
- /** Builds an attachment command option. */ export class AttachmentOptionBuilder extends CommandOptionBuilder {
353
- /** Creates an attachment option. */ public constructor() {
354
- super(ApplicationCommandOptionType.Attachment);
355
- }
356
- }
357
- /** Builds a channel command option. */ export class ChannelOptionBuilder extends CommandOptionBuilder {
358
- /** Creates a channel option. */ public constructor() {
359
- super(ApplicationCommandOptionType.Channel);
360
- }
361
- /** Restricts accepted channel types. */ public addChannelTypes(
362
- ...types: number[]
363
- ): this {
364
- if (types.some((type) => !Number.isInteger(type) || type < 0))
365
- throw new RangeError("Channel types must be non-negative integers.");
366
- this.data.channel_types = [...new Set(types)];
367
- return this;
368
- }
369
- }
370
- /** Builds a nested subcommand. */ export class SubcommandBuilder extends CommandOptionBuilder {
371
- /** Creates a subcommand. */ public constructor() {
372
- super(ApplicationCommandOptionType.Subcommand);
373
- }
374
- /** Adds a string option to this subcommand. */ public addStringOption(
375
- configure: (option: StringOptionBuilder) => StringOptionBuilder,
376
- ): this {
377
- return this.addChildOption(configure(new StringOptionBuilder()));
378
- }
379
- /** Adds an integer option to this subcommand. */ public addIntegerOption(
380
- configure: (option: IntegerOptionBuilder) => IntegerOptionBuilder,
381
- ): this {
382
- return this.addChildOption(configure(new IntegerOptionBuilder()));
383
- }
384
- /** Adds a number option to this subcommand. */ public addNumberOption(
385
- configure: (option: NumberOptionBuilder) => NumberOptionBuilder,
386
- ): this {
387
- return this.addChildOption(configure(new NumberOptionBuilder()));
388
- }
389
- /** Adds a boolean option to this subcommand. */ public addBooleanOption(
390
- configure: (option: BooleanOptionBuilder) => BooleanOptionBuilder,
391
- ): this {
392
- return this.addChildOption(configure(new BooleanOptionBuilder()));
393
- }
394
- /** Adds a user option to this subcommand. */ public addUserOption(
395
- configure: (option: UserOptionBuilder) => UserOptionBuilder,
396
- ): this {
397
- return this.addChildOption(configure(new UserOptionBuilder()));
398
- }
399
- /** Adds a channel option to this subcommand. */ public addChannelOption(
400
- configure: (option: ChannelOptionBuilder) => ChannelOptionBuilder,
401
- ): this {
402
- return this.addChildOption(configure(new ChannelOptionBuilder()));
403
- }
404
- /** Adds a role option to this subcommand. */ public addRoleOption(
405
- configure: (option: RoleOptionBuilder) => RoleOptionBuilder,
406
- ): this {
407
- return this.addChildOption(configure(new RoleOptionBuilder()));
408
- }
409
- /** Adds a mentionable option to this subcommand. */ public addMentionableOption(
410
- configure: (option: MentionableOptionBuilder) => MentionableOptionBuilder,
411
- ): this {
412
- return this.addChildOption(configure(new MentionableOptionBuilder()));
413
- }
414
- /** Adds an attachment option to this subcommand. */ public addAttachmentOption(
415
- configure: (option: AttachmentOptionBuilder) => AttachmentOptionBuilder,
416
- ): this {
417
- return this.addChildOption(configure(new AttachmentOptionBuilder()));
418
- }
419
- protected addChildOption(option: CommandOptionBuilder): this {
420
- const options = (this.data.options as unknown[] | undefined) ?? [];
421
- if (options.length >= 25)
422
- throw new RangeError("A subcommand cannot contain more than 25 options.");
423
- const payload = option.toJSON();
424
- if (
425
- options.some(
426
- (existing) =>
427
- (existing as Record<string, unknown>).name === payload.name,
428
- )
429
- )
430
- throw new RangeError(`Duplicate option name: ${String(payload.name)}.`);
431
- if (
432
- payload.required === true &&
433
- options.some(
434
- (existing) => (existing as Record<string, unknown>).required !== true,
435
- )
436
- )
437
- throw new RangeError(
438
- "Required subcommand options must be placed before optional options.",
439
- );
440
- if (
441
- options.some(
442
- (existing) =>
443
- (existing as Record<string, unknown>).type ===
444
- ApplicationCommandOptionType.Subcommand,
445
- )
446
- )
447
- throw new RangeError("Subcommands cannot contain nested subcommands.");
448
- options.push(payload);
449
- this.data.options = options;
450
- return this;
451
- }
452
- }
453
- /** Builds a nested subcommand group. */ export class SubcommandGroupBuilder extends CommandOptionBuilder {
454
- /** Creates a subcommand group. */ public constructor() {
455
- super(ApplicationCommandOptionType.SubcommandGroup);
456
- }
457
- /** Adds a subcommand to this group. */ public addSubcommand(
458
- configure: (option: SubcommandBuilder) => SubcommandBuilder,
459
- ): this {
460
- const options = (this.data.options as unknown[] | undefined) ?? [];
461
- if (options.length >= 25)
462
- throw new RangeError(
463
- "A subcommand group cannot contain more than 25 subcommands.",
464
- );
465
- const payload = configure(new SubcommandBuilder()).toJSON();
466
- if (
467
- options.some(
468
- (existing) =>
469
- (existing as Record<string, unknown>).name === payload.name,
470
- )
471
- )
472
- throw new RangeError(
473
- `Duplicate subcommand name: ${String(payload.name)}.`,
474
- );
475
- if (payload.type !== ApplicationCommandOptionType.Subcommand)
476
- throw new TypeError("Subcommand group children must be subcommands.");
477
- options.push(payload);
478
- this.data.options = options;
479
- return this;
480
- }
481
- }
482
- function validateName(value: string, field: string): void {
483
- if (!/^[\p{L}\p{N}_-]{1,32}$/u.test(value) || value !== value.toLowerCase())
484
- throw new RangeError(
485
- `${field} must contain 1-32 lowercase letters, numbers, underscores, or hyphens.`,
486
- );
487
- }
488
- function validateText(value: string, field: string, max: number): void {
489
- if (typeof value !== "string" || !value.trim() || value.length > max)
490
- throw new RangeError(`${field} must contain 1-${max} characters.`);
491
- }
492
- function validateIntegerRange(
493
- value: number,
494
- minimum: number,
495
- maximum: number,
496
- field: string,
497
- ): void {
498
- if (!Number.isInteger(value) || value < minimum || value > maximum)
499
- throw new RangeError(
500
- `${field} must be an integer between ${minimum} and ${maximum}.`,
501
- );
502
- }
503
- function validateNumberRange(
504
- value: number,
505
- minimum: number,
506
- maximum: number,
507
- field: string,
508
- integerOnly = true,
509
- ): void {
510
- if (
511
- !Number.isFinite(value) ||
512
- (integerOnly && !Number.isInteger(value)) ||
513
- value < minimum ||
514
- value > maximum
515
- )
516
- throw new RangeError(
517
- `${field} must be ${integerOnly ? "an integer" : "a number"} between ${minimum} and ${maximum}.`,
518
- );
519
- }
1
+ /** Discord application command option types. */
2
+ export const ApplicationCommandOptionType = {
3
+ Subcommand: 1,
4
+ SubcommandGroup: 2,
5
+ String: 3,
6
+ Integer: 4,
7
+ Boolean: 5,
8
+ User: 6,
9
+ Channel: 7,
10
+ Role: 8,
11
+ Mentionable: 9,
12
+ Number: 10,
13
+ Attachment: 11,
14
+ } as const;
15
+
16
+ /** Builds Discord application command payloads. */
17
+ export class SlashCommandBuilder {
18
+ readonly #data: Record<string, unknown> = { type: 1 };
19
+ /** Sets the command name. */ public setName(name: string): this {
20
+ validateName(name, "Command name");
21
+ this.#data.name = name;
22
+ return this;
23
+ }
24
+ /** Sets the command description. */ public setDescription(
25
+ description: string,
26
+ ): this {
27
+ validateText(description, "Command description", 100);
28
+ this.#data.description = description;
29
+ return this;
30
+ }
31
+ /** Sets whether the command is available in direct messages. */ public setDMPermission(
32
+ enabled: boolean,
33
+ ): this {
34
+ this.#data.dm_permission = enabled;
35
+ return this;
36
+ }
37
+ /** Sets whether the command is NSFW. */
38
+ public setNSFW(nsfw = true): this {
39
+ this.#data.nsfw = nsfw;
40
+ return this;
41
+ }
42
+ /** Sets whether the command is NSFW. */
43
+ public setNsfw(nsfw = true): this {
44
+ return this.setNSFW(nsfw);
45
+ }
46
+ /** Sets command default member permissions. */ public setDefaultMemberPermissions(
47
+ permissions: bigint | number | string | null,
48
+ ): this {
49
+ this.#data.default_member_permissions =
50
+ permissions === null ? null : BigInt(permissions).toString();
51
+ return this;
52
+ }
53
+ /** Sets command integration types. */ public setIntegrationTypes(
54
+ ...types: number[]
55
+ ): this {
56
+ this.#data.integration_types = [...types];
57
+ return this;
58
+ }
59
+ /** Adds a string option. */ public addStringOption(
60
+ configure: (option: StringOptionBuilder) => StringOptionBuilder,
61
+ ): this {
62
+ return this.addOption(configure(new StringOptionBuilder()));
63
+ }
64
+ /** Adds an integer option. */ public addIntegerOption(
65
+ configure: (option: IntegerOptionBuilder) => IntegerOptionBuilder,
66
+ ): this {
67
+ return this.addOption(configure(new IntegerOptionBuilder()));
68
+ }
69
+ /** Adds a number option. */ public addNumberOption(
70
+ configure: (option: NumberOptionBuilder) => NumberOptionBuilder,
71
+ ): this {
72
+ return this.addOption(configure(new NumberOptionBuilder()));
73
+ }
74
+ /** Adds a boolean option. */ public addBooleanOption(
75
+ configure: (option: BooleanOptionBuilder) => BooleanOptionBuilder,
76
+ ): this {
77
+ return this.addOption(configure(new BooleanOptionBuilder()));
78
+ }
79
+ /** Adds a user option. */ public addUserOption(
80
+ configure: (option: UserOptionBuilder) => UserOptionBuilder,
81
+ ): this {
82
+ return this.addOption(configure(new UserOptionBuilder()));
83
+ }
84
+ /** Adds a channel option. */ public addChannelOption(
85
+ configure: (option: ChannelOptionBuilder) => ChannelOptionBuilder,
86
+ ): this {
87
+ return this.addOption(configure(new ChannelOptionBuilder()));
88
+ }
89
+ /** Adds a role option. */ public addRoleOption(
90
+ configure: (option: RoleOptionBuilder) => RoleOptionBuilder,
91
+ ): this {
92
+ return this.addOption(configure(new RoleOptionBuilder()));
93
+ }
94
+ /** Adds a mentionable option. */ public addMentionableOption(
95
+ configure: (
96
+ option: MentionableOptionBuilder,
97
+ ) => MentionableOptionBuilder,
98
+ ): this {
99
+ return this.addOption(configure(new MentionableOptionBuilder()));
100
+ }
101
+ /** Adds an attachment option. */ public addAttachmentOption(
102
+ configure: (option: AttachmentOptionBuilder) => AttachmentOptionBuilder,
103
+ ): this {
104
+ return this.addOption(configure(new AttachmentOptionBuilder()));
105
+ }
106
+ /** Adds a subcommand. */ public addSubcommand(
107
+ configure: (option: SubcommandBuilder) => SubcommandBuilder,
108
+ ): this {
109
+ return this.addOption(configure(new SubcommandBuilder()));
110
+ }
111
+ /** Adds a subcommand group. */ public addSubcommandGroup(
112
+ configure: (option: SubcommandGroupBuilder) => SubcommandGroupBuilder,
113
+ ): this {
114
+ return this.addOption(configure(new SubcommandGroupBuilder()));
115
+ }
116
+ /** Serializes the command payload. */ public toJSON(): Record<
117
+ string,
118
+ unknown
119
+ > {
120
+ return structuredClone(this.#data);
121
+ }
122
+ /** Adds a validated top-level command option. */ protected addOption(
123
+ option: CommandOptionBuilder,
124
+ ): this {
125
+ const options = (this.#data.options as unknown[] | undefined) ?? [];
126
+ if (options.length >= 25)
127
+ throw new RangeError(
128
+ "An application command cannot contain more than 25 options.",
129
+ );
130
+ const payload = option.toJSON();
131
+ if (
132
+ options.some(
133
+ (existing) =>
134
+ (existing as Record<string, unknown>).name === payload.name,
135
+ )
136
+ )
137
+ throw new RangeError(
138
+ `Duplicate option name: ${String(payload.name)}.`,
139
+ );
140
+ if (
141
+ payload.required === true &&
142
+ options.some(
143
+ (existing) =>
144
+ (existing as Record<string, unknown>).required !== true,
145
+ )
146
+ )
147
+ throw new RangeError(
148
+ "Required application command options must be placed before optional options.",
149
+ );
150
+ options.push(payload);
151
+ this.#data.options = options;
152
+ return this;
153
+ }
154
+ }
155
+
156
+ /** Base builder for command options. */
157
+ export class CommandOptionBuilder {
158
+ protected readonly data: Record<string, unknown>;
159
+ /** Creates an option builder. */ public constructor(type: number) {
160
+ this.data = { type };
161
+ }
162
+ /** Sets the option name. */ public setName(name: string): this {
163
+ validateName(name, "Option name");
164
+ this.data.name = name;
165
+ return this;
166
+ }
167
+ /** Sets the option description. */ public setDescription(
168
+ description: string,
169
+ ): this {
170
+ validateText(description, "Option description", 100);
171
+ this.data.description = description;
172
+ return this;
173
+ }
174
+ /** Makes the option required or optional. */ public setRequired(
175
+ required = true,
176
+ ): this {
177
+ this.data.required = required;
178
+ return this;
179
+ }
180
+ /** Sets autocomplete. */ public setAutocomplete(enabled = true): this {
181
+ this.data.autocomplete = enabled;
182
+ return this;
183
+ }
184
+ /** Serializes the option payload. */ public toJSON(): Record<
185
+ string,
186
+ unknown
187
+ > {
188
+ return structuredClone(this.data);
189
+ }
190
+ }
191
+
192
+ /** Builds a string command option. */
193
+ export class StringOptionBuilder extends CommandOptionBuilder {
194
+ /** Creates a string option. */ public constructor() {
195
+ super(ApplicationCommandOptionType.String);
196
+ }
197
+ /** Adds string choices. */ public addChoices(
198
+ ...choices: Array<{ name: string; value: string }>
199
+ ): this {
200
+ const current = (this.data.choices as unknown[] | undefined) ?? [];
201
+ if (!choices.length)
202
+ throw new TypeError("At least one choice is required.");
203
+ if (this.data.autocomplete === true)
204
+ throw new RangeError("Autocomplete options cannot define choices.");
205
+ if (current.length + choices.length > 25)
206
+ throw new RangeError(
207
+ "An option cannot contain more than 25 choices.",
208
+ );
209
+ for (const choice of choices) {
210
+ validateText(choice.name, "Choice name", 100);
211
+ validateText(choice.value, "Choice value", 100);
212
+ }
213
+ this.data.choices = [
214
+ ...current,
215
+ ...choices.map((choice) => ({ ...choice })),
216
+ ];
217
+ return this;
218
+ }
219
+ /** Sets the minimum string length. */ public setMinLength(
220
+ value: number,
221
+ ): this {
222
+ validateIntegerRange(value, 0, 6000, "min_length");
223
+ this.data.min_length = value;
224
+ return this;
225
+ }
226
+ /** Sets the maximum string length. */ public setMaxLength(
227
+ value: number,
228
+ ): this {
229
+ validateIntegerRange(value, 1, 6000, "max_length");
230
+ if (
231
+ (this.data.min_length as number | undefined) !== undefined &&
232
+ (this.data.min_length as number) > value
233
+ )
234
+ throw new RangeError("min_length cannot exceed max_length.");
235
+ this.data.max_length = value;
236
+ return this;
237
+ }
238
+ }
239
+ /** Builds an integer command option. */
240
+ export class IntegerOptionBuilder extends CommandOptionBuilder {
241
+ public constructor() {
242
+ super(ApplicationCommandOptionType.Integer);
243
+ }
244
+ public setMinValue(value: number): this {
245
+ validateNumberRange(
246
+ value,
247
+ -9_007_199_254_740_991,
248
+ 9_007_199_254_740_991,
249
+ "min_value",
250
+ true,
251
+ );
252
+ this.data.min_value = value;
253
+ return this;
254
+ }
255
+ public setMaxValue(value: number): this {
256
+ validateNumberRange(
257
+ value,
258
+ -9_007_199_254_740_991,
259
+ 9_007_199_254_740_991,
260
+ "max_value",
261
+ true,
262
+ );
263
+ if (
264
+ (this.data.min_value as number | undefined) !== undefined &&
265
+ (this.data.min_value as number) > value
266
+ )
267
+ throw new RangeError("min_value cannot exceed max_value.");
268
+ this.data.max_value = value;
269
+ return this;
270
+ }
271
+ public setAutocomplete(enabled = true): this {
272
+ if (
273
+ enabled &&
274
+ Array.isArray(this.data.choices) &&
275
+ this.data.choices.length
276
+ )
277
+ throw new RangeError("Autocomplete options cannot define choices.");
278
+ this.data.autocomplete = enabled;
279
+ return this;
280
+ }
281
+ public addChoices(...choices: { name: string; value: number }[]): this {
282
+ const current = (this.data.choices as unknown[] | undefined) ?? [];
283
+ if (!choices.length)
284
+ throw new TypeError("At least one choice is required.");
285
+ if (this.data.autocomplete === true)
286
+ throw new RangeError("Autocomplete options cannot define choices.");
287
+ if (current.length + choices.length > 25)
288
+ throw new RangeError(
289
+ "An option cannot contain more than 25 choices.",
290
+ );
291
+ this.data.choices = [...current, ...choices];
292
+ return this;
293
+ }
294
+ }
295
+ /** Builds a number command option. */
296
+ export class NumberOptionBuilder extends CommandOptionBuilder {
297
+ public constructor() {
298
+ super(ApplicationCommandOptionType.Number);
299
+ }
300
+ public setMinValue(value: number): this {
301
+ validateNumberRange(
302
+ value,
303
+ -9_007_199_254_740_991,
304
+ 9_007_199_254_740_991,
305
+ "min_value",
306
+ false,
307
+ );
308
+ this.data.min_value = value;
309
+ return this;
310
+ }
311
+ public setMaxValue(value: number): this {
312
+ validateNumberRange(
313
+ value,
314
+ -9_007_199_254_740_991,
315
+ 9_007_199_254_740_991,
316
+ "max_value",
317
+ false,
318
+ );
319
+ if (
320
+ (this.data.min_value as number | undefined) !== undefined &&
321
+ (this.data.min_value as number) > value
322
+ )
323
+ throw new RangeError("min_value cannot exceed max_value.");
324
+ this.data.max_value = value;
325
+ return this;
326
+ }
327
+ public setAutocomplete(enabled = true): this {
328
+ if (
329
+ enabled &&
330
+ Array.isArray(this.data.choices) &&
331
+ this.data.choices.length
332
+ )
333
+ throw new RangeError("Autocomplete options cannot define choices.");
334
+ this.data.autocomplete = enabled;
335
+ return this;
336
+ }
337
+ public addChoices(...choices: { name: string; value: number }[]): this {
338
+ const current = (this.data.choices as unknown[] | undefined) ?? [];
339
+ if (!choices.length)
340
+ throw new TypeError("At least one choice is required.");
341
+ if (this.data.autocomplete === true)
342
+ throw new RangeError("Autocomplete options cannot define choices.");
343
+ if (current.length + choices.length > 25)
344
+ throw new RangeError(
345
+ "An option cannot contain more than 25 choices.",
346
+ );
347
+ this.data.choices = [...current, ...choices];
348
+ return this;
349
+ }
350
+ }
351
+ /** Builds a boolean command option. */ export class BooleanOptionBuilder extends CommandOptionBuilder {
352
+ /** Creates a boolean option. */ public constructor() {
353
+ super(ApplicationCommandOptionType.Boolean);
354
+ }
355
+ }
356
+ /** Builds a user command option. */ export class UserOptionBuilder extends CommandOptionBuilder {
357
+ /** Creates a user option. */ public constructor() {
358
+ super(ApplicationCommandOptionType.User);
359
+ }
360
+ }
361
+ /** Builds a role command option. */ export class RoleOptionBuilder extends CommandOptionBuilder {
362
+ /** Creates a role option. */ public constructor() {
363
+ super(ApplicationCommandOptionType.Role);
364
+ }
365
+ }
366
+ /** Builds a mentionable command option. */ export class MentionableOptionBuilder extends CommandOptionBuilder {
367
+ /** Creates a mentionable option. */ public constructor() {
368
+ super(ApplicationCommandOptionType.Mentionable);
369
+ }
370
+ }
371
+ /** Builds an attachment command option. */ export class AttachmentOptionBuilder extends CommandOptionBuilder {
372
+ /** Creates an attachment option. */ public constructor() {
373
+ super(ApplicationCommandOptionType.Attachment);
374
+ }
375
+ }
376
+ /** Builds a channel command option. */ export class ChannelOptionBuilder extends CommandOptionBuilder {
377
+ /** Creates a channel option. */ public constructor() {
378
+ super(ApplicationCommandOptionType.Channel);
379
+ }
380
+ /** Restricts accepted channel types. */ public addChannelTypes(
381
+ ...types: number[]
382
+ ): this {
383
+ if (types.some((type) => !Number.isInteger(type) || type < 0))
384
+ throw new RangeError(
385
+ "Channel types must be non-negative integers.",
386
+ );
387
+ this.data.channel_types = [...new Set(types)];
388
+ return this;
389
+ }
390
+ }
391
+ /** Builds a nested subcommand. */ export class SubcommandBuilder extends CommandOptionBuilder {
392
+ /** Creates a subcommand. */ public constructor() {
393
+ super(ApplicationCommandOptionType.Subcommand);
394
+ }
395
+ /** Adds a string option to this subcommand. */ public addStringOption(
396
+ configure: (option: StringOptionBuilder) => StringOptionBuilder,
397
+ ): this {
398
+ return this.addChildOption(configure(new StringOptionBuilder()));
399
+ }
400
+ /** Adds an integer option to this subcommand. */ public addIntegerOption(
401
+ configure: (option: IntegerOptionBuilder) => IntegerOptionBuilder,
402
+ ): this {
403
+ return this.addChildOption(configure(new IntegerOptionBuilder()));
404
+ }
405
+ /** Adds a number option to this subcommand. */ public addNumberOption(
406
+ configure: (option: NumberOptionBuilder) => NumberOptionBuilder,
407
+ ): this {
408
+ return this.addChildOption(configure(new NumberOptionBuilder()));
409
+ }
410
+ /** Adds a boolean option to this subcommand. */ public addBooleanOption(
411
+ configure: (option: BooleanOptionBuilder) => BooleanOptionBuilder,
412
+ ): this {
413
+ return this.addChildOption(configure(new BooleanOptionBuilder()));
414
+ }
415
+ /** Adds a user option to this subcommand. */ public addUserOption(
416
+ configure: (option: UserOptionBuilder) => UserOptionBuilder,
417
+ ): this {
418
+ return this.addChildOption(configure(new UserOptionBuilder()));
419
+ }
420
+ /** Adds a channel option to this subcommand. */ public addChannelOption(
421
+ configure: (option: ChannelOptionBuilder) => ChannelOptionBuilder,
422
+ ): this {
423
+ return this.addChildOption(configure(new ChannelOptionBuilder()));
424
+ }
425
+ /** Adds a role option to this subcommand. */ public addRoleOption(
426
+ configure: (option: RoleOptionBuilder) => RoleOptionBuilder,
427
+ ): this {
428
+ return this.addChildOption(configure(new RoleOptionBuilder()));
429
+ }
430
+ /** Adds a mentionable option to this subcommand. */ public addMentionableOption(
431
+ configure: (
432
+ option: MentionableOptionBuilder,
433
+ ) => MentionableOptionBuilder,
434
+ ): this {
435
+ return this.addChildOption(configure(new MentionableOptionBuilder()));
436
+ }
437
+ /** Adds an attachment option to this subcommand. */ public addAttachmentOption(
438
+ configure: (option: AttachmentOptionBuilder) => AttachmentOptionBuilder,
439
+ ): this {
440
+ return this.addChildOption(configure(new AttachmentOptionBuilder()));
441
+ }
442
+ protected addChildOption(option: CommandOptionBuilder): this {
443
+ const options = (this.data.options as unknown[] | undefined) ?? [];
444
+ if (options.length >= 25)
445
+ throw new RangeError(
446
+ "A subcommand cannot contain more than 25 options.",
447
+ );
448
+ const payload = option.toJSON();
449
+ if (
450
+ options.some(
451
+ (existing) =>
452
+ (existing as Record<string, unknown>).name === payload.name,
453
+ )
454
+ )
455
+ throw new RangeError(
456
+ `Duplicate option name: ${String(payload.name)}.`,
457
+ );
458
+ if (
459
+ payload.required === true &&
460
+ options.some(
461
+ (existing) =>
462
+ (existing as Record<string, unknown>).required !== true,
463
+ )
464
+ )
465
+ throw new RangeError(
466
+ "Required subcommand options must be placed before optional options.",
467
+ );
468
+ if (
469
+ options.some(
470
+ (existing) =>
471
+ (existing as Record<string, unknown>).type ===
472
+ ApplicationCommandOptionType.Subcommand,
473
+ )
474
+ )
475
+ throw new RangeError(
476
+ "Subcommands cannot contain nested subcommands.",
477
+ );
478
+ options.push(payload);
479
+ this.data.options = options;
480
+ return this;
481
+ }
482
+ }
483
+ /** Builds a nested subcommand group. */ export class SubcommandGroupBuilder extends CommandOptionBuilder {
484
+ /** Creates a subcommand group. */ public constructor() {
485
+ super(ApplicationCommandOptionType.SubcommandGroup);
486
+ }
487
+ /** Adds a subcommand to this group. */ public addSubcommand(
488
+ configure: (option: SubcommandBuilder) => SubcommandBuilder,
489
+ ): this {
490
+ const options = (this.data.options as unknown[] | undefined) ?? [];
491
+ if (options.length >= 25)
492
+ throw new RangeError(
493
+ "A subcommand group cannot contain more than 25 subcommands.",
494
+ );
495
+ const payload = configure(new SubcommandBuilder()).toJSON();
496
+ if (
497
+ options.some(
498
+ (existing) =>
499
+ (existing as Record<string, unknown>).name === payload.name,
500
+ )
501
+ )
502
+ throw new RangeError(
503
+ `Duplicate subcommand name: ${String(payload.name)}.`,
504
+ );
505
+ if (payload.type !== ApplicationCommandOptionType.Subcommand)
506
+ throw new TypeError(
507
+ "Subcommand group children must be subcommands.",
508
+ );
509
+ options.push(payload);
510
+ this.data.options = options;
511
+ return this;
512
+ }
513
+ }
514
+ function validateName(value: string, field: string): void {
515
+ if (!/^[\p{L}\p{N}_-]{1,32}$/u.test(value) || value !== value.toLowerCase())
516
+ throw new RangeError(
517
+ `${field} must contain 1-32 lowercase letters, numbers, underscores, or hyphens.`,
518
+ );
519
+ }
520
+ function validateText(value: string, field: string, max: number): void {
521
+ if (typeof value !== "string" || !value.trim() || value.length > max)
522
+ throw new RangeError(`${field} must contain 1-${max} characters.`);
523
+ }
524
+ function validateIntegerRange(
525
+ value: number,
526
+ minimum: number,
527
+ maximum: number,
528
+ field: string,
529
+ ): void {
530
+ if (!Number.isInteger(value) || value < minimum || value > maximum)
531
+ throw new RangeError(
532
+ `${field} must be an integer between ${minimum} and ${maximum}.`,
533
+ );
534
+ }
535
+ function validateNumberRange(
536
+ value: number,
537
+ minimum: number,
538
+ maximum: number,
539
+ field: string,
540
+ integerOnly = true,
541
+ ): void {
542
+ if (
543
+ !Number.isFinite(value) ||
544
+ (integerOnly && !Number.isInteger(value)) ||
545
+ value < minimum ||
546
+ value > maximum
547
+ )
548
+ throw new RangeError(
549
+ `${field} must be ${integerOnly ? "an integer" : "a number"} between ${minimum} and ${maximum}.`,
550
+ );
551
+ }
552
+
553
+ // ─── Context Menu Builders ────────────────────────────────────────────────────
554
+
555
+ /** Base builder for application commands that appear in right-click context menus. */
556
+ class ContextMenuCommandBuilder {
557
+ protected readonly data: Record<string, unknown>;
558
+ public constructor(type: 2 | 3) {
559
+ this.data = { type };
560
+ }
561
+ /** Sets the command name (shown in the right-click menu).
562
+ * Unlike CHAT_INPUT commands, USER (type 2) and MESSAGE (type 3) context-menu
563
+ * command names may contain uppercase letters and spaces, so no lowercase/charset
564
+ * validation is applied here — only Discord's 1-32 length limit and a non-empty
565
+ * (non-whitespace) requirement are enforced.
566
+ * @param name Display name; 1-32 characters, mixed case and spaces allowed.
567
+ * @throws {RangeError} If the name is empty/whitespace-only or exceeds 32 characters.
568
+ */
569
+ public setName(name: string): this {
570
+ if (
571
+ typeof name !== "string" ||
572
+ name.trim().length < 1 ||
573
+ name.length > 32
574
+ )
575
+ throw new RangeError("Command name must contain 1-32 characters.");
576
+ this.data.name = name;
577
+ return this;
578
+ }
579
+ /** Sets default member permissions required to see this command. */
580
+ public setDefaultMemberPermissions(
581
+ permissions: bigint | number | string | null,
582
+ ): this {
583
+ this.data.default_member_permissions =
584
+ permissions === null ? null : BigInt(permissions).toString();
585
+ return this;
586
+ }
587
+ /** Sets whether the command is available in direct messages. */
588
+ public setDMPermission(enabled: boolean): this {
589
+ this.data.dm_permission = enabled;
590
+ return this;
591
+ }
592
+ /** Sets command integration types. */
593
+ public setIntegrationTypes(...types: number[]): this {
594
+ this.data.integration_types = [...types];
595
+ return this;
596
+ }
597
+ /** Serializes the command payload for the Discord API. */
598
+ public toJSON(): Record<string, unknown> {
599
+ return structuredClone(this.data);
600
+ }
601
+ }
602
+
603
+ /** Builds a User context menu command (appears when right-clicking a user, type 2).
604
+ * @example
605
+ * new UserCommandBuilder().setName("View Profile").toJSON()
606
+ */
607
+ export class UserCommandBuilder extends ContextMenuCommandBuilder {
608
+ public constructor() {
609
+ super(2);
610
+ }
611
+ }
612
+
613
+ /** Builds a Message context menu command (appears when right-clicking a message, type 3).
614
+ * @example
615
+ * new MessageCommandBuilder().setName("Translate Message").toJSON()
616
+ */
617
+ export class MessageCommandBuilder extends ContextMenuCommandBuilder {
618
+ public constructor() {
619
+ super(3);
620
+ }
621
+ }