Skip to content
Merged
5 changes: 5 additions & 0 deletions .changeset/subcommands-advanced-plugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@wolfstar/plugin-subcommands-advanced": major
---

Add `@wolfstar/plugin-subcommands-advanced`: modular slash subcommands as separate command classes for `@wolfstar/http-framework`, adapted from `@kaname-png/plugin-subcommands-advanced`.
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,17 @@

### Commands (defined in root `package.json`)

- `pnpm build` — `turbo run build` (tsdown → `dist/`).
- `pnpm build` — `turbo run build` (tsdown → `dist/esm/`, shared options in `scripts/tsdown.config.ts`).
- `pnpm test` — `vitest run` (unit + in-process HTTP integration tests).
- `pnpm typecheck` — `turbo run typecheck` (`tsc --noEmit`).
- `pnpm lint` / `pnpm lint:fix` — oxlint + oxfmt.
- Turbo `test`/`typecheck` tasks `dependsOn: ["^build"]`, so a build is triggered as needed.

### Gotchas

- `pnpm clean` is broken: it runs `node scripts/clean.mjs`, but `scripts/` does not exist in the repo. Do not rely on it.
- `pnpm clean` is broken: it runs `node scripts/clean.mjs`, but that file does not exist (only `scripts/tsdown.config.ts` is present). Do not rely on it.
- Git hooks are active (husky): `pre-commit` runs nano-staged (oxfmt + `oxlint --fix`) and `commit-msg` runs commitlint. Commit messages **must** follow Conventional Commits.
- Vitest is pinned to Vite 6 via `pnpm-workspace.yaml` overrides so TypeScript experimental decorators still transform through esbuild (Vite 8 / oxc does not).

### Exercising the core functionality (ApiServer)

Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@

## Packages

