fmt
vi did:web:vt3e.cat
Fri, 12 Jun 2026 04:28:22 +0100
12 files changed,
903 insertions(+),
901 deletions(-)
M
.oxfmtrc.json
→
.oxfmtrc.json
@@ -1,5 +1,5 @@
{ - "$schema": "./node_modules/oxfmt/configuration_schema.json", - "ignorePatterns": [], - "useTabs": true + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "ignorePatterns": [], + "useTabs": true }
M
DESIGN.md
→
DESIGN.md
@@ -11,15 +11,15 @@ you get awesome typesafety in the `run` callback.
```ts const whois = kitten.command("whois", { - description: "get information about a user", - options: { - user: option.user("the user to get information about", { required: true }), - ephemeral: option.boolean("whether to hide the response"), - }, - async run(interaction, { user, ephemeral }) { - // user: GuildMember/User - // ephemeral: boolean | undefined - }, + description: "get information about a user", + options: { + user: option.user("the user to get information about", { required: true }), + ephemeral: option.boolean("whether to hide the response"), + }, + async run(interaction, { user, ephemeral }) { + // user: GuildMember/User + // ephemeral: boolean | undefined + }, }); ```@@ -27,17 +27,17 @@ subcommands are similar.
```ts const command = kitten.command("command", { - description: "a command with subcommands", + description: "a command with subcommands", }); command.subcommand("sub1", { - description: "the first subcommand", - options: { - user: option.user("a user option", { required: true }), - }, - async run(interaction, { user }) { - // user: GuildMember/User - }, + description: "the first subcommand", + options: { + user: option.user("a user option", { required: true }), + }, + async run(interaction, { user }) { + // user: GuildMember/User + }, }); ```@@ -49,17 +49,15 @@ buttons, modals, and select menus work similarly.
```ts const banButton = kitten.button("close-ticket", { - options: { - ticketId: option.string(), - }, - async run(interaction, { ticketId }) { - // ticketId: string | undefined - }, + options: { + ticketId: option.string(), + }, + async run(interaction, { ticketId }) { + // ticketId: string | undefined + }, }); -const button = new ButtonBuilder().setCustomId( - banButton.id({ ticketId: "1234" }), -); +const button = new ButtonBuilder().setCustomId(banButton.id({ ticketId: "1234" })); ``` this would serialise to `close-ticket:1234`@@ -70,17 +68,17 @@ you can deifne autocomplete handlers inside of optino definitions.
```ts const repoSearch = kitten.command("repo", { - description: "search a github repository", - options: { - name: option.string("the repository name", { - required: true, - async autocomplete(interaction, value) { - const matches = await searchRepos(value); - return matches.map((r) => ({ name: r.name, value: r.id })); - }, - }), - }, - async run(interaction, { name }) {}, + description: "search a github repository", + options: { + name: option.string("the repository name", { + required: true, + async autocomplete(interaction, value) { + const matches = await searchRepos(value); + return matches.map((r) => ({ name: r.name, value: r.id })); + }, + }), + }, + async run(interaction, { name }) {}, }); ```@@ -90,28 +88,28 @@ ```ts
const base = kitten.builder(); const authed = base - .use(async (interaction) => { - const dbUser = await db.getUser(interaction.user.id); + .use(async (interaction) => { + const dbUser = await db.getUser(interaction.user.id); - if (!dbUser) { - await interaction.reply({ content: "no account found", ephemeral: true }); - throw new HaltExecution(); - } + if (!dbUser) { + await interaction.reply({ content: "no account found", ephemeral: true }); + throw new HaltExecution(); + } - return { dbUser }; - }) - .use(async (interaction, ctx) => { - // ctx.dbUser: DbUser - const isPremium = ctx.dbUser.subscription === "premium"; - return { isPremium }; - }); + return { dbUser }; + }) + .use(async (interaction, ctx) => { + // ctx.dbUser: DbUser + const isPremium = ctx.dbUser.subscription === "premium"; + return { isPremium }; + }); const premiumStats = authed.command("meow", { - description: "meow", - async run(interaction, args, ctx) { - // ctx.dbUser is typed - // ctx.isPremium is typed - }, + description: "meow", + async run(interaction, args, ctx) { + // ctx.dbUser is typed + // ctx.isPremium is typed + }, }); ```@@ -129,15 +127,15 @@ import * as buttons from "./components";
kitten.register({ commands, components }); client.once("ready", async () => { - const isDev = process.env.NODE_ENV === "development"; + const isDev = process.env.NODE_ENV === "development"; - try { - await kitten.sync({ - guildId: isDev ? process.env.GUILD_ID : undefined, - }); - } catch (error) { - console.error("failed to sync commands:", error); - } + try { + await kitten.sync({ + guildId: isDev ? process.env.GUILD_ID : undefined, + }); + } catch (error) { + console.error("failed to sync commands:", error); + } }); await client.login(process.env.BOT_TOKEN);
M
package.json
→
package.json
@@ -1,26 +1,26 @@
{ - "name": "kitten", - "module": "src/index.ts", - "author": { - "name": "apr", - "email": "apr@vt3e.cat", - "url": "https://vt3e.cat/" - }, - "license": "EUPL-1.2", - "type": "module", - "workspaces": [ - "pkgs/*" - ], - "devDependencies": { - "@types/bun": "latest", - "oxfmt": "^0.53.0", - "oxlint": "^1.68.0" - }, - "peerDependencies": { - "typescript": "^5" - }, - "scripts": { - "fmt": "bunx oxfmt fmt src tests", - "lint": "bunx oxlint lint src tests" - } + "name": "kitten", + "license": "EUPL-1.2", + "author": { + "name": "apr", + "email": "apr@vt3e.cat", + "url": "https://vt3e.cat/" + }, + "workspaces": [ + "pkgs/*" + ], + "type": "module", + "module": "src/index.ts", + "scripts": { + "fmt": "bunx oxfmt fmt src tests", + "lint": "bunx oxlint lint src tests" + }, + "devDependencies": { + "@types/bun": "latest", + "oxfmt": "^0.53.0", + "oxlint": "^1.68.0" + }, + "peerDependencies": { + "typescript": "^5" + } }
M
pkgs/core/package.json
→
pkgs/core/package.json
@@ -1,17 +1,17 @@
{ - "name": "@kitten/core", - "type": "module", - "module": "src/index.ts", - "dependencies": { - "discord.js": "^14.26.4" - }, - "devDependencies": { - "@types/bun": "latest" - }, - "peerDependencies": { - "typescript": "^5" - }, - "files": [ - "src/**/*" - ] + "name": "@kitten/core", + "files": [ + "src/**/*" + ], + "type": "module", + "module": "src/index.ts", + "dependencies": { + "discord.js": "^14.26.4" + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + } }
M
pkgs/core/src/commands.ts
→
pkgs/core/src/commands.ts
@@ -1,222 +1,222 @@
import type { - ChatInputCommandInteraction, - CommandInteraction, - UserContextMenuCommandInteraction, - MessageContextMenuCommandInteraction, + ChatInputCommandInteraction, + CommandInteraction, + UserContextMenuCommandInteraction, + MessageContextMenuCommandInteraction, } from "discord.js"; import type { Option, InferOptions } from "./options"; export const INTEGRATION_TYPES = { - GUILD_INSTALL: 0, - USER_INSTALL: 1, + GUILD_INSTALL: 0, + USER_INSTALL: 1, } as const; export const INTERACTION_CONTEXTS = { - GUILD: 0, - BOT_DM: 1, - PRIVATE_CHANNEL: 2, + GUILD: 0, + BOT_DM: 1, + PRIVATE_CHANNEL: 2, } as const; export const INTEGRATION_TYPE_PRESETS = { - ALL: Object.values(INTEGRATION_TYPES), - GUILD_ONLY: [INTEGRATION_TYPES.GUILD_INSTALL], - USER_ONLY: [INTEGRATION_TYPES.USER_INSTALL], + ALL: Object.values(INTEGRATION_TYPES), + GUILD_ONLY: [INTEGRATION_TYPES.GUILD_INSTALL], + USER_ONLY: [INTEGRATION_TYPES.USER_INSTALL], }; export const INTEGRATION_CONTEXT_PRESETS = { - EVERYWHERE: Object.values(INTERACTION_CONTEXTS), - GUILDS_ONLY: [INTERACTION_CONTEXTS.GUILD], - DMS_ONLY: [INTERACTION_CONTEXTS.BOT_DM, INTERACTION_CONTEXTS.PRIVATE_CHANNEL], + EVERYWHERE: Object.values(INTERACTION_CONTEXTS), + GUILDS_ONLY: [INTERACTION_CONTEXTS.GUILD], + DMS_ONLY: [INTERACTION_CONTEXTS.BOT_DM, INTERACTION_CONTEXTS.PRIVATE_CHANNEL], }; export type IntegrationType = (typeof INTEGRATION_TYPES)[keyof typeof INTEGRATION_TYPES]; export type InteractionContextType = - (typeof INTERACTION_CONTEXTS)[keyof typeof INTERACTION_CONTEXTS]; + (typeof INTERACTION_CONTEXTS)[keyof typeof INTERACTION_CONTEXTS]; export class Subcommand { - constructor( - public name: string, - public config: { - description: string; - options?: Record<string, Option<any, any>>; - run: Function; - }, - ) {} + constructor( + public name: string, + public config: { + description: string; + options?: Record<string, Option<any, any>>; + run: Function; + }, + ) {} } export class SubcommandGroup<Ctx = {}> { - public subcommands: Record<string, Subcommand> = {}; + public subcommands: Record<string, Subcommand> = {}; - constructor( - public name: string, - public description: string, - ) {} + constructor( + public name: string, + public description: string, + ) {} - subcommand<O extends Record<string, Option<any, any>>>( - name: string, - config: { - description: string; - options?: O; - run: ( - interaction: ChatInputCommandInteraction, - args: InferOptions<O>, - ctx: Ctx, - ) => Promise<void> | void; - }, - ): this { - this.subcommands[name] = new Subcommand(name, config); - return this; - } + subcommand<O extends Record<string, Option<any, any>>>( + name: string, + config: { + description: string; + options?: O; + run: ( + interaction: ChatInputCommandInteraction, + args: InferOptions<O>, + ctx: Ctx, + ) => Promise<void> | void; + }, + ): this { + this.subcommands[name] = new Subcommand(name, config); + return this; + } } export class SubcommandGroupBuilder<Ctx = {}> { - public subcommands: Record<string, Subcommand> = {}; - public groups: Record<string, SubcommandGroup<Ctx>> = {}; + public subcommands: Record<string, Subcommand> = {}; + public groups: Record<string, SubcommandGroup<Ctx>> = {}; - constructor( - public name: string, - public description: string, - public middlewares: Function[] = [], - public config?: { - integrationTypes?: IntegrationType[]; - contexts?: InteractionContextType[]; - }, - ) {} + constructor( + public name: string, + public description: string, + public middlewares: Function[] = [], + public config?: { + integrationTypes?: IntegrationType[]; + contexts?: InteractionContextType[]; + }, + ) {} - subcommand<O extends Record<string, Option<any, any>>>( - name: string, - config: { - description: string; - options?: O; - run: ( - interaction: ChatInputCommandInteraction, - args: InferOptions<O>, - ctx: Ctx, - ) => Promise<void> | void; - }, - ): this { - this.subcommands[name] = new Subcommand(name, config); - return this; - } + subcommand<O extends Record<string, Option<any, any>>>( + name: string, + config: { + description: string; + options?: O; + run: ( + interaction: ChatInputCommandInteraction, + args: InferOptions<O>, + ctx: Ctx, + ) => Promise<void> | void; + }, + ): this { + this.subcommands[name] = new Subcommand(name, config); + return this; + } - group( - name: string, - config: { description: string }, - setup: (group: SubcommandGroup<Ctx>) => void, - ): this { - const group = new SubcommandGroup<Ctx>(name, config.description); - setup(group); - this.groups[name] = group; - return this; - } + group( + name: string, + config: { description: string }, + setup: (group: SubcommandGroup<Ctx>) => void, + ): this { + const group = new SubcommandGroup<Ctx>(name, config.description); + setup(group); + this.groups[name] = group; + return this; + } } export class Command { - constructor( - public name: string, - public config: { - description: string; - options?: Record<string, Option<any, any>>; - integrationTypes?: IntegrationType[]; - contexts?: InteractionContextType[]; - run: Function; - }, - public middlewares: Function[] = [], - ) {} + constructor( + public name: string, + public config: { + description: string; + options?: Record<string, Option<any, any>>; + integrationTypes?: IntegrationType[]; + contexts?: InteractionContextType[]; + run: Function; + }, + public middlewares: Function[] = [], + ) {} } export class ContextMenuCommand { - constructor( - public name: string, - public type: "user" | "message", - public run: Function, - public middlewares: Function[] = [], - public config?: { - integrationTypes?: IntegrationType[]; - contexts?: InteractionContextType[]; - }, - ) {} + constructor( + public name: string, + public type: "user" | "message", + public run: Function, + public middlewares: Function[] = [], + public config?: { + integrationTypes?: IntegrationType[]; + contexts?: InteractionContextType[]; + }, + ) {} } export class CommandBuilder<Ctx = {}> { - constructor(private middlewares: Function[] = []) {} + constructor(private middlewares: Function[] = []) {} - use<NewCtx extends Record<string, any>>( - mw: (interaction: CommandInteraction, ctx: Ctx) => Promise<NewCtx | void> | NewCtx | void, - ): CommandBuilder<Ctx & NewCtx> { - return new CommandBuilder<Ctx & NewCtx>([...this.middlewares, mw]); - } + use<NewCtx extends Record<string, any>>( + mw: (interaction: CommandInteraction, ctx: Ctx) => Promise<NewCtx | void> | NewCtx | void, + ): CommandBuilder<Ctx & NewCtx> { + return new CommandBuilder<Ctx & NewCtx>([...this.middlewares, mw]); + } - command<O extends Record<string, Option<any, any>>>( - name: string, - config: { - description: string; - options?: O; - integrationTypes?: IntegrationType[]; - contexts?: InteractionContextType[]; - run: ( - interaction: ChatInputCommandInteraction, - args: InferOptions<O>, - ctx: Ctx, - ) => Promise<void> | void; - }, - ): Command; + command<O extends Record<string, Option<any, any>>>( + name: string, + config: { + description: string; + options?: O; + integrationTypes?: IntegrationType[]; + contexts?: InteractionContextType[]; + run: ( + interaction: ChatInputCommandInteraction, + args: InferOptions<O>, + ctx: Ctx, + ) => Promise<void> | void; + }, + ): Command; - command( - name: string, - config: { - description: string; - integrationTypes?: IntegrationType[]; - contexts?: InteractionContextType[]; - }, - ): SubcommandGroupBuilder<Ctx>; + command( + name: string, + config: { + description: string; + integrationTypes?: IntegrationType[]; + contexts?: InteractionContextType[]; + }, + ): SubcommandGroupBuilder<Ctx>; - command(name: string, config: any): any { - if (config.run) { - return new Command(name, config, this.middlewares); - } - return new SubcommandGroupBuilder<Ctx>(name, config.description, this.middlewares, { - integrationTypes: config.integrationTypes, - contexts: config.contexts, - }); - } + command(name: string, config: any): any { + if (config.run) { + return new Command(name, config, this.middlewares); + } + return new SubcommandGroupBuilder<Ctx>(name, config.description, this.middlewares, { + integrationTypes: config.integrationTypes, + contexts: config.contexts, + }); + } - userContextMenu( - name: string, - runOrConfig: - | ((interaction: UserContextMenuCommandInteraction, ctx: Ctx) => Promise<void> | void) - | { - integrationTypes?: IntegrationType[]; - contexts?: InteractionContextType[]; - run: (interaction: UserContextMenuCommandInteraction, ctx: Ctx) => Promise<void> | void; - }, - ): ContextMenuCommand { - if (typeof runOrConfig === "function") - return new ContextMenuCommand(name, "user", runOrConfig, this.middlewares); + userContextMenu( + name: string, + runOrConfig: + | ((interaction: UserContextMenuCommandInteraction, ctx: Ctx) => Promise<void> | void) + | { + integrationTypes?: IntegrationType[]; + contexts?: InteractionContextType[]; + run: (interaction: UserContextMenuCommandInteraction, ctx: Ctx) => Promise<void> | void; + }, + ): ContextMenuCommand { + if (typeof runOrConfig === "function") + return new ContextMenuCommand(name, "user", runOrConfig, this.middlewares); - return new ContextMenuCommand(name, "user", runOrConfig.run, this.middlewares, { - integrationTypes: runOrConfig.integrationTypes, - contexts: runOrConfig.contexts, - }); - } + return new ContextMenuCommand(name, "user", runOrConfig.run, this.middlewares, { + integrationTypes: runOrConfig.integrationTypes, + contexts: runOrConfig.contexts, + }); + } - messageContextMenu( - name: string, - runOrConfig: - | ((interaction: MessageContextMenuCommandInteraction, ctx: Ctx) => Promise<void> | void) - | { - integrationTypes?: IntegrationType[]; - contexts?: InteractionContextType[]; - run: ( - interaction: MessageContextMenuCommandInteraction, - ctx: Ctx, - ) => Promise<void> | void; - }, - ): ContextMenuCommand { - if (typeof runOrConfig === "function") - return new ContextMenuCommand(name, "message", runOrConfig, this.middlewares); + messageContextMenu( + name: string, + runOrConfig: + | ((interaction: MessageContextMenuCommandInteraction, ctx: Ctx) => Promise<void> | void) + | { + integrationTypes?: IntegrationType[]; + contexts?: InteractionContextType[]; + run: ( + interaction: MessageContextMenuCommandInteraction, + ctx: Ctx, + ) => Promise<void> | void; + }, + ): ContextMenuCommand { + if (typeof runOrConfig === "function") + return new ContextMenuCommand(name, "message", runOrConfig, this.middlewares); - return new ContextMenuCommand(name, "message", runOrConfig.run, this.middlewares, { - integrationTypes: runOrConfig.integrationTypes, - contexts: runOrConfig.contexts, - }); - } + return new ContextMenuCommand(name, "message", runOrConfig.run, this.middlewares, { + integrationTypes: runOrConfig.integrationTypes, + contexts: runOrConfig.contexts, + }); + } }
M
pkgs/core/src/components.ts
→
pkgs/core/src/components.ts
@@ -1,63 +1,63 @@
import type { - ButtonInteraction, - AnySelectMenuInteraction, - ModalSubmitInteraction, + ButtonInteraction, + AnySelectMenuInteraction, + ModalSubmitInteraction, } from "discord.js"; import type { Option, InferOptions } from "./options"; import { CustomIdTooLong } from "./errors"; export type KittenComponentInteraction = - | ButtonInteraction - | AnySelectMenuInteraction - | ModalSubmitInteraction; + | ButtonInteraction + | AnySelectMenuInteraction + | ModalSubmitInteraction; export class KittenComponent< - I extends KittenComponentInteraction = KittenComponentInteraction, - O extends Record<string, Option<any, any>> = Record<string, Option<any, any>>, + I extends KittenComponentInteraction = KittenComponentInteraction, + O extends Record<string, Option<any, any>> = Record<string, Option<any, any>>, > { - type = "component" as const; + type = "component" as const; - constructor( - public name: string, - public config: { - options?: Record<string, Option<any, any>>; - run: (interaction: I, args: any) => Promise<void> | void; - }, - ) {} + constructor( + public name: string, + public config: { + options?: Record<string, Option<any, any>>; + run: (interaction: I, args: any) => Promise<void> | void; + }, + ) {} - /** - * @throws {CustomIdTooLong} if the generated custom ID exceeds Discord's 100 character limit. - * @returns a custom ID string that encodes the component name and its options' values. - */ - id( - ...[args]: {} extends InferOptions<O> ? [args?: InferOptions<O>] : [args: InferOptions<O>] - ): string { - const keys = Object.keys(this.config.options || {}); - const values = keys.map((key) => encodeURIComponent(String(args?.[key] ?? ""))); - const customId = [this.name, ...values].join(":"); + /** + * @throws {CustomIdTooLong} if the generated custom ID exceeds Discord's 100 character limit. + * @returns a custom ID string that encodes the component name and its options' values. + */ + id( + ...[args]: {} extends InferOptions<O> ? [args?: InferOptions<O>] : [args: InferOptions<O>] + ): string { + const keys = Object.keys(this.config.options || {}); + const values = keys.map((key) => encodeURIComponent(String(args?.[key] ?? ""))); + const customId = [this.name, ...values].join(":"); - if (customId.length > 100) throw new CustomIdTooLong(this.name, customId.length); + if (customId.length > 100) throw new CustomIdTooLong(this.name, customId.length); - return customId; - } + return customId; + } - parseArgs(customId: string): Record<string, any> { - const [, ...parts] = customId.split(":"); - const keys = Object.keys(this.config.options || {}); - const parsed: Record<string, any> = {}; + parseArgs(customId: string): Record<string, any> { + const [, ...parts] = customId.split(":"); + const keys = Object.keys(this.config.options || {}); + const parsed: Record<string, any> = {}; - keys.forEach((key, index) => { - const rawVal = parts[index]; - if (rawVal === undefined) return; + keys.forEach((key, index) => { + const rawVal = parts[index]; + if (rawVal === undefined) return; - const val = decodeURIComponent(rawVal); - const opt = this.config.options?.[key]; + const val = decodeURIComponent(rawVal); + const opt = this.config.options?.[key]; - if (opt?.type === "boolean") parsed[key] = val === "true"; - else if (opt?.type === "integer" || opt?.type === "number") parsed[key] = Number(val); - else parsed[key] = val === "" ? undefined : val; - }); + if (opt?.type === "boolean") parsed[key] = val === "true"; + else if (opt?.type === "integer" || opt?.type === "number") parsed[key] = Number(val); + else parsed[key] = val === "" ? undefined : val; + }); - return parsed; - } + return parsed; + } }
M
pkgs/core/src/errors.ts
→
pkgs/core/src/errors.ts
@@ -1,13 +1,13 @@
export class HaltExecution extends Error { - constructor() { - super("execution stopped by middleware"); - } + constructor() { + super("execution stopped by middleware"); + } } export class CustomIdTooLong extends Error { - constructor(componentName: string, length: number) { - super( - `custom ID for component "${componentName}" is too long (${length} characters). discord limits custom IDs to 100 characters.`, - ); - } + constructor(componentName: string, length: number) { + super( + `custom ID for component "${componentName}" is too long (${length} characters). discord limits custom IDs to 100 characters.`, + ); + } }
M
pkgs/core/src/kitten.ts
→
pkgs/core/src/kitten.ts
@@ -1,463 +1,467 @@
import { - type Client, - type ChatInputCommandInteraction, - type AutocompleteInteraction, - type ButtonInteraction, - type AnySelectMenuInteraction, - type ModalSubmitInteraction, - type APIApplicationCommandOption, - type APIApplicationCommandBasicOption, - type CommandInteraction, - type UserContextMenuCommandInteraction, - type MessageContextMenuCommandInteraction, - BaseInteraction, - type RESTPostAPIApplicationCommandsJSONBody, - ApplicationCommandType, + type Client, + type ChatInputCommandInteraction, + type AutocompleteInteraction, + type ButtonInteraction, + type AnySelectMenuInteraction, + type ModalSubmitInteraction, + type APIApplicationCommandOption, + type APIApplicationCommandBasicOption, + type CommandInteraction, + type UserContextMenuCommandInteraction, + type MessageContextMenuCommandInteraction, + BaseInteraction, + type RESTPostAPIApplicationCommandsJSONBody, + ApplicationCommandType, } from "discord.js"; import { HaltExecution } from "./errors"; import { type Option, type InferOptions, OPTION_TYPES } from "./options"; import { KittenComponent, type KittenComponentInteraction } from "./components"; import { - Command, - CommandBuilder, - SubcommandGroupBuilder, - ContextMenuCommand, - type IntegrationType, - type InteractionContextType, + Command, + CommandBuilder, + SubcommandGroupBuilder, + ContextMenuCommand, + type IntegrationType, + type InteractionContextType, } from "./commands"; import { baseLogger, type KittenLogger } from "./logger"; export interface KittenOptions { - logger?: KittenLogger; + logger?: KittenLogger; } export class Kitten { - private commands = new Map<string, Command | SubcommandGroupBuilder<any>>(); - private userContextMenus = new Map<string, ContextMenuCommand>(); - private messageContextMenus = new Map<string, ContextMenuCommand>(); - private components = new Map<string, KittenComponent<any, any>>(); - private logger: KittenLogger; + private commands = new Map<string, Command | SubcommandGroupBuilder<any>>(); + private userContextMenus = new Map<string, ContextMenuCommand>(); + private messageContextMenus = new Map<string, ContextMenuCommand>(); + private components = new Map<string, KittenComponent<any, any>>(); + private logger: KittenLogger; - constructor( - private client: Client, - options: KittenOptions = {}, - ) { - this.client.on("interactionCreate", this.handleInteraction.bind(this)); - this.logger = options.logger ?? baseLogger; - } + constructor( + private client: Client, + options: KittenOptions = {}, + ) { + this.client.on("interactionCreate", this.handleInteraction.bind(this)); + this.logger = options.logger ?? baseLogger; + } - builder() { - return new CommandBuilder(); - } + builder() { + return new CommandBuilder(); + } - command<O extends Record<string, Option<any, any>>>( - name: string, - config: { - description: string; - options?: O; - integrationTypes?: IntegrationType[]; - contexts?: InteractionContextType[]; - run: ( - interaction: ChatInputCommandInteraction, - args: InferOptions<O>, - ) => Promise<void> | void; - }, - ): Command; + command<O extends Record<string, Option<any, any>>>( + name: string, + config: { + description: string; + options?: O; + integrationTypes?: IntegrationType[]; + contexts?: InteractionContextType[]; + run: ( + interaction: ChatInputCommandInteraction, + args: InferOptions<O>, + ) => Promise<void> | void; + }, + ): Command; - command( - name: string, - config: { - description: string; - integrationTypes?: IntegrationType[]; - contexts?: InteractionContextType[]; - }, - ): SubcommandGroupBuilder<{}>; + command( + name: string, + config: { + description: string; + integrationTypes?: IntegrationType[]; + contexts?: InteractionContextType[]; + }, + ): SubcommandGroupBuilder<{}>; - command(name: string, config: any): any { - return this.builder().command(name, config); - } + command(name: string, config: any): any { + return this.builder().command(name, config); + } - userContextMenu( - name: string, - runOrConfig: - | ((interaction: UserContextMenuCommandInteraction) => Promise<void> | void) - | { - integrationTypes?: IntegrationType[]; - contexts?: InteractionContextType[]; - run: (interaction: UserContextMenuCommandInteraction) => Promise<void> | void; - }, - ): ContextMenuCommand { - return this.builder().userContextMenu(name, runOrConfig as any); - } + userContextMenu( + name: string, + runOrConfig: + | ((interaction: UserContextMenuCommandInteraction) => Promise<void> | void) + | { + integrationTypes?: IntegrationType[]; + contexts?: InteractionContextType[]; + run: (interaction: UserContextMenuCommandInteraction) => Promise<void> | void; + }, + ): ContextMenuCommand { + return this.builder().userContextMenu(name, runOrConfig as any); + } - messageContextMenu( - name: string, - runOrConfig: - | ((interaction: MessageContextMenuCommandInteraction) => Promise<void> | void) - | { - integrationTypes?: IntegrationType[]; - contexts?: InteractionContextType[]; - run: (interaction: MessageContextMenuCommandInteraction) => Promise<void> | void; - }, - ): ContextMenuCommand { - return this.builder().messageContextMenu(name, runOrConfig as any); - } + messageContextMenu( + name: string, + runOrConfig: + | ((interaction: MessageContextMenuCommandInteraction) => Promise<void> | void) + | { + integrationTypes?: IntegrationType[]; + contexts?: InteractionContextType[]; + run: (interaction: MessageContextMenuCommandInteraction) => Promise<void> | void; + }, + ): ContextMenuCommand { + return this.builder().messageContextMenu(name, runOrConfig as any); + } - button<O extends Record<string, Option<any, any>>>( - name: string, - config: { - options?: O; - run: (interaction: ButtonInteraction, args: InferOptions<O>) => Promise<void> | void; - }, - ): KittenComponent<ButtonInteraction, O> { - return new KittenComponent<ButtonInteraction, O>(name, config); - } + button<O extends Record<string, Option<any, any>>>( + name: string, + config: { + options?: O; + run: (interaction: ButtonInteraction, args: InferOptions<O>) => Promise<void> | void; + }, + ): KittenComponent<ButtonInteraction, O> { + return new KittenComponent<ButtonInteraction, O>(name, config); + } - selectMenu<O extends Record<string, Option<any, any>>>( - name: string, - config: { - options?: O; - run: (interaction: AnySelectMenuInteraction, args: InferOptions<O>) => Promise<void> | void; - }, - ): KittenComponent<AnySelectMenuInteraction, O> { - return new KittenComponent<AnySelectMenuInteraction, O>(name, config); - } + selectMenu<O extends Record<string, Option<any, any>>>( + name: string, + config: { + options?: O; + run: (interaction: AnySelectMenuInteraction, args: InferOptions<O>) => Promise<void> | void; + }, + ): KittenComponent<AnySelectMenuInteraction, O> { + return new KittenComponent<AnySelectMenuInteraction, O>(name, config); + } - modal<O extends Record<string, Option<any, any>>>( - name: string, - config: { - options?: O; - run: (interaction: ModalSubmitInteraction, args: InferOptions<O>) => Promise<void> | void; - }, - ): KittenComponent<ModalSubmitInteraction, O> { - return new KittenComponent<ModalSubmitInteraction, O>(name, config); - } + modal<O extends Record<string, Option<any, any>>>( + name: string, + config: { + options?: O; + run: (interaction: ModalSubmitInteraction, args: InferOptions<O>) => Promise<void> | void; + }, + ): KittenComponent<ModalSubmitInteraction, O> { + return new KittenComponent<ModalSubmitInteraction, O>(name, config); + } - register(registry: { - commands?: (Command | SubcommandGroupBuilder<any> | ContextMenuCommand)[]; - components?: KittenComponent<any, any>[]; - }) { - registry.commands?.forEach((cmd) => { - if (cmd instanceof ContextMenuCommand) { - if (cmd.type === "user") this.userContextMenus.set(cmd.name, cmd); - else if (cmd.type === "message") this.messageContextMenus.set(cmd.name, cmd); - } else { - this.commands.set(cmd.name, cmd); - } - }); - registry.components?.forEach((comp) => this.components.set(comp.name, comp)); - } + register(registry: { + commands?: (Command | SubcommandGroupBuilder<any> | ContextMenuCommand)[]; + components?: KittenComponent<any, any>[]; + }) { + registry.commands?.forEach((cmd) => { + if (cmd instanceof ContextMenuCommand) { + if (cmd.type === "user") this.userContextMenus.set(cmd.name, cmd); + else if (cmd.type === "message") this.messageContextMenus.set(cmd.name, cmd); + } else { + this.commands.set(cmd.name, cmd); + } + }); + registry.components?.forEach((comp) => this.components.set(comp.name, comp)); + } - private async handleInteraction(interaction: BaseInteraction) { - switch (true) { - // commands - case interaction.isChatInputCommand(): - return await this.routeCommand(interaction); - case interaction.isAutocomplete(): - return await this.routeAutocomplete(interaction); - case interaction.isUserContextMenuCommand(): - return await this.routeContextMenu(interaction, "user"); - case interaction.isMessageContextMenuCommand(): - return await this.routeContextMenu(interaction, "message"); + private async handleInteraction(interaction: BaseInteraction) { + switch (true) { + // commands + case interaction.isChatInputCommand(): + return await this.routeCommand(interaction); + case interaction.isAutocomplete(): + return await this.routeAutocomplete(interaction); + case interaction.isUserContextMenuCommand(): + return await this.routeContextMenu(interaction, "user"); + case interaction.isMessageContextMenuCommand(): + return await this.routeContextMenu(interaction, "message"); - // components - case interaction.isButton(): - return await this.routeComponent(interaction, "button"); - case interaction.isAnySelectMenu(): - return await this.routeComponent(interaction, "select menu"); - case interaction.isModalSubmit(): - return await this.routeComponent(interaction, "modal"); - } - } + // components + case interaction.isButton(): + return await this.routeComponent(interaction, "button"); + case interaction.isAnySelectMenu(): + return await this.routeComponent(interaction, "select menu"); + case interaction.isModalSubmit(): + return await this.routeComponent(interaction, "modal"); + } + } - private async routeCommand(interaction: ChatInputCommandInteraction) { - const entry = this.commands.get(interaction.commandName); - if (!entry) return; + private async routeCommand(interaction: ChatInputCommandInteraction) { + const entry = this.commands.get(interaction.commandName); + if (!entry) return; - if (entry instanceof SubcommandGroupBuilder) { - const groupName = interaction.options.getSubcommandGroup(false); - const subName = interaction.options.getSubcommand(false); + if (entry instanceof SubcommandGroupBuilder) { + const groupName = interaction.options.getSubcommandGroup(false); + const subName = interaction.options.getSubcommand(false); - if (groupName) { - const group = entry.groups[groupName]; - if (!group) return; - const subcommand = group.subcommands[subName ?? ""]; - if (!subcommand) return; + if (groupName) { + const group = entry.groups[groupName]; + if (!group) return; + const subcommand = group.subcommands[subName ?? ""]; + if (!subcommand) return; - await this.executeWithMiddlewares( - interaction, - entry.middlewares, - subcommand.config.run, - subcommand.config.options, - ); - } else if (subName) { - const subcommand = entry.subcommands[subName]; - if (!subcommand) return; + await this.executeWithMiddlewares( + interaction, + entry.middlewares, + subcommand.config.run, + subcommand.config.options, + ); + } else if (subName) { + const subcommand = entry.subcommands[subName]; + if (!subcommand) return; - await this.executeWithMiddlewares( - interaction, - entry.middlewares, - subcommand.config.run, - subcommand.config.options, - ); - } - } else { - await this.executeWithMiddlewares( - interaction, - entry.middlewares, - entry.config.run, - entry.config.options, - ); - } - } + await this.executeWithMiddlewares( + interaction, + entry.middlewares, + subcommand.config.run, + subcommand.config.options, + ); + } + } else { + await this.executeWithMiddlewares( + interaction, + entry.middlewares, + entry.config.run, + entry.config.options, + ); + } + } - private async routeContextMenu( - interaction: UserContextMenuCommandInteraction | MessageContextMenuCommandInteraction, - type: "user" | "message", - ) { - const entry = - type === "user" - ? this.userContextMenus.get(interaction.commandName) - : this.messageContextMenus.get(interaction.commandName); + private async routeContextMenu( + interaction: UserContextMenuCommandInteraction | MessageContextMenuCommandInteraction, + type: "user" | "message", + ) { + const entry = + type === "user" + ? this.userContextMenus.get(interaction.commandName) + : this.messageContextMenus.get(interaction.commandName); - if (!entry) return; - await this.executeWithMiddlewares(interaction, entry.middlewares, entry.run); - } + if (!entry) return; + await this.executeWithMiddlewares(interaction, entry.middlewares, entry.run); + } - private async executeWithMiddlewares( - interaction: CommandInteraction, - middlewares: Function[], - run: Function, - optionsSchema?: Record<string, Option<any, any>>, - ) { - try { - let context = {}; - for (const mw of middlewares) { - const result = await mw(interaction, context); - if (result) context = { ...context, ...result }; - } + private async executeWithMiddlewares( + interaction: CommandInteraction, + middlewares: Function[], + run: Function, + optionsSchema?: Record<string, Option<any, any>>, + ) { + try { + let context = {}; + for (const mw of middlewares) { + const result = await mw(interaction, context); + if (result) context = { ...context, ...result }; + } - if (interaction.isChatInputCommand()) { - const args = this.parseSlashOptions(interaction, optionsSchema); - await run(interaction, args, context); - } else { - await run(interaction, context); - } - } catch (err) { - if (err instanceof HaltExecution) return; + if (interaction.isChatInputCommand()) { + const args = this.parseSlashOptions(interaction, optionsSchema); + await run(interaction, args, context); + } else { + await run(interaction, context); + } + } catch (err) { + if (err instanceof HaltExecution) return; - this.logger.error(`unhandled error executing command ${interaction.commandName}:`, { - error: err instanceof Error ? err.stack : String(err), - command: interaction.commandName, - user: interaction.user.id, - }); + this.logger.error(`unhandled error executing command ${interaction.commandName}:`, { + error: err instanceof Error ? err.stack : String(err), + command: interaction.commandName, + user: interaction.user.id, + }); - if (!interaction.replied && !interaction.deferred) { - await interaction - .reply({ content: "an error occurred while executing this command.", ephemeral: true }) - .catch(() => null); - } - } - } + if (!interaction.replied && !interaction.deferred) { + await interaction + .reply({ content: "an error occurred while executing this command.", ephemeral: true }) + .catch(() => null); + } + } + } - private async routeAutocomplete(interaction: AutocompleteInteraction) { - const entry = this.commands.get(interaction.commandName); - if (!entry) return; + private async routeAutocomplete(interaction: AutocompleteInteraction) { + const entry = this.commands.get(interaction.commandName); + if (!entry) return; - const focused = interaction.options.getFocused(true); - let optionsSchema: Record<string, Option<any, any>> | undefined; + const focused = interaction.options.getFocused(true); + let optionsSchema: Record<string, Option<any, any>> | undefined; - if (entry instanceof SubcommandGroupBuilder) { - const groupName = interaction.options.getSubcommandGroup(false); - const subName = interaction.options.getSubcommand(false); + if (entry instanceof SubcommandGroupBuilder) { + const groupName = interaction.options.getSubcommandGroup(false); + const subName = interaction.options.getSubcommand(false); - if (groupName && subName) - optionsSchema = entry.groups[groupName]?.subcommands[subName]?.config.options; - else if (subName) optionsSchema = entry.subcommands[subName]?.config.options; - } else { - optionsSchema = entry.config.options; - } + if (groupName && subName) + optionsSchema = entry.groups[groupName]?.subcommands[subName]?.config.options; + else if (subName) optionsSchema = entry.subcommands[subName]?.config.options; + } else { + optionsSchema = entry.config.options; + } - const opt = optionsSchema?.[focused.name]; - if (opt?.autocomplete) { - const choices = await opt.autocomplete(interaction, focused.value); - await interaction.respond(choices).catch(() => null); - } - } + const opt = optionsSchema?.[focused.name]; + if (opt?.autocomplete) { + const choices = await opt.autocomplete(interaction, focused.value); + await interaction.respond(choices).catch(() => null); + } + } - private async routeComponent(interaction: KittenComponentInteraction, typeName: string) { - const [prefix] = interaction.customId.split(":"); - if (!prefix) return; + private async routeComponent(interaction: KittenComponentInteraction, typeName: string) { + const [prefix] = interaction.customId.split(":"); + if (!prefix) return; - const component = this.components.get(prefix); - if (!component) return; + const component = this.components.get(prefix); + if (!component) return; - try { - const args = component.parseArgs(interaction.customId); - await component.config.run(interaction, args); - } catch (err) { - this.logger.error(`error executing ${typeName} ${prefix}:`, { - error: err instanceof Error ? err.stack : String(err), - component: prefix, - user: interaction.user.id, - }); - } - } + try { + const args = component.parseArgs(interaction.customId); + await component.config.run(interaction, args); + } catch (err) { + this.logger.error(`error executing ${typeName} ${prefix}:`, { + error: err instanceof Error ? err.stack : String(err), + component: prefix, + user: interaction.user.id, + }); + } + } - private parseSlashOptions( - interaction: ChatInputCommandInteraction, - optionsSchema?: Record<string, Option<any, any>>, - ): Record<string, any> { - if (!optionsSchema) return {}; - const parsed: Record<string, any> = {}; + private parseSlashOptions( + interaction: ChatInputCommandInteraction, + optionsSchema?: Record<string, Option<any, any>>, + ): Record<string, any> { + if (!optionsSchema) return {}; + const parsed: Record<string, any> = {}; - for (const [name, opt] of Object.entries(optionsSchema)) { - switch (opt.type) { - case "string": - parsed[name] = interaction.options.getString(name); - break; - case "integer": - parsed[name] = interaction.options.getInteger(name); - break; - case "number": - parsed[name] = interaction.options.getNumber(name); - break; - case "boolean": - parsed[name] = interaction.options.getBoolean(name); - break; - case "user": - parsed[name] = interaction.options.getUser(name); - break; - case "channel": - parsed[name] = interaction.options.getChannel(name); - break; - case "role": - parsed[name] = interaction.options.getRole(name); - break; - case "mentionable": - parsed[name] = interaction.options.getMentionable(name); - break; - case "attachment": - parsed[name] = interaction.options.getAttachment(name); - break; - } - } - return parsed; - } + for (const [name, opt] of Object.entries(optionsSchema)) { + switch (opt.type) { + case "string": + parsed[name] = interaction.options.getString(name); + break; + case "integer": + parsed[name] = interaction.options.getInteger(name); + break; + case "number": + parsed[name] = interaction.options.getNumber(name); + break; + case "boolean": + parsed[name] = interaction.options.getBoolean(name); + break; + case "user": + parsed[name] = interaction.options.getUser(name); + break; + case "channel": + parsed[name] = interaction.options.getChannel(name); + break; + case "role": + parsed[name] = interaction.options.getRole(name); + break; + case "mentionable": + parsed[name] = interaction.options.getMentionable(name); + break; + case "attachment": + parsed[name] = interaction.options.getAttachment(name); + break; + } + } + return parsed; + } - async sync({ guildId }: { guildId?: string } = {}) { - const payloads: RESTPostAPIApplicationCommandsJSONBody[] = []; + async sync({ guildId }: { guildId?: string } = {}) { + const payloads: RESTPostAPIApplicationCommandsJSONBody[] = []; - // chat input commands - for (const cmd of this.commands.values()) { - if (cmd instanceof SubcommandGroupBuilder) { - const subcommandOptions: APIApplicationCommandOption[] = Object.values(cmd.subcommands).map((sub) => ({ - type: OPTION_TYPES.subcommand, - name: sub.name, - description: sub.config.description, - options: this.transformOptionsForDiscord(sub.config.options), - })); + // chat input commands + for (const cmd of this.commands.values()) { + if (cmd instanceof SubcommandGroupBuilder) { + const subcommandOptions: APIApplicationCommandOption[] = Object.values(cmd.subcommands).map( + (sub) => ({ + type: OPTION_TYPES.subcommand, + name: sub.name, + description: sub.config.description, + options: this.transformOptionsForDiscord(sub.config.options), + }), + ); - const groupOptions: APIApplicationCommandOption[] = Object.values(cmd.groups).map((group) => ({ - type: OPTION_TYPES.subcommandGroup, - name: group.name, - description: group.description, - options: Object.values(group.subcommands).map((sub) => ({ - type: OPTION_TYPES.subcommand, - name: sub.name, - description: sub.config.description, - options: this.transformOptionsForDiscord(sub.config.options), - })), - })); + const groupOptions: APIApplicationCommandOption[] = Object.values(cmd.groups).map( + (group) => ({ + type: OPTION_TYPES.subcommandGroup, + name: group.name, + description: group.description, + options: Object.values(group.subcommands).map((sub) => ({ + type: OPTION_TYPES.subcommand, + name: sub.name, + description: sub.config.description, + options: this.transformOptionsForDiscord(sub.config.options), + })), + }), + ); - const payload: RESTPostAPIApplicationCommandsJSONBody = { - type: ApplicationCommandType.ChatInput, - name: cmd.name, - description: cmd.description, - options: [...subcommandOptions, ...groupOptions], - }; + const payload: RESTPostAPIApplicationCommandsJSONBody = { + type: ApplicationCommandType.ChatInput, + name: cmd.name, + description: cmd.description, + options: [...subcommandOptions, ...groupOptions], + }; - if (cmd.config?.integrationTypes) payload.integration_types = cmd.config.integrationTypes; - if (cmd.config?.contexts) payload.contexts = cmd.config.contexts; + if (cmd.config?.integrationTypes) payload.integration_types = cmd.config.integrationTypes; + if (cmd.config?.contexts) payload.contexts = cmd.config.contexts; - payloads.push(payload); - } else { - const payload: RESTPostAPIApplicationCommandsJSONBody = { - type: ApplicationCommandType.ChatInput, - name: cmd.name, - description: cmd.config.description, - options: this.transformOptionsForDiscord(cmd.config.options), - }; + payloads.push(payload); + } else { + const payload: RESTPostAPIApplicationCommandsJSONBody = { + type: ApplicationCommandType.ChatInput, + name: cmd.name, + description: cmd.config.description, + options: this.transformOptionsForDiscord(cmd.config.options), + }; - if (cmd.config.integrationTypes) payload.integration_types = cmd.config.integrationTypes; - if (cmd.config.contexts) payload.contexts = cmd.config.contexts; + if (cmd.config.integrationTypes) payload.integration_types = cmd.config.integrationTypes; + if (cmd.config.contexts) payload.contexts = cmd.config.contexts; - payloads.push(payload); - } - } + payloads.push(payload); + } + } - // context menus - for (const cmd of this.userContextMenus.values()) { - const payload: RESTPostAPIApplicationCommandsJSONBody = { - type: ApplicationCommandType.User, - name: cmd.name, - }; + // context menus + for (const cmd of this.userContextMenus.values()) { + const payload: RESTPostAPIApplicationCommandsJSONBody = { + type: ApplicationCommandType.User, + name: cmd.name, + }; - if (cmd.config?.integrationTypes) payload.integration_types = cmd.config.integrationTypes; - if (cmd.config?.contexts) payload.contexts = cmd.config.contexts; + if (cmd.config?.integrationTypes) payload.integration_types = cmd.config.integrationTypes; + if (cmd.config?.contexts) payload.contexts = cmd.config.contexts; - payloads.push(payload); - } + payloads.push(payload); + } - for (const cmd of this.messageContextMenus.values()) { - const payload: RESTPostAPIApplicationCommandsJSONBody = { - type: ApplicationCommandType.Message, - name: cmd.name, - }; + for (const cmd of this.messageContextMenus.values()) { + const payload: RESTPostAPIApplicationCommandsJSONBody = { + type: ApplicationCommandType.Message, + name: cmd.name, + }; - if (cmd.config?.integrationTypes) payload.integration_types = cmd.config.integrationTypes; - if (cmd.config?.contexts) payload.contexts = cmd.config.contexts; + if (cmd.config?.integrationTypes) payload.integration_types = cmd.config.integrationTypes; + if (cmd.config?.contexts) payload.contexts = cmd.config.contexts; - payloads.push(payload); - } + payloads.push(payload); + } - if (!this.client.application) { - throw new Error("client application not ready to sync. ensure you await client.login()"); - } + if (!this.client.application) { + throw new Error("client application not ready to sync. ensure you await client.login()"); + } - if (guildId) { - await this.client.application.commands.set(payloads, guildId); - this.logger.info(`synced ${payloads.length} commands to guild ${guildId}.`, { - guildId, - commandCount: payloads.length, - }); - } else { - await this.client.application.commands.set(payloads); - this.logger.info(`synced ${payloads.length} global commands.`, { - commandCount: payloads.length, - }); - } - } + if (guildId) { + await this.client.application.commands.set(payloads, guildId); + this.logger.info(`synced ${payloads.length} commands to guild ${guildId}.`, { + guildId, + commandCount: payloads.length, + }); + } else { + await this.client.application.commands.set(payloads); + this.logger.info(`synced ${payloads.length} global commands.`, { + commandCount: payloads.length, + }); + } + } - private transformOptionsForDiscord( - options?: Record<string, Option<any, any>>, - ): APIApplicationCommandBasicOption[] { - if (!options) return []; + private transformOptionsForDiscord( + options?: Record<string, Option<any, any>>, + ): APIApplicationCommandBasicOption[] { + if (!options) return []; - return Object.entries(options).map(([name, opt]) => { - const payload: any = { - name, - description: opt.description, - type: OPTION_TYPES[opt.type], - required: opt.required, - }; + return Object.entries(options).map(([name, opt]) => { + const payload: any = { + name, + description: opt.description, + type: OPTION_TYPES[opt.type], + required: opt.required, + }; - if (opt.autocomplete) { - payload.autocomplete = true; - } + if (opt.autocomplete) { + payload.autocomplete = true; + } - return payload as APIApplicationCommandBasicOption; - }); - } + return payload as APIApplicationCommandBasicOption; + }); + } }
M
pkgs/core/src/logger.ts
→
pkgs/core/src/logger.ts
@@ -1,94 +1,94 @@
export type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal"; export interface LogEntry { - level: LogLevel; - msg: string; - time: number; - [key: string]: unknown; + level: LogLevel; + msg: string; + time: number; + [key: string]: unknown; } export interface LoggerOptions { - level?: LogLevel; - name?: string; - base?: Record<string, unknown>; - transport?: (entry: LogEntry) => void; + level?: LogLevel; + name?: string; + base?: Record<string, unknown>; + transport?: (entry: LogEntry) => void; } export interface KittenLogger { - trace(msg: string, data?: Record<string, unknown>): void; - debug(msg: string, data?: Record<string, unknown>): void; - info(msg: string, data?: Record<string, unknown>): void; - warn(msg: string, data?: Record<string, unknown>): void; - error(msg: string, data?: Record<string, unknown>): void; - fatal(msg: string, data?: Record<string, unknown>): void; - child(bindings: Record<string, unknown>): KittenLogger; + trace(msg: string, data?: Record<string, unknown>): void; + debug(msg: string, data?: Record<string, unknown>): void; + info(msg: string, data?: Record<string, unknown>): void; + warn(msg: string, data?: Record<string, unknown>): void; + error(msg: string, data?: Record<string, unknown>): void; + fatal(msg: string, data?: Record<string, unknown>): void; + child(bindings: Record<string, unknown>): KittenLogger; } const LEVELS: Record<LogLevel, number> = { - trace: 10, - debug: 20, - info: 30, - warn: 40, - error: 50, - fatal: 60, + trace: 10, + debug: 20, + info: 30, + warn: 40, + error: 50, + fatal: 60, }; const defaultTransport = (entry: LogEntry) => { - const out = JSON.stringify(entry); - if (entry.level === "error" || entry.level === "fatal") console.error(out); - else console.log(out); + const out = JSON.stringify(entry); + if (entry.level === "error" || entry.level === "fatal") console.error(out); + else console.log(out); }; export class Logger implements KittenLogger { - private readonly minLevel: number; - private readonly base: Record<string, unknown>; - private readonly transport: (entry: LogEntry) => void; + private readonly minLevel: number; + private readonly base: Record<string, unknown>; + private readonly transport: (entry: LogEntry) => void; - constructor(private readonly options: LoggerOptions = {}) { - this.minLevel = LEVELS[options.level ?? "info"]; - this.base = { - ...(options.name ? { name: options.name } : {}), - ...options.base, - }; - this.transport = options.transport ?? defaultTransport; - } + constructor(private readonly options: LoggerOptions = {}) { + this.minLevel = LEVELS[options.level ?? "info"]; + this.base = { + ...(options.name ? { name: options.name } : {}), + ...options.base, + }; + this.transport = options.transport ?? defaultTransport; + } - private write(level: LogLevel, msg: string, data?: Record<string, unknown>) { - if (LEVELS[level] < this.minLevel) return; - this.transport({ - level, - msg, - time: Date.now(), - ...this.base, - ...data, - }); - } + private write(level: LogLevel, msg: string, data?: Record<string, unknown>) { + if (LEVELS[level] < this.minLevel) return; + this.transport({ + level, + msg, + time: Date.now(), + ...this.base, + ...data, + }); + } - trace(msg: string, data?: Record<string, unknown>) { - this.write("trace", msg, data); - } - debug(msg: string, data?: Record<string, unknown>) { - this.write("debug", msg, data); - } - info(msg: string, data?: Record<string, unknown>) { - this.write("info", msg, data); - } - warn(msg: string, data?: Record<string, unknown>) { - this.write("warn", msg, data); - } - error(msg: string, data?: Record<string, unknown>) { - this.write("error", msg, data); - } - fatal(msg: string, data?: Record<string, unknown>) { - this.write("fatal", msg, data); - } + trace(msg: string, data?: Record<string, unknown>) { + this.write("trace", msg, data); + } + debug(msg: string, data?: Record<string, unknown>) { + this.write("debug", msg, data); + } + info(msg: string, data?: Record<string, unknown>) { + this.write("info", msg, data); + } + warn(msg: string, data?: Record<string, unknown>) { + this.write("warn", msg, data); + } + error(msg: string, data?: Record<string, unknown>) { + this.write("error", msg, data); + } + fatal(msg: string, data?: Record<string, unknown>) { + this.write("fatal", msg, data); + } - child(bindings: Record<string, unknown>): KittenLogger { - return new Logger({ - ...this.options, - base: { ...this.base, ...bindings }, - }); - } + child(bindings: Record<string, unknown>): KittenLogger { + return new Logger({ + ...this.options, + base: { ...this.base, ...bindings }, + }); + } } const baseLogger = new Logger();
M
pkgs/core/src/options.ts
→
pkgs/core/src/options.ts
@@ -1,77 +1,77 @@
import { - type AutocompleteInteraction, - type User, - type Attachment, - type Role, - type GuildBasedChannel, - type ApplicationCommandOptionChoiceData, + type AutocompleteInteraction, + type User, + type Attachment, + type Role, + type GuildBasedChannel, + type ApplicationCommandOptionChoiceData, } from "discord.js"; export const OPTION_TYPES = { - subcommand: 1, - subcommandGroup: 2, - string: 3, - integer: 4, - boolean: 5, - user: 6, - channel: 7, - role: 8, - mentionable: 9, - number: 10, - attachment: 11, + subcommand: 1, + subcommandGroup: 2, + string: 3, + integer: 4, + boolean: 5, + user: 6, + channel: 7, + role: 8, + mentionable: 9, + number: 10, + attachment: 11, } as const; type OptionType = keyof typeof OPTION_TYPES; export interface Option<T, Req extends boolean> { - type: OptionType; - description: string; - required: Req; - autocomplete?: ( - interaction: AutocompleteInteraction, - value: T, - ) => Promise<ApplicationCommandOptionChoiceData[]> | ApplicationCommandOptionChoiceData[]; - __type?: T; + type: OptionType; + description: string; + required: Req; + autocomplete?: ( + interaction: AutocompleteInteraction, + value: T, + ) => Promise<ApplicationCommandOptionChoiceData[]> | ApplicationCommandOptionChoiceData[]; + __type?: T; } export type InferOptions<O> = { - [K in keyof O]: O[K] extends Option<infer Type, infer Required> - ? Required extends true - ? Type - : Type | undefined - : never; + [K in keyof O]: O[K] extends Option<infer Type, infer Required> + ? Required extends true + ? Type + : Type | undefined + : never; }; export type AutocompleteFn<T> = ( - interaction: AutocompleteInteraction, - value: T, + interaction: AutocompleteInteraction, + value: T, ) => Promise<ApplicationCommandOptionChoiceData[]> | ApplicationCommandOptionChoiceData[]; const createOption = <T>(type: OptionType) => { - return ((description: string, config?: any): Option<T, any> => ({ - type, - description, - required: config?.required ?? false, - autocomplete: config?.autocomplete, - })) as { - ( - description: string, - config: { required: true; autocomplete?: AutocompleteFn<T> }, - ): Option<T, true>; - ( - description: string, - config?: { required?: false; autocomplete?: AutocompleteFn<T> }, - ): Option<T, false>; - }; + return ((description: string, config?: any): Option<T, any> => ({ + type, + description, + required: config?.required ?? false, + autocomplete: config?.autocomplete, + })) as { + ( + description: string, + config: { required: true; autocomplete?: AutocompleteFn<T> }, + ): Option<T, true>; + ( + description: string, + config?: { required?: false; autocomplete?: AutocompleteFn<T> }, + ): Option<T, false>; + }; }; export const option = { - string: createOption<string>("string"), - integer: createOption<number>("integer"), - number: createOption<number>("number"), - boolean: createOption<boolean>("boolean"), - user: createOption<User>("user"), - channel: createOption<GuildBasedChannel>("channel"), - role: createOption<Role>("role"), - mentionable: createOption<User | Role | any>("mentionable"), - attachment: createOption<Attachment>("attachment"), + string: createOption<string>("string"), + integer: createOption<number>("integer"), + number: createOption<number>("number"), + boolean: createOption<boolean>("boolean"), + user: createOption<User>("user"), + channel: createOption<GuildBasedChannel>("channel"), + role: createOption<Role>("role"), + mentionable: createOption<User | Role | any>("mentionable"), + attachment: createOption<Attachment>("attachment"), };
M
pkgs/core/tsconfig.json
→
pkgs/core/tsconfig.json
@@ -1,30 +1,30 @@
{ - "compilerOptions": { - // Environment setup & latest features - "lib": ["ESNext"], - "target": "ESNext", - "module": "Preserve", - "moduleDetection": "force", - "jsx": "react-jsx", - "allowJs": true, - "types": ["bun"], + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + "types": ["bun"], - // Bundler mode - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, - // Best practices - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true, - "noImplicitOverride": true, + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, - // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false - } + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } }
M
tsconfig.json
→
tsconfig.json
@@ -1,30 +1,30 @@
{ - "compilerOptions": { - // Environment setup & latest features - "lib": ["ESNext"], - "target": "ESNext", - "module": "Preserve", - "moduleDetection": "force", - "jsx": "react-jsx", - "allowJs": true, - "types": ["bun"], + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + "types": ["bun"], - // Bundler mode - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, - // Best practices - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true, - "noImplicitOverride": true, + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, - // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false - } + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } }