Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/code-quality.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,11 @@ jobs:
- name: Check TypeScript Types
run: pnpm dlx turbo check-types

- name: Run CommandKit Command Handler Tests
run: pnpm test:commandkit

- name: Check Generated API Docs
run: pnpm docgen:check

- name: Check Prettier Formatting
run: pnpm prettier:check
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,14 @@ preserve convention-based discovery:
- Config file at root: `commandkit.config.ts` (or `.js`)
- App entrypoint: `src/app.ts` (exports discord.js client)
- Commands: `src/app/commands/**`
- Hierarchical Discovery Tokens:
- `[command]` - top-level command directory
- `{group}` - subcommand group directory
- `(category)` - organizational category directory
- `command.ts` / `group.ts` - definition files
- `<name>.subcommand.ts` - subcommand shorthand
- Events: `src/app/events/**`

- Optional feature paths:
- i18n: `src/app/locales/**`
- tasks: `src/app/tasks/**`
Expand Down
1 change: 1 addition & 0 deletions apps/test-bot/src/app/commands/(general)/(animal)/cat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const command: CommandData = {
export const metadata: CommandMetadata = {
nameAliases: {
message: 'Cat Message',
user: 'Cat User',
},
};

Expand Down
27 changes: 27 additions & 0 deletions apps/test-bot/src/app/commands/(general)/help.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CommandData, ChatInputCommand } from 'commandkit';
import { redEmbedColor } from '@/feature-flags/red-embed-color';
import { toMessageRoute, toSlashRoute } from '@/utils/hierarchical-demo';
import { Colors } from 'discord.js';

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

const hierarchicalRoutes = ctx.commandkit.commandHandler
.getRuntimeCommandsArray()
.map((command) => {
return (
(command.data.command as Record<string, any>).__routeKey ??
command.data.command.name
);
})
.filter((route) => route.includes('.'))
.sort()
.map((route) => `${toSlashRoute(route)} | ${toMessageRoute(route)}`)
.join('\n');