| Package | Version | Downloads |
| ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| [`@wolfstar/plugin-api`](https://npmx.dev/package/@wolfstar/plugin-api) | [![version](https://npmx.dev/api/registry/badge/version/@wolfstar/plugin-api)](https://npmx.dev/package/@wolfstar/plugin-api) | [![downloads](https://npmx.dev/api/registry/badge/downloads/@wolfstar/plugin-api)](https://npmx.dev/package/@wolfstar/plugin-api) |
| Package | Version | Downloads |
| --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`@wolfstar/plugin-api`](https://npmx.dev/package/@wolfstar/plugin-api) | [![version](https://npmx.dev/api/registry/badge/version/@wolfstar/plugin-api)](https://npmx.dev/package/@wolfstar/plugin-api) | [![downloads](https://npmx.dev/api/registry/badge/downloads/@wolfstar/plugin-api)](https://npmx.dev/package/@wolfstar/plugin-api) |
| [`@wolfstar/plugin-subcommands-advanced`](https://npmx.dev/package/@wolfstar/plugin-subcommands-advanced) | [![version](https://npmx.dev/api/registry/badge/version/@wolfstar/plugin-subcommands-advanced)](https://npmx.dev/package/@wolfstar/plugin-subcommands-advanced) | [![downloads](https://npmx.dev/api/registry/badge/downloads/@wolfstar/plugin-subcommands-advanced)](https://npmx.dev/package/@wolfstar/plugin-subcommands-advanced) |

---

Expand Down
21 changes: 13 additions & 8 deletions packages/plugin-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,26 +21,31 @@
],
"type": "module",
"sideEffects": [
"./dist/register.js"
"./dist/esm/register.js"
],
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"main": "./dist/esm/index.js",
"module": "./dist/esm/index.js",
"types": "./dist/esm/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
"import": {
"types": "./dist/esm/index.d.ts",
"default": "./dist/esm/index.js"
}
},
"./register": {
"types": "./dist/register.d.ts",
"import": "./dist/register.js"
"import": {
"types": "./dist/esm/register.d.ts",
"default": "./dist/esm/register.js"
}
}
},
"publishConfig": {
"access": "public",
"provenance": true
},
"scripts": {
"build": "tsdown",
"build": "tsdown --config-loader unrun",
"typecheck": "tsc --noEmit",
"lint": "oxlint src --fix",
"test": "vitest run",
Expand Down
16 changes: 6 additions & 10 deletions packages/plugin-api/tsdown.config.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import { defineConfig } from "tsdown";
import { createTsdownOptions } from "../../scripts/tsdown.config";

export default defineConfig({
entry: ["src/index.ts", "src/register.ts"],
format: "esm",
target: "es2022",
dts: true,
clean: true,
sourcemap: true,
fixedExtension: false,
outDir: "dist",
});
export default defineConfig(
createTsdownOptions({
entry: ["src/index.ts", "src/register.ts"],
}),
);
7 changes: 7 additions & 0 deletions packages/plugin-subcommands-advanced/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# @wolfstar/plugin-subcommands-advanced

## 1.0.0

### Major Changes

- Initial release: modular slash subcommands as separate command classes for `@wolfstar/http-framework`.
124 changes: 124 additions & 0 deletions packages/plugin-subcommands-advanced/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# `@wolfstar/plugin-subcommands-advanced`

Plugin for [`@wolfstar/http-framework`](https://www.npmjs.com/package/@wolfstar/http-framework) that lets you split slash **subcommands** (and subcommand groups) into **separate command classes**, instead of putting every handler method on the parent.

Adapted from [`@kaname-png/plugin-subcommands-advanced`](https://github.com/sawa-ko/neko-plugins/tree/main/packages/subcommands-advanced) for WolfStar’s HTTP interaction framework.

## Installation

```bash
pnpm add @wolfstar/http-framework @wolfstar/plugin-subcommands-advanced
```

## Usage

Import the register entrypoint **before** creating the client:

```typescript
import "@wolfstar/plugin-subcommands-advanced/register";
import { Client, RegisterCommand } from "@wolfstar/http-framework";
import {
Command,
Subcommand,
RegisterAsSubcommand,
RegisterAsSubcommandGroup,
} from "@wolfstar/plugin-subcommands-advanced";

const client = new Client({
subcommandsAdvanced: {
// optional: piece names become parent/sub or parent/group/sub
nameCommandsAutogenerated: true,
},
});
```

Recommended layout:

```text
commands/
└── utils/
├── parent.ts // parent chat-input command
├── ping.ts // subcommand
└── poll/
└── create.ts // grouped subcommand
```

### Parent command

```typescript
import { RegisterCommand } from "@wolfstar/http-framework";
import { Subcommand } from "@wolfstar/plugin-subcommands-advanced";

@RegisterCommand((builder) =>
builder
.setName("utils")
.setDescription("Utility commands")
.addSubcommandGroup((group) => group.setName("poll").setDescription("Poll tools")),
)
export class UtilsCommand extends Subcommand {}
```

### Direct subcommand

```typescript
import { Command, RegisterAsSubcommand } from "@wolfstar/plugin-subcommands-advanced";

@RegisterAsSubcommand("utils", (builder) => builder.setName("ping").setDescription("Ping the bot"))
export class PingCommand extends Command {
public override chatInputRun(interaction: Command.ChatInputInteraction) {
return interaction.reply({ content: "Pong!" });
}
}
```

### Grouped subcommand

```typescript
import { Command, RegisterAsSubcommandGroup } from "@wolfstar/plugin-subcommands-advanced";

@RegisterAsSubcommandGroup("utils", "poll", (builder) =>
builder.setName("create").setDescription("Create a poll"),
)
export class PollCreateCommand extends Command {
public override chatInputRun(interaction: Command.ChatInputInteraction) {
return interaction.reply({ content: "Created!" });
}
}
```

### Constructor options (no decorators)

```typescript
import { Command } from "@wolfstar/plugin-subcommands-advanced";

export class PingCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
registerSubCommand: {
parentCommandName: "utils",
slashSubcommand: (builder) => builder.setName("ping").setDescription("Ping!"),
},
});
}

public override chatInputRun(interaction: Command.ChatInputInteraction) {
return interaction.reply({ content: "Pong!" });
}
}
```

## How it works

`@wolfstar/http-framework` routes subcommands to **methods on the parent** command instance. This plugin:

1. Lets child command classes register themselves into in-memory registries (by parent name).
2. After all command pieces are constructed, rebuilds the parent’s chat-input resolver and `CommandRouter`, installing thin delegate methods that call each child’s `chatInputRun`.

You do not need to call the optional `hooks.subcommands` / `hooks.groups` helpers unless you are registering the parent imperatively and want to attach builders yourself.

## Notes

- HTTP-only: there is no message-command support (unlike the Sapphire original).
- Child classes should **not** use `@RegisterCommand` — only the parent registers the top-level slash command.
- Subcommand groups must still be declared on the parent (via `@RegisterCommand` builder or `registerApplicationCommands`).
72 changes: 72 additions & 0 deletions packages/plugin-subcommands-advanced/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{
"name": "@wolfstar/plugin-subcommands-advanced",
"version": "1.0.0",
"description": "Plugin for @wolfstar/http-framework to modularize slash subcommands into separate command classes",
"keywords": [
"discord",
"http-framework",
"plugin",
"subcommands",
"wolfstar"
],
"homepage": "https://github.com/wolfstar-project/plugins/tree/main/packages/plugin-subcommands-advanced",
"bugs": {
"url": "https://github.com/wolfstar-project/plugins/issues"
},
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/wolfstar-project/plugins.git",
"directory": "packages/plugin-subcommands-advanced"
},
"files": [
"dist"
],
"type": "module",
"sideEffects": [
"./dist/esm/register.js"
],
"main": "./dist/esm/index.js",
"module": "./dist/esm/index.js",
"types": "./dist/esm/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/esm/index.d.ts",
"default": "./dist/esm/index.js"
}
},
"./register": {
"import": {
"types": "./dist/esm/register.d.ts",
"default": "./dist/esm/register.js"
}
}
},
"publishConfig": {
"access": "public",
"provenance": true
},
"scripts": {
"build": "tsdown --config-loader unrun",
"typecheck": "tsc --noEmit",
"lint": "oxlint src --fix",
"test": "vitest run",
"prepack": "pnpm build"
},
"dependencies": {
"@discordjs/builders": "^1.13.0",
"@discordjs/collection": "^2.1.1",
"discord-api-types": "^0.38.33"
},
"devDependencies": {
"@wolfstar/http-framework": "^3.1.0",
"@wolfstar/http-framework-test-utils": "^3.0.1"
},
"peerDependencies": {
"@wolfstar/http-framework": "^3.1.0"
},
"engines": {
"node": ">=20.0.0"
}
}
37 changes: 37 additions & 0 deletions packages/plugin-subcommands-advanced/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
export { Command, Subcommand } from "./lib/structures/command.js";
export { RegisterAsSubcommand, RegisterAsSubcommandGroup } from "./lib/utils/decorators.js";
export {
analyzeSubCommandParsed,
analyzeSubcommandGroupParsed,
parseSlashSubcommand,
subCommandsGroupRegistry,
subCommandsRegistry,
subcommandGroupMethodName,
subcommandMethodName,
} from "./lib/utils/functions.js";
export { RegisterSubcommandsHooks } from "./lib/utils/hooks.js";
export { SubcommandsAdvancedLoaderStrategy } from "./lib/utils/strategy.js";
export type {
PluginSubcommandOptions,
RegisterSubCommandGroupOptions,
RegisterSubCommandOptions,
SlashSubcommandResolvable,
SubcommandCommandOptions,
SubcommandMappingCollection,
} from "./lib/utils/types.js";
export {
clearSubcommandRegistries,
hasRegisteredSubcommands,
wireParentSubcommands,
} from "./lib/utils/wiring.js";

import type { PluginSubcommandOptions } from "./lib/utils/types.js";

declare module "@wolfstar/http-framework" {
interface ClientOptions {
/**
* Options for `@wolfstar/plugin-subcommands-advanced`.
*/
subcommandsAdvanced?: PluginSubcommandOptions;
}
}
Loading