all repos — kitten @ 2f9ac75e21a898fba92010820bd8b6df696bc368

support setting integration contexts & types
vi did:web:vt3e.cat
Fri, 12 Jun 2026 04:23:25 +0100
commit

2f9ac75e21a898fba92010820bd8b6df696bc368

parent

a8518eafa7b03c967acd7fcdb1192661c80bf151

4 files changed, 164 insertions(+), 22 deletions(-)

jump to
A .oxfmtrc.json

@@ -0,0 +1,5 @@

+{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "ignorePatterns": [], + "useTabs": true +}
A .oxlintrc.json

@@ -0,0 +1,15 @@

+{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": [ + "typescript", + "unicorn", + "oxc" + ], + "categories": { + "correctness": "error" + }, + "rules": {}, + "env": { + "builtin": true + } +}
M pkgs/core/src/commands.tspkgs/core/src/commands.ts

@@ -6,6 +6,31 @@ MessageContextMenuCommandInteraction,

} from "discord.js"; import type { Option, InferOptions } from "./options"; +export const INTEGRATION_TYPES = { + GUILD_INSTALL: 0, + USER_INSTALL: 1, +} as const; +export const INTERACTION_CONTEXTS = { + 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], +}; +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], +}; + +export type IntegrationType = (typeof INTEGRATION_TYPES)[keyof typeof INTEGRATION_TYPES]; +export type InteractionContextType = + (typeof INTERACTION_CONTEXTS)[keyof typeof INTERACTION_CONTEXTS]; + export class Subcommand { constructor( public name: string,

@@ -50,6 +75,10 @@ constructor(

public name: string, public description: string, public middlewares: Function[] = [], + public config?: { + integrationTypes?: IntegrationType[]; + contexts?: InteractionContextType[]; + }, ) {} subcommand<O extends Record<string, Option<any, any>>>(

@@ -86,6 +115,8 @@ public name: string,

public config: { description: string; options?: Record<string, Option<any, any>>; + integrationTypes?: IntegrationType[]; + contexts?: InteractionContextType[]; run: Function; }, public middlewares: Function[] = [],

@@ -98,6 +129,10 @@ public name: string,

public type: "user" | "message", public run: Function, public middlewares: Function[] = [], + public config?: { + integrationTypes?: IntegrationType[]; + contexts?: InteractionContextType[]; + }, ) {} }

@@ -115,6 +150,8 @@ name: string,

config: { description: string; options?: O; + integrationTypes?: IntegrationType[]; + contexts?: InteractionContextType[]; run: ( interaction: ChatInputCommandInteraction, args: InferOptions<O>,

@@ -123,26 +160,63 @@ ) => Promise<void> | void;

}, ): Command; - command(name: string, config: { description: string }): 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); + return new SubcommandGroupBuilder<Ctx>(name, config.description, this.middlewares, { + integrationTypes: config.integrationTypes, + contexts: config.contexts, + }); } userContextMenu( name: string, - run: (interaction: UserContextMenuCommandInteraction, ctx: Ctx) => Promise<void> | void, + runOrConfig: + | ((interaction: UserContextMenuCommandInteraction, ctx: Ctx) => Promise<void> | void) + | { + integrationTypes?: IntegrationType[]; + contexts?: InteractionContextType[]; + run: (interaction: UserContextMenuCommandInteraction, ctx: Ctx) => Promise<void> | void; + }, ): ContextMenuCommand { - return new ContextMenuCommand(name, "user", run, this.middlewares); + 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, + }); } messageContextMenu( name: string, - run: (interaction: MessageContextMenuCommandInteraction, ctx: Ctx) => Promise<void> | void, + runOrConfig: + | ((interaction: MessageContextMenuCommandInteraction, ctx: Ctx) => Promise<void> | void) + | { + integrationTypes?: IntegrationType[]; + contexts?: InteractionContextType[]; + run: ( + interaction: MessageContextMenuCommandInteraction, + ctx: Ctx, + ) => Promise<void> | void; + }, ): ContextMenuCommand { - return new ContextMenuCommand(name, "message", run, this.middlewares); + 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, + }); } }
M pkgs/core/src/kitten.tspkgs/core/src/kitten.ts

@@ -10,13 +10,21 @@ 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 } from "./commands"; +import { + Command, + CommandBuilder, + SubcommandGroupBuilder, + ContextMenuCommand, + type IntegrationType, + type InteractionContextType, +} from "./commands"; import { baseLogger, type KittenLogger } from "./logger"; export interface KittenOptions {

@@ -47,6 +55,8 @@ name: string,

config: { description: string; options?: O; + integrationTypes?: IntegrationType[]; + contexts?: InteractionContextType[]; run: ( interaction: ChatInputCommandInteraction, args: InferOptions<O>,

@@ -54,7 +64,14 @@ ) => Promise<void> | void;

}, ): Command; - command(name: string, config: { description: string }): SubcommandGroupBuilder<{}>; + command( + name: string, + config: { + description: string; + integrationTypes?: IntegrationType[]; + contexts?: InteractionContextType[]; + }, + ): SubcommandGroupBuilder<{}>; command(name: string, config: any): any { return this.builder().command(name, config);

@@ -62,16 +79,28 @@ }

userContextMenu( name: string, - run: (interaction: UserContextMenuCommandInteraction) => Promise<void> | void, + runOrConfig: + | ((interaction: UserContextMenuCommandInteraction) => Promise<void> | void) + | { + integrationTypes?: IntegrationType[]; + contexts?: InteractionContextType[]; + run: (interaction: UserContextMenuCommandInteraction) => Promise<void> | void; + }, ): ContextMenuCommand { - return this.builder().userContextMenu(name, run); + return this.builder().userContextMenu(name, runOrConfig as any); } messageContextMenu( name: string, - run: (interaction: MessageContextMenuCommandInteraction) => Promise<void> | void, + runOrConfig: + | ((interaction: MessageContextMenuCommandInteraction) => Promise<void> | void) + | { + integrationTypes?: IntegrationType[]; + contexts?: InteractionContextType[]; + run: (interaction: MessageContextMenuCommandInteraction) => Promise<void> | void; + }, ): ContextMenuCommand { - return this.builder().messageContextMenu(name, run); + return this.builder().messageContextMenu(name, runOrConfig as any); } button<O extends Record<string, Option<any, any>>>(

@@ -215,7 +244,6 @@ } else {

await run(interaction, context); } } catch (err) { - // TODO)) custom error handlers if (err instanceof HaltExecution) return; this.logger.error(`unhandled error executing command ${interaction.commandName}:`, {

@@ -318,7 +346,7 @@ return parsed;

} async sync({ guildId }: { guildId?: string } = {}) { - const payloads: any[] = []; + const payloads: RESTPostAPIApplicationCommandsJSONBody[] = []; // chat input commands for (const cmd of this.commands.values()) {

@@ -342,35 +370,55 @@ options: this.transformOptionsForDiscord(sub.config.options),

})), })); - payloads.push({ + 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; + + payloads.push(payload); } else { - payloads.push({ + 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; + + payloads.push(payload); } } // context menus for (const cmd of this.userContextMenus.values()) { - payloads.push({ + 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; + + payloads.push(payload); } for (const cmd of this.messageContextMenus.values()) { - payloads.push({ + 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; + + payloads.push(payload); } if (!this.client.application) {