@@ -2,6 +2,7 @@ import { AliasPiece, AliasPieceOptions, PieceContext } from '@sapphire/pieces';
2
2
import { Awaited , isNullish } from '@sapphire/utilities' ;
3
3
import { Message , PermissionResolvable , Permissions , Snowflake } from 'discord.js' ;
4
4
import * as Lexure from 'lexure' ;
5
+ import { sep } from 'path' ;
5
6
import { Args } from '../parsers/Args' ;
6
7
import { BucketScope } from '../types/Enums' ;
7
8
import { PreconditionContainerArray , PreconditionEntryResolvable } from '../utils/preconditions/PreconditionContainerArray' ;
@@ -26,6 +27,16 @@ export abstract class Command<T = Args> extends AliasPiece {
26
27
*/
27
28
public detailedDescription : string ;
28
29
30
+ /**
31
+ * The full category for the command. Either an array of strings that denote every (sub)folder the command is in,
32
+ * or `null` if it could not be resolved automatically.
33
+ *
34
+ * If this is `null` for how you setup your code then you can overwrite how the `fullCategory` is resolved by
35
+ * extending this class and overwriting the assignment in the constructor.
36
+ * @since 2.0.0
37
+ */
38
+ public readonly fullCategory : readonly string [ ] | null = null ;
39
+
29
40
/**
30
41
* The strategy to use for the lexer.
31
42
* @since 1.0.0
@@ -49,6 +60,7 @@ export abstract class Command<T = Args> extends AliasPiece {
49
60
this . description = options . description ?? '' ;
50
61
this . detailedDescription = options . detailedDescription ?? '' ;
51
62
this . strategy = new FlagUnorderedStrategy ( options ) ;
63
+
52
64
this . lexer . setQuotes (
53
65
options . quotes ?? [
54
66
[ '"' , '"' ] , // Double quotes
@@ -57,6 +69,19 @@ export abstract class Command<T = Args> extends AliasPiece {
57
69
]
58
70
) ;
59
71
72
+ if ( options . fullCategory ) {
73
+ this . fullCategory = options . fullCategory ;
74
+ } else {
75
+ const commandsFolders = [ ...this . container . stores . get ( 'commands' ) . paths . values ( ) ] . map ( ( p ) => p . split ( sep ) . pop ( ) ?? '' ) ;
76
+ const commandPath = context . path . split ( sep ) ;
77
+ for ( const commandFolder of commandsFolders ) {
78
+ if ( commandPath . includes ( commandFolder ) ) {
79
+ this . fullCategory = commandPath . slice ( commandPath . indexOf ( commandFolder ) + 1 , - 1 ) ;
80
+ break ;
81
+ }
82
+ }
83
+ }
84
+
60
85
if ( options . generateDashLessAliases ) {
61
86
const dashLessAliases = [ ] ;
62
87
if ( this . name . includes ( '-' ) ) dashLessAliases . push ( this . name . replace ( / - / g, '' ) ) ;
@@ -81,6 +106,46 @@ export abstract class Command<T = Args> extends AliasPiece {
81
106
return new Args ( message , this as any , args , context ) as any ;
82
107
}
83
108
109
+ /**
110
+ * Get all the main categories of commands.
111
+ */
112
+ public get categories ( ) : ( string | null ) [ ] {
113
+ return Array . from ( new Set ( [ ...this . container . stores . get ( 'commands' ) . values ( ) ] . map ( ( { category } ) => category ) ) ) ;
114
+ }
115
+
116
+ /**
117
+ * The main category for the command, if any.
118
+ * This is resolved from {@link Command.fullCategory}, which is automatically
119
+ * resolved in the constructor. If you need different logic for category
120
+ * then please first look into overwriting {@link Command.fullCategory} before
121
+ * looking to overwrite this getter.
122
+ */
123
+ public get category ( ) : string | null {
124
+ return ( this . fullCategory ?. length ?? 0 ) > 0 ? this . fullCategory ?. [ 0 ] ?? null : null ;
125
+ }
126
+
127
+ /**
128
+ * The sub category for the command
129
+ * This is resolved from {@link Command.fullCategory}, which is automatically
130
+ * resolved in the constructor. If you need different logic for category
131
+ * then please first look into overwriting {@link Command.fullCategory} before
132
+ * looking to overwrite this getter.
133
+ */
134
+ public get subCategory ( ) : string | null {
135
+ return ( this . fullCategory ?. length ?? 0 ) > 1 ? this . fullCategory ?. [ 1 ] ?? null : null ;
136
+ }
137
+
138
+ /**
139
+ * The parent category for the command
140
+ * This is resolved from {@link Command.fullCategory}, which is automatically
141
+ * resolved in the constructor. If you need different logic for category
142
+ * then please first look into overwriting {@link Command.fullCategory} before
143
+ * looking to overwrite this getter.
144
+ */
145
+ public get parentCategory ( ) : string | null {
146
+ return ( this . fullCategory ?. length ?? 0 ) > 0 ? this . fullCategory ?. [ ( this . fullCategory ?. length ?? 1 ) - 1 ] ?? null : null ;
147
+ }
148
+
84
149
/**
85
150
* Executes the command's logic.
86
151
* @param message The message that triggered the command.
@@ -96,6 +161,7 @@ export abstract class Command<T = Args> extends AliasPiece {
96
161
...super . toJSON ( ) ,
97
162
description : this . description ,
98
163
detailedDescription : this . detailedDescription ,
164
+ category : this . category ,
99
165
strategy : this . strategy
100
166
} ;
101
167
}
@@ -312,6 +378,21 @@ export interface CommandOptions extends AliasPieceOptions, FlagStrategyOptions {
312
378
*/
313
379
detailedDescription ?: string ;
314
380
381
+ /**
382
+ * The full category path for the command
383
+ * @since 2.0.0
384
+ * @default 'An array of folder names that lead back to the folder that is registered for in the commands store'
385
+ * @example
386
+ * ```typescript
387
+ * // Given a file named `ping.js` at the path of `commands/General/ping.js`
388
+ * ['General']
389
+ *
390
+ * // Given a file named `info.js` at the path of `commands/General/About/ping.js`
391
+ * ['General', 'About']
392
+ * ```
393
+ */
394
+ fullCategory ?: string [ ] ;
395
+
315
396
/**
316
397
* The {@link Precondition}s to be run, accepts an array of their names.
317
398
* @seealso {@link PreconditionContainerArray }
0 commit comments