return interaction.editReply({
embeds: [
{
title: 'Help',
description: commands,
fields: hierarchicalRoutes
? [
{
name: 'Hierarchical Routes',
value: hierarchicalRoutes,
},
{
name: 'Hierarchical Middleware',
value:
'Global middleware always runs first. Hierarchical leaves then use only the current directory `+middleware` and any same-directory `+<command>.middleware`.',
},
]
: undefined,
footer: {
text: `Bot Version: ${botVersion} | Shard ID ${interaction.guild?.shardId ?? 'N/A'}`,
},
Expand Down
6 changes: 6 additions & 0 deletions apps/test-bot/src/app/commands/(hierarchical)/+middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { MiddlewareContext } from 'commandkit';
import { recordHierarchyStage } from '@/utils/hierarchical-demo';

export function beforeExecute(ctx: MiddlewareContext) {
recordHierarchyStage(ctx, 'category:(hierarchical)');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { MiddlewareContext } from 'commandkit';
import { recordHierarchyStage } from '@/utils/hierarchical-demo';

export function beforeExecute(ctx: MiddlewareContext) {
recordHierarchyStage(ctx, 'root:ops');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { MiddlewareContext } from 'commandkit';
import { recordHierarchyStage } from '@/utils/hierarchical-demo';

export function beforeExecute(ctx: MiddlewareContext) {
recordHierarchyStage(ctx, 'command:status');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { CommandData } from 'commandkit';

export const command: CommandData = {
name: 'ops',
description:
'Direct-subcommand hierarchical root. Try /ops status or /ops deploy.',
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { MiddlewareContext } from 'commandkit';
import { recordHierarchyStage } from '@/utils/hierarchical-demo';

export function beforeExecute(ctx: MiddlewareContext) {
recordHierarchyStage(ctx, 'leaf-dir:deploy');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { ApplicationCommandOptionType } from 'discord.js';
import {
ChatInputCommandContext,
CommandData,
MessageCommandContext,
} from 'commandkit';
import { replyWithHierarchyDemo } from '@/utils/hierarchical-demo';

export const command: CommandData = {
name: 'deploy',
description: 'Run a folder-based direct subcommand under the root.',
options: [
{
name: 'environment',
description: 'Where the deployment should go',
type: ApplicationCommandOptionType.String,
required: false,
choices: [
{ name: 'staging', value: 'staging' },
{ name: 'production', value: 'production' },
],
},
{
name: 'dry_run',
description: 'Whether to simulate the deployment',
type: ApplicationCommandOptionType.Boolean,
required: false,
},
],
};

async function execute(ctx: ChatInputCommandContext | MessageCommandContext) {
const environment = ctx.options.getString('environment') ?? 'staging';
const dryRun = ctx.options.getBoolean('dry_run') ?? true;

return replyWithHierarchyDemo(ctx, {
title: 'Ops Deploy',
shape: 'root command -> direct subcommand',
leafStyle: 'folder leaf ([deploy]/command.ts)',
summary:
'Shows a direct folder-based subcommand with middleware scoped only to the leaf directory, plus the same prefix route syntax.',
details: [`environment: ${environment}`, `dry_run: ${dryRun}`],
});
}

export const chatInput = execute;
export const message = execute;
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { ApplicationCommandOptionType } from 'discord.js';
import {
ChatInputCommandContext,
CommandData,
MessageCommandContext,
} from 'commandkit';
import { replyWithHierarchyDemo } from '@/utils/hierarchical-demo';

export const command: CommandData = {
name: 'status',
description: 'Inspect a direct shorthand subcommand under the root.',
options: [
{
name: 'scope',
description: 'Which subsystem to inspect',
type: ApplicationCommandOptionType.String,
required: false,
choices: [
{ name: 'bot', value: 'bot' },
{ name: 'database', value: 'database' },
{ name: 'workers', value: 'workers' },
],
},
],
};

async function execute(ctx: ChatInputCommandContext | MessageCommandContext) {
const scope = ctx.options.getString('scope') ?? 'bot';

return replyWithHierarchyDemo(ctx, {
title: 'Ops Status',
shape: 'root command -> direct subcommand',
leafStyle: 'shorthand file (status.subcommand.ts)',
summary:
'Shows a direct subcommand branch without groups, where the leaf shares the root command directory and therefore uses that directory middleware plus same-directory command middleware.',
details: [`scope: ${scope}`],
});
}

export const chatInput = execute;
export const message = execute;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { MiddlewareContext } from 'commandkit';
import { recordHierarchyStage } from '@/utils/hierarchical-demo';

export function beforeExecute(ctx: MiddlewareContext) {
recordHierarchyStage(ctx, 'root:workspace');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { CommandData } from 'commandkit';

export const command: CommandData = {
name: 'workspace',
description:
'Grouped hierarchical root. Try /workspace notes add or /workspace team handoff.',
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { MiddlewareContext } from 'commandkit';
import { recordHierarchyStage } from '@/utils/hierarchical-demo';

export function beforeExecute(ctx: MiddlewareContext) {
recordHierarchyStage(ctx, 'command:add');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { MiddlewareContext } from 'commandkit';
import { recordHierarchyStage } from '@/utils/hierarchical-demo';

export function beforeExecute(ctx: MiddlewareContext) {
recordHierarchyStage(ctx, 'group:notes');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { ApplicationCommandOptionType } from 'discord.js';
import {
ChatInputCommandContext,
CommandData,
MessageCommandContext,
} from 'commandkit';
import { replyWithHierarchyDemo } from '@/utils/hierarchical-demo';

export const command: CommandData = {
name: 'add',
description: 'Create a note using a grouped shorthand subcommand.',
options: [
{
name: 'title',
description: 'Title for the note',
type: ApplicationCommandOptionType.String,
required: false,
},
{
name: 'topic',
description: 'Topic bucket for the note',
type: ApplicationCommandOptionType.String,
required: false,
},
],
};

async function execute(ctx: ChatInputCommandContext | MessageCommandContext) {
const title = ctx.options.getString('title') ?? 'untitled';
const topic = ctx.options.getString('topic') ?? 'general';

return replyWithHierarchyDemo(ctx, {
title: 'Workspace Notes Add',
shape: 'root command -> group -> subcommand',
leafStyle: 'shorthand file (add.subcommand.ts)',
summary:
'Shows a grouped shorthand leaf that uses only middleware from the current group directory, including same-directory command middleware.',
details: [`title: ${title}`, `topic: ${topic}`],
});
}

export const chatInput = execute;
export const message = execute;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { MiddlewareContext } from 'commandkit';
import { recordHierarchyStage } from '@/utils/hierarchical-demo';

export function beforeExecute(ctx: MiddlewareContext) {
recordHierarchyStage(ctx, 'leaf-dir:archive');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { ApplicationCommandOptionType } from 'discord.js';
import {
ChatInputCommandContext,
CommandData,
MessageCommandContext,
} from 'commandkit';
import { replyWithHierarchyDemo } from '@/utils/hierarchical-demo';

export const command: CommandData = {
name: 'archive',
description: 'Archive a note using a folder-based grouped subcommand.',
options: [
{
name: 'note',
description: 'The note name to archive',
type: ApplicationCommandOptionType.String,
required: false,
},
{
name: 'reason',
description: 'Why the note is being archived',
type: ApplicationCommandOptionType.String,
required: false,
},
],
};

async function execute(ctx: ChatInputCommandContext | MessageCommandContext) {
const note = ctx.options.getString('note') ?? 'sprint-plan';
const reason = ctx.options.getString('reason') ?? 'cleanup';

return replyWithHierarchyDemo(ctx, {
title: 'Workspace Notes Archive',
shape: 'root command -> group -> subcommand',
leafStyle: 'folder leaf ([archive]/command.ts)',
summary:
'Shows a grouped leaf discovered from a nested command directory with middleware scoped only to that leaf directory.',
details: [`note: ${note}`, `reason: ${reason}`],
});
}

export const chatInput = execute;
export const message = execute;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { CommandData } from 'commandkit';

export const command: CommandData = {
name: 'notes',
description: 'Workspace note operations grouped under a root command.',
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { MiddlewareContext } from 'commandkit';
import { recordHierarchyStage } from '@/utils/hierarchical-demo';

export function beforeExecute(ctx: MiddlewareContext) {
recordHierarchyStage(ctx, 'group:team');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { CommandData } from 'commandkit';

export const command: CommandData = {
name: 'team',
description: 'Team-oriented subcommands under the workspace root.',
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { ApplicationCommandOptionType } from 'discord.js';
import {
ChatInputCommandContext,
CommandData,
MessageCommandContext,
} from 'commandkit';
import { replyWithHierarchyDemo } from '@/utils/hierarchical-demo';

export const command: CommandData = {
name: 'handoff',
description: 'Hand work over to a teammate from a second group branch.',
options: [
{
name: 'owner',
description: 'Who is taking over the work',
type: ApplicationCommandOptionType.String,
required: false,
},
{
name: 'project',
description: 'Which project is being handed off',
type: ApplicationCommandOptionType.String,
required: false,
},
],
};

async function execute(ctx: ChatInputCommandContext | MessageCommandContext) {
const owner = ctx.options.getString('owner') ?? 'alex';
const project = ctx.options.getString('project') ?? 'migration';

return replyWithHierarchyDemo(ctx, {
title: 'Workspace Team Handoff',
shape: 'root command -> sibling group -> subcommand',
leafStyle: 'shorthand file (handoff.subcommand.ts)',
summary:
'Shows that one root can host multiple groups while each leaf still resolves through the same route index and only uses middleware from its own group directory.',
details: [`owner: ${owner}`, `project: ${project}`],
});
}

export const chatInput = execute;
export const message = execute;
Loading