Popii - v0.5.1
    Preparing search index...

    Commands

    Commands are the primary way users interact with your bot. Each file in src/commands/ exports a single command() definition, and Popii registers it as a slash command automatically.

    // src/commands/hello.ts
    import { command } from "popii";

    export default command({
    name: "hello",
    description: "Say hello back",
    async do(pop) {
    await pop.reply(`Hello, ${pop.user.username}!`);
    }
    });

    Nested folders become subcommand groups automatically:

    src/commands/
    config/
    set.ts → /config set
    reset.ts → /config reset
    admin/
    ban.ts → /admin ban
    ping.ts → /ping

    Use options to declare slash command parameters. The values are accessed via pop.options:

    import { command } from "popii";
    import { ApplicationCommandOptionType } from "discord.js";

    export default command({
    name: "greet",
    description: "Greet a user",
    options: [
    {
    name: "user",
    description: "Who to greet",
    type: ApplicationCommandOptionType.User,
    required: true,
    },
    {
    name: "message",
    description: "Custom greeting message",
    type: ApplicationCommandOptionType.String,
    }
    ],
    async do(pop) {
    const target = pop.options.getUser("user")!;
    const msg = pop.options.getString("message") ?? "Hello!";
    await pop.reply(`${msg}, ${target}!`);
    }
    });

    Pass a schema to validate and type the options object as pop.input:

    import { command } from "popii";
    import { z } from "zod";
    import { ApplicationCommandOptionType } from "discord.js";

    export default command({
    name: "echo",
    description: "Repeats your text",
    options: [{ name: "text", description: "Text to echo", type: ApplicationCommandOptionType.String, required: true }],
    schema: z.object({ text: z.string().min(1).max(200) }),
    async do(pop) {
    await pop.reply(pop.input.text); // fully typed
    }
    });
    export default command({
    name: "daily",
    description: "Claim your daily reward",
    cooldown: { ms: 86_400_000, scope: "user" }, // once per 24h per user
    async do(pop) {
    await pop.reply("Here's your daily reward!");
    }
    });

    scope can be "user" (default), "guild", or "global".

    import { PermissionFlagsBits } from "discord.js";

    export default command({
    name: "kick",
    description: "Kick a member",
    guildOnly: true,
    permissions: ["KickMembers"], // requires this Discord permission
    allowedRoles: ["123456789012345678"], // or restrict to specific role IDs
    // ...
    });
    export default command({
    ephemeral: true, // all replies from this command are ephemeral by default
    // ...
    async do(pop) {
    await pop.reply("Only you can see this.");
    }
    });

    You can also pass ephemeral: true inside pop.reply() for per-reply control.

    For commands that take longer than ~3 seconds, defer first:

    async do(pop) {
    await pop.defer();
    const result = await someLongOperation();
    await pop.reply(result);
    }
    import { ApplicationCommandType } from "discord.js";

    export default command({
    name: "Report Message",
    type: ApplicationCommandType.Message,
    async do(pop) {
    const msg = pop.targetMessage!;
    await pop.reply({ content: `Reported: "${msg.content}"`, ephemeral: true });
    }
    });
    export default command({
    name: "ping",
    description: "Legacy prefix command",
    text: true, // enables !ping
    slash: false, // disable slash registration
    async do(pop) {
    await pop.reply("Pong!");
    }
    });

    Set prefix in popiiClient() config to enable text commands globally.

    From the web dashboard or programmatically:

    client._disabledCommands.add("ping");
    

    Disabled commands still appear in Discord's command list but return an "unavailable" message when invoked.