Skip to content

Commit ac4e2c5

Browse files
AboMeezOtwlite
andauthored
feat: implement filesystem-based hierarchical subcommand routing (#623)
* feat: add hierarchical command proposal docs and initial test scaffolding for command parsing and routing * feat: implement filesystem-based command router with tree-based path resolution and middleware support * feat: implement hierarchical command routing and registration system with middleware support * feat: implement hierarchical command structure with middleware tracing and demo utilities * fromat & docs update * chore: update path-to-regexp security override in package.json and lockfile * feat: implement CommandsRouter for filesystem-based command and middleware discovery * feat: implement AppCommandHandler and CommandsRouter for enhanced command management and routing * feat: implement AppCommandHandler, CommandsRouter, and development CLI utilities for enhanced command management * Refactor command handling system to dynamically support arbitrary subcommand nesting depth across parsers, handlers, and execution contexts. * docs: update skills and operational guides to support hierarchical command discovery and routing conventions * Fix command handler reloads, middleware scope, and docs * Normalize generated docs and reject empty command bodies - Normalize docs output line endings before writing - Treat prefix-only message inputs as invalid command prefixes - Update parser tests and generated API docs --------- Co-authored-by: Twilight <46562212+twlite@users.noreply.github.com>
1 parent 0cf06a7 commit ac4e2c5

90 files changed

Lines changed: 4086 additions & 487 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/code-quality.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,11 @@ jobs:
3030
- name: Check TypeScript Types
3131
run: pnpm dlx turbo check-types
3232

33+
- name: Run CommandKit Command Handler Tests
34+
run: pnpm test:commandkit
35+
36+
- name: Check Generated API Docs
37+
run: pnpm docgen:check
38+
3339
- name: Check Prettier Formatting
3440
run: pnpm prettier:check

AGENTS.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,14 @@ preserve convention-based discovery:
3737
- Config file at root: `commandkit.config.ts` (or `.js`)
3838
- App entrypoint: `src/app.ts` (exports discord.js client)
3939
- Commands: `src/app/commands/**`
40+
- Hierarchical Discovery Tokens:
41+
- `[command]` - top-level command directory
42+
- `{group}` - subcommand group directory
43+
- `(category)` - organizational category directory
44+
- `command.ts` / `group.ts` - definition files
45+
- `<name>.subcommand.ts` - subcommand shorthand
4046
- Events: `src/app/events/**`
47+
4148
- Optional feature paths:
4249
- i18n: `src/app/locales/**`
4350
- tasks: `src/app/tasks/**`

apps/test-bot/src/app/commands/(general)/(animal)/cat.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export const command: CommandData = {
2828
export const metadata: CommandMetadata = {
2929
nameAliases: {
3030
message: 'Cat Message',
31+
user: 'Cat User',
3132
},
3233
};
3334

apps/test-bot/src/app/commands/(general)/help.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { CommandData, ChatInputCommand } from 'commandkit';
22
import { redEmbedColor } from '@/feature-flags/red-embed-color';
3+
import { toMessageRoute, toSlashRoute } from '@/utils/hierarchical-demo';
34
import { Colors } from 'discord.js';
45

56
export const command: CommandData = {
@@ -34,11 +35,37 @@ export const chatInput: ChatInputCommand = async (ctx) => {
3435
})
3536
.join('\n');
3637

38+
const hierarchicalRoutes = ctx.commandkit.commandHandler
39+
.getRuntimeCommandsArray()
40+
.map((command) => {
41+
return (
42+
(command.data.command as Record<string, any>).__routeKey ??
43+
command.data.command.name
44+
);
45+
})
46+
.filter((route) => route.includes('.'))
47+
.sort()
48+
.map((route) => `${toSlashRoute(route)} | ${toMessageRoute(route)}`)
49+
.join('\n');
50+
3751
return interaction.editReply({
3852
embeds: [
3953
{
4054
title: 'Help',
4155
description: commands,
56+
fields: hierarchicalRoutes
57+
? [
58+
{
59+
name: 'Hierarchical Routes',
60+
value: hierarchicalRoutes,
61+
},
62+
{
63+
name: 'Hierarchical Middleware',
64+
value:
65+
'Global middleware always runs first. Hierarchical leaves then use only the current directory `+middleware` and any same-directory `+<command>.middleware`.',
66+
},
67+
]
68+
: undefined,
4269
footer: {
4370
text: `Bot Version: ${botVersion} | Shard ID ${interaction.guild?.shardId ?? 'N/A'}`,
4471
},
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { MiddlewareContext } from 'commandkit';
2+
import { recordHierarchyStage } from '@/utils/hierarchical-demo';
3+
4+
export function beforeExecute(ctx: MiddlewareContext) {
5+
recordHierarchyStage(ctx, 'category:(hierarchical)');
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { MiddlewareContext } from 'commandkit';
2+
import { recordHierarchyStage } from '@/utils/hierarchical-demo';
3+
4+
export function beforeExecute(ctx: MiddlewareContext) {
5+
recordHierarchyStage(ctx, 'root:ops');
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { MiddlewareContext } from 'commandkit';
2+
import { recordHierarchyStage } from '@/utils/hierarchical-demo';
3+
4+
export function beforeExecute(ctx: MiddlewareContext) {
5+
recordHierarchyStage(ctx, 'command:status');
6+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { CommandData } from 'commandkit';
2+
3+
export const command: CommandData = {
4+
name: 'ops',
5+
description:
6+
'Direct-subcommand hierarchical root. Try /ops status or /ops deploy.',
7+
};
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { MiddlewareContext } from 'commandkit';
2+
import { recordHierarchyStage } from '@/utils/hierarchical-demo';
3+
4+
export function beforeExecute(ctx: MiddlewareContext) {
5+
recordHierarchyStage(ctx, 'leaf-dir:deploy');
6+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { ApplicationCommandOptionType } from 'discord.js';
2+
import {
3+
ChatInputCommandContext,
4+
CommandData,
5+
MessageCommandContext,
6+
} from 'commandkit';
7+
import { replyWithHierarchyDemo } from '@/utils/hierarchical-demo';
8+
9+
export const command: CommandData = {
10+
name: 'deploy',
11+
description: 'Run a folder-based direct subcommand under the root.',
12+
options: [
13+
{
14+
name: 'environment',
15+
description: 'Where the deployment should go',
16+
type: ApplicationCommandOptionType.String,
17+
required: false,
18+
choices: [
19+
{ name: 'staging', value: 'staging' },
20+
{ name: 'production', value: 'production' },
21+
],
22+
},
23+
{
24+
name: 'dry_run',
25+
description: 'Whether to simulate the deployment',
26+
type: ApplicationCommandOptionType.Boolean,
27+
required: false,
28+
},
29+
],
30+
};
31+
32+
async function execute(ctx: ChatInputCommandContext | MessageCommandContext) {
33+
const environment = ctx.options.getString('environment') ?? 'staging';
34+
const dryRun = ctx.options.getBoolean('dry_run') ?? true;
35+
36+
return replyWithHierarchyDemo(ctx, {
37+
title: 'Ops Deploy',
38+
shape: 'root command -> direct subcommand',
39+
leafStyle: 'folder leaf ([deploy]/command.ts)',
40+
summary:
41+
'Shows a direct folder-based subcommand with middleware scoped only to the leaf directory, plus the same prefix route syntax.',
42+
details: [`environment: ${environment}`, `dry_run: ${dryRun}`],
43+
});
44+
}
45+
46+
export const chatInput = execute;
47+
export const message = execute;

0 commit comments

Comments
 (0)