@@ -51,6 +51,8 @@ export interface ParsedCommand {
51
51
middlewares : string [ ] ;
52
52
/** Absolute path to this command */
53
53
fullPath : string ;
54
+ /** Category the command belongs to, if any */
55
+ category : string | null ;
54
56
}
55
57
56
58
/**
@@ -66,6 +68,8 @@ export interface ParsedMiddleware {
66
68
path : string ;
67
69
/** Absolute path to the middleware file */
68
70
fullPath : string ;
71
+ /** Category the middleware belongs to, if any */
72
+ category : string | null ;
69
73
}
70
74
71
75
/**
@@ -244,68 +248,81 @@ export class CommandsRouter {
244
248
* @returns Promise resolving to the complete commands tree
245
249
*/
246
250
public async scan ( ) : Promise < CommandsTree > {
251
+ this . clear ( ) ;
247
252
const files = await this . scanDirectory ( this . entrypoint , [ ] ) ;
248
253
249
- for ( const file of files ) {
250
- if ( this . execMatcher ( this . matchers . command , file ) ) {
251
- const location = this . resolveRelativePath ( file ) ;
252
- const parts = location . split ( path . sep ) ;
253
-
254
- const parentSegments : string [ ] = [ ] ;
255
-
256
- parts . forEach ( ( part , index , arr ) => {
257
- const isLast = index === arr . length - 1 ;
258
-
259
- // ignore last because it's definitely a command source file
260
- if ( isLast ) return ;
261
-
262
- // we ignore groups
263
- if ( ! / \( .+ \) / . test ( part ) ) {
264
- parentSegments . push ( part . trim ( ) ) ;
265
- }
266
- } ) ;
267
-
268
- const parent = parentSegments . join ( ' ' ) ;
269
- const name = parts [ parts . length - 2 ] ;
270
-
271
- const command : ParsedCommand = {
272
- name,
273
- middlewares : [ ] ,
274
- parent : parent || null ,
275
- path : location ,
276
- fullPath : file ,
277
- parentSegments,
278
- } ;
254
+ // First pass: collect all files
255
+ const commandFiles = files . filter ( ( file ) => {
256
+ const basename = path . basename ( file ) ;
257
+ return ! this . isIgnoredFile ( basename ) && this . isCommandFile ( file ) ;
258
+ } ) ;
279
259
280
- this . commands . set ( name , command ) ;
281
- }
260
+ // Second pass: process middleware
261
+ const middlewareFiles = files . filter ( ( file ) =>
262
+ this . execMatcher ( this . matchers . middleware , file ) ,
263
+ ) ;
264
+
265
+ // Process commands
266
+ for ( const file of commandFiles ) {
267
+ const parsedPath = this . parseCommandPath ( file ) ;
268
+ const location = this . resolveRelativePath ( file ) ;
269
+
270
+ const command : ParsedCommand = {
271
+ name : parsedPath . name ,
272
+ path : location ,
273
+ fullPath : file ,
274
+ parent : parsedPath . parent ,
275
+ parentSegments : parsedPath . parentSegments ,
276
+ category : parsedPath . category ,
277
+ middlewares : [ ] ,
278
+ } ;
282
279
283
- if ( this . execMatcher ( this . matchers . middleware , file ) ) {
284
- const location = this . resolveRelativePath ( file ) ;
285
- const name = location . replace ( / \. ( m | c ) ? ( j | t ) s x ? $ / , '' ) ;
286
- const middlewareDir = path . dirname ( location ) ;
280
+ this . commands . set ( parsedPath . name , command ) ;
281
+ }
287
282
288
- const command = Array . from ( this . commands . values ( ) ) . filter ( ( command ) => {
289
- const commandDir = path . dirname ( command . path ) ;
290
- return (
291
- commandDir === middlewareDir || commandDir . startsWith ( middlewareDir )
292
- ) ;
293
- } ) ;
283
+ // Process middleware
284
+ for ( const file of middlewareFiles ) {
285
+ const location = this . resolveRelativePath ( file ) ;
286
+ const dirname = path . dirname ( location ) ;
287
+ const id = crypto . randomUUID ( ) ;
288
+ const parts = location . split ( path . sep ) . filter ( ( p ) => p ) ;
289
+ const categories = this . parseCategories ( parts ) ;
290
+
291
+ const middleware : ParsedMiddleware = {
292
+ id,
293
+ name : dirname ,
294
+ path : location ,
295
+ fullPath : file ,
296
+ category : categories . length ? categories . join ( '/' ) : null ,
297
+ } ;
294
298
295
- const id = crypto . randomUUID ( ) ;
299
+ this . middlewares . set ( id , middleware ) ;
296
300
297
- const middleware : ParsedMiddleware = {
298
- id,
299
- name,
300
- path : location ,
301
- fullPath : file ,
302
- } ;
301
+ // Apply middleware based on location
302
+ const isGlobalMiddleware = path . parse ( file ) . name === 'middleware' ;
303
+ const commands = Array . from ( this . commands . values ( ) ) ;
303
304
304
- this . middlewares . set ( id , middleware ) ;
305
+ for ( const command of commands ) {
306
+ const commandDir = path . dirname ( command . path ) ;
305
307
306
- command . forEach ( ( cmd ) => {
307
- cmd . middlewares . push ( id ) ;
308
- } ) ;
308
+ if ( isGlobalMiddleware ) {
309
+ // Global middleware applies if command is in same dir or nested
310
+ if (
311
+ commandDir === dirname ||
312
+ commandDir . startsWith ( dirname + path . sep )
313
+ ) {
314
+ command . middlewares . push ( id ) ;
315
+ }
316
+ } else {
317
+ // Specific middleware only applies to exact command match
318
+ const commandName = command . name ;
319
+ const middlewareName = path
320
+ . basename ( file )
321
+ . replace ( / \. m i d d l e w a r e \. ( m | c ) ? ( j | t ) s x ? $ / , '' ) ;
322
+ if ( commandName === middlewareName && commandDir === dirname ) {
323
+ command . middlewares . push ( id ) ;
324
+ }
325
+ }
309
326
}
310
327
}
311
328
@@ -357,4 +374,45 @@ export class CommandsRouter {
357
374
358
375
return entries ;
359
376
}
377
+
378
+ private isIgnoredFile ( filename : string ) : boolean {
379
+ return filename . startsWith ( '_' ) ;
380
+ }
381
+
382
+ private isCommandFile ( path : string ) : boolean {
383
+ if ( this . execMatcher ( this . matchers . middleware , path ) ) return false ;
384
+ return (
385
+ / i n d e x \. ( m | c ) ? ( j | t ) s x ? $ / . test ( path ) || / \. ( m | c ) ? ( j | t ) s x ? $ / . test ( path )
386
+ ) ;
387
+ }
388
+
389
+ private parseCategories ( parts : string [ ] ) : string [ ] {
390
+ return parts
391
+ . filter ( ( part ) => part . startsWith ( '(' ) && part . endsWith ( ')' ) )
392
+ . map ( ( part ) => part . slice ( 1 , - 1 ) ) ;
393
+ }
394
+
395
+ private parseCommandPath ( filepath : string ) : {
396
+ name : string ;
397
+ category : string | null ;
398
+ parent : string | null ;
399
+ parentSegments : string [ ] ;
400
+ } {
401
+ const location = this . resolveRelativePath ( filepath ) ;
402
+ const parts = location . split ( path . sep ) . filter ( ( p ) => p ) ;
403
+ const categories = this . parseCategories ( parts ) ;
404
+ const segments : string [ ] = parts . filter (
405
+ ( part ) => ! ( part . startsWith ( '(' ) && part . endsWith ( ')' ) ) ,
406
+ ) ;
407
+
408
+ let name = segments . pop ( ) || '' ;
409
+ name = name . replace ( / \. ( m | c ) ? ( j | t ) s x ? $ / , '' ) . replace ( / ^ i n d e x $ / , '' ) ;
410
+
411
+ return {
412
+ name,
413
+ category : categories . length ? categories . join ( '/' ) : null ,
414
+ parent : segments . length ? segments . join ( ' ' ) : null ,
415
+ parentSegments : segments ,
416
+ } ;
417
+ }
360
418
}
0 commit comments