Skip to content

Commit 2d0e9a4

Browse files
committed
refactor: update command router and cli
1 parent 4bdf3c9 commit 2d0e9a4

File tree

23 files changed

+422
-687
lines changed

23 files changed

+422
-687
lines changed

apps/test-bot/commandkit.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
import { defineConfig } from 'commandkit';
22

3-
export default defineConfig({});
3+
export default defineConfig();

packages/commandkit/src/app/router/CommandsRouter.ts

Lines changed: 111 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ export interface ParsedCommand {
5151
middlewares: string[];
5252
/** Absolute path to this command */
5353
fullPath: string;
54+
/** Category the command belongs to, if any */
55+
category: string | null;
5456
}
5557

5658
/**
@@ -66,6 +68,8 @@ export interface ParsedMiddleware {
6668
path: string;
6769
/** Absolute path to the middleware file */
6870
fullPath: string;
71+
/** Category the middleware belongs to, if any */
72+
category: string | null;
6973
}
7074

7175
/**
@@ -244,68 +248,81 @@ export class CommandsRouter {
244248
* @returns Promise resolving to the complete commands tree
245249
*/
246250
public async scan(): Promise<CommandsTree> {
251+
this.clear();
247252
const files = await this.scanDirectory(this.entrypoint, []);
248253

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+
});
279259

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+
};
282279

283-
if (this.execMatcher(this.matchers.middleware, file)) {
284-
const location = this.resolveRelativePath(file);
285-
const name = location.replace(/\.(m|c)?(j|t)sx?$/, '');
286-
const middlewareDir = path.dirname(location);
280+
this.commands.set(parsedPath.name, command);
281+
}
287282

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+
};
294298

295-
const id = crypto.randomUUID();
299+
this.middlewares.set(id, middleware);
296300

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());
303304

304-
this.middlewares.set(id, middleware);
305+
for (const command of commands) {
306+
const commandDir = path.dirname(command.path);
305307

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(/\.middleware\.(m|c)?(j|t)sx?$/, '');
322+
if (commandName === middlewareName && commandDir === dirname) {
323+
command.middlewares.push(id);
324+
}
325+
}
309326
}
310327
}
311328

@@ -357,4 +374,45 @@ export class CommandsRouter {
357374

358375
return entries;
359376
}
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+
/index\.(m|c)?(j|t)sx?$/.test(path) || /\.(m|c)?(j|t)sx?$/.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)sx?$/, '').replace(/^index$/, '');
410+
411+
return {
412+
name,
413+
category: categories.length ? categories.join('/') : null,
414+
parent: segments.length ? segments.join(' ') : null,
415+
parentSegments: segments,
416+
};
417+
}
360418
}

0 commit comments

Comments
 (0)