31 KiB
Inventory Display Redesign Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Redesign the /inventory command into a polished Components V2 experience with rarity emojis, paginated list, item detail view with artwork, and inline item actions (use/discard).
Architecture: Rewrite inventory.view.ts to produce CV2 containers for list and detail views. Rewrite inventory.ts command to manage pagination and view state with a collector. Add inventory.interaction.ts for interaction routing. Extend RARITY_CONFIG with square emojis.
Tech Stack: discord.js Components V2 (ContainerBuilder, TextDisplayBuilder, SectionBuilder, MediaGalleryBuilder, ActionRowBuilder, ButtonBuilder, StringSelectMenuBuilder), Drizzle ORM, Bun test runner.
File Structure
| File | Action | Responsibility |
|---|---|---|
shared/lib/rarity.ts |
Modify | Add squareEmoji field to RARITY_CONFIG |
bot/modules/inventory/inventory.view.ts |
Rewrite | CV2 list message builder, CV2 detail message builder (keep getLootboxResultMessage untouched) |
bot/modules/inventory/inventory.interaction.ts |
Create | Handle all inventory interactions (select, pagination, back, use, discard, confirm) |
bot/commands/inventory/inventory.ts |
Rewrite | Command definition with view subcommand, pagination collector, autocomplete |
Task 1: Add squareEmoji to RARITY_CONFIG
Files:
-
Modify:
shared/lib/rarity.ts -
Step 1: Update the RARITY_CONFIG type and entries
In shared/lib/rarity.ts, update the type signature and add squareEmoji to each entry:
/**
* Shared Rarity Configuration
* Provides the canonical rarity display config (colors, emoji, labels)
* used by lootbox pull results and shop loot table views.
*/
export const RARITY_CONFIG: Record<string, { color: number; emoji: string; squareEmoji: string; label: string }> = {
C: { color: 0x95A5A6, emoji: "📦", squareEmoji: "🟤", label: "Common" },
R: { color: 0x3498DB, emoji: "📦", squareEmoji: "🔵", label: "Rare" },
SR: { color: 0x9B59B6, emoji: "✨", squareEmoji: "🟣", label: "Super Rare" },
SSR: { color: 0xF1C40F, emoji: "🌟", squareEmoji: "🟡", label: "SSR" },
CURRENCY: { color: 0x2ECC71, emoji: "💰", squareEmoji: "💰", label: "Currency" },
XP: { color: 0x1ABC9C, emoji: "🔮", squareEmoji: "🔮", label: "Experience" },
NOTHING: { color: 0x636363, emoji: "💨", squareEmoji: "💨", label: "Empty" },
};
export function getRarityConfig(rarity: string): { color: number; emoji: string; squareEmoji: string; label: string } {
return RARITY_CONFIG[rarity] ?? (RARITY_CONFIG["C"]!);
}
- Step 2: Verify nothing is broken
Run: bun test
Expected: All existing tests pass (lootbox and other rarity consumers still work since they access emoji, not squareEmoji).
- Step 3: Commit
git add shared/lib/rarity.ts
git commit -m "feat(inventory): add squareEmoji to RARITY_CONFIG"
Task 2: Build the inventory list view (CV2)
Files:
- Rewrite:
bot/modules/inventory/inventory.view.ts
This task rewrites getInventoryEmbed → getInventoryListMessage and adds getItemDetailMessage. The existing getLootboxResultMessage function stays untouched.
- Step 1: Define constants and types at the top of inventory.view.ts
Replace the existing InventoryEntry interface and add constants. Keep all existing imports and add the new ones needed:
import {
EmbedBuilder,
AttachmentBuilder,
ContainerBuilder,
SectionBuilder,
TextDisplayBuilder,
MediaGalleryBuilder,
MediaGalleryItemBuilder,
ThumbnailBuilder,
SeparatorBuilder,
SeparatorSpacingSize,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
StringSelectMenuBuilder,
StringSelectMenuOptionBuilder,
MessageFlags,
} from "discord.js";
import { resolveAssetUrl, isLocalAssetUrl } from "@shared/lib/assets";
import { getRarityConfig, defaultName, stripQuery } from "@shared/lib/rarity";
import { ItemType } from "@shared/lib/constants";
import type { ItemUsageData } from "@shared/lib/types";
import { join } from "path";
import { existsSync } from "fs";
export const ITEMS_PER_PAGE = 5;
const RARITY_SORT_ORDER: Record<string, number> = {
SSR: 0,
SR: 1,
R: 2,
C: 3,
};
export interface InventoryItem {
id: number;
name: string;
description: string | null;
rarity: string | null;
type: string;
price: bigint | null;
iconUrl: string;
imageUrl: string;
usageData: unknown;
}
export interface InventoryEntry {
quantity: bigint | null;
item: InventoryItem;
}
- Step 2: Add the sortInventoryItems helper
export function sortInventoryItems(entries: InventoryEntry[]): InventoryEntry[] {
return [...entries].sort((a, b) => {
const rarityA = RARITY_SORT_ORDER[a.item.rarity ?? "C"] ?? 3;
const rarityB = RARITY_SORT_ORDER[b.item.rarity ?? "C"] ?? 3;
if (rarityA !== rarityB) return rarityA - rarityB;
return a.item.name.localeCompare(b.item.name);
});
}
- Step 3: Implement getInventoryListMessage
export function getInventoryListMessage(
entries: InventoryEntry[],
username: string,
page: number,
viewerId: string,
ownerId: string,
) {
const sorted = sortInventoryItems(entries);
const totalPages = Math.max(1, Math.ceil(sorted.length / ITEMS_PER_PAGE));
const safePage = Math.min(page, totalPages - 1);
const pageItems = sorted.slice(safePage * ITEMS_PER_PAGE, (safePage + 1) * ITEMS_PER_PAGE);
// Accent color from highest-rarity item on page
const highestRarity = pageItems[0]?.item.rarity ?? "C";
const accentColor = getRarityConfig(highestRarity).color;
const container = new ContainerBuilder()
.setAccentColor(accentColor)
.addTextDisplayComponents(
new TextDisplayBuilder().setContent(`# 📦 ${username}'s Inventory`),
new TextDisplayBuilder().setContent(`-# ${sorted.length} item${sorted.length !== 1 ? "s" : ""} total`)
);
container.addSeparatorComponents(
new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small)
);
// Item rows
const lines = pageItems.map((entry) => {
const rc = getRarityConfig(entry.item.rarity ?? "C");
return `${rc.squareEmoji} **${entry.item.name}** — ${rc.label} · ${entry.item.type} · ×${entry.quantity}`;
});
container.addTextDisplayComponents(
new TextDisplayBuilder().setContent(lines.join("\n"))
);
container.addSeparatorComponents(
new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small)
);
// Select menu with current page items
const selectMenu = new StringSelectMenuBuilder()
.setCustomId(`inv_select_${viewerId}`)
.setPlaceholder("Select an item for details");
for (const entry of pageItems) {
const rc = getRarityConfig(entry.item.rarity ?? "C");
selectMenu.addOptions(
new StringSelectMenuOptionBuilder()
.setLabel(entry.item.name)
.setDescription(`${rc.label} · ${entry.item.type}`)
.setValue(entry.item.id.toString())
);
}
container.addActionRowComponents(
new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(selectMenu)
);
// Pagination buttons
const navRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId(`inv_prev_${viewerId}`)
.setLabel("◀ Previous")
.setStyle(ButtonStyle.Secondary)
.setDisabled(safePage <= 0),
new ButtonBuilder()
.setCustomId(`inv_page_${viewerId}`)
.setLabel(`Page ${safePage + 1}/${totalPages}`)
.setStyle(ButtonStyle.Secondary)
.setDisabled(true),
new ButtonBuilder()
.setCustomId(`inv_next_${viewerId}`)
.setLabel("Next ▶")
.setStyle(ButtonStyle.Secondary)
.setDisabled(safePage >= totalPages - 1),
);
container.addActionRowComponents(navRow);
return {
components: [container] as any,
files: [] as AttachmentBuilder[],
flags: MessageFlags.IsComponentsV2,
embeds: [],
};
}
- Step 4: Implement getEmptyInventoryMessage
export function getEmptyInventoryMessage(username: string) {
const container = new ContainerBuilder()
.setAccentColor(0x95A5A6)
.addTextDisplayComponents(
new TextDisplayBuilder().setContent(`# 📦 ${username}'s Inventory`),
new TextDisplayBuilder().setContent("*No items yet. Visit the shop or complete quests to earn items!*")
);
return {
components: [container] as any,
files: [],
flags: MessageFlags.IsComponentsV2,
embeds: [],
};
}
- Step 5: Implement getItemDetailMessage
export function getItemDetailMessage(
entry: InventoryEntry,
viewerId: string,
ownerId: string,
) {
const { item } = entry;
const rc = getRarityConfig(item.rarity ?? "C");
const files: AttachmentBuilder[] = [];
const container = new ContainerBuilder().setAccentColor(rc.color);
// Header section with thumbnail
const section = new SectionBuilder().addTextDisplayComponents(
new TextDisplayBuilder().setContent(`${rc.squareEmoji} **${item.name}**`),
new TextDisplayBuilder().setContent(`-# ${rc.label} · ${item.type}`)
);
// Resolve icon thumbnail
const iconUrl = resolveItemUrl(item.iconUrl, files);
if (iconUrl) {
section.setThumbnailAccessory(new ThumbnailBuilder().setURL(iconUrl));
}
container.addSectionComponents(section);
// Artwork via MediaGallery
const imageUrl = resolveItemUrl(item.imageUrl, files);
if (imageUrl && item.imageUrl !== item.iconUrl) {
container.addMediaGalleryComponents(
new MediaGalleryBuilder().addItems(
new MediaGalleryItemBuilder().setURL(imageUrl)
)
);
}
// Description
if (item.description) {
container.addTextDisplayComponents(
new TextDisplayBuilder().setContent(item.description)
);
}
container.addSeparatorComponents(
new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small)
);
// Stats row
const priceText = item.price ? `${item.price} 🪙` : "Not tradeable";
container.addTextDisplayComponents(
new TextDisplayBuilder().setContent(
`Owned: **×${entry.quantity}** · Value: **${priceText}**`
)
);
// Action buttons
const isOwner = viewerId === ownerId;
const usageData = item.usageData as ItemUsageData | null;
const isUsable = isOwner && item.type === ItemType.CONSUMABLE &&
usageData?.effects && usageData.effects.length > 0;
const actionRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId(`inv_back_${viewerId}`)
.setLabel("◀ Back")
.setStyle(ButtonStyle.Primary)
);
if (isUsable) {
actionRow.addComponents(
new ButtonBuilder()
.setCustomId(`inv_use_${viewerId}`)
.setLabel("🧪 Use")
.setStyle(ButtonStyle.Success)
);
}
if (isOwner) {
actionRow.addComponents(
new ButtonBuilder()
.setCustomId(`inv_discard_${viewerId}`)
.setLabel("🗑 Discard")
.setStyle(ButtonStyle.Danger)
);
}
container.addActionRowComponents(actionRow);
return {
components: [container] as any,
files,
flags: MessageFlags.IsComponentsV2,
embeds: [],
};
}
- Step 6: Implement getDiscardConfirmMessage and the resolveItemUrl helper
export function getDiscardConfirmMessage(entry: InventoryEntry, viewerId: string) {
const rc = getRarityConfig(entry.item.rarity ?? "C");
const container = new ContainerBuilder()
.setAccentColor(0xED4245)
.addTextDisplayComponents(
new TextDisplayBuilder().setContent(
`Are you sure you want to discard 1× **${entry.item.name}**?`
)
)
.addActionRowComponents(
new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId(`inv_discard_confirm_${viewerId}`)
.setLabel("Confirm")
.setStyle(ButtonStyle.Danger),
new ButtonBuilder()
.setCustomId(`inv_discard_cancel_${viewerId}`)
.setLabel("Cancel")
.setStyle(ButtonStyle.Secondary)
)
);
return {
components: [container] as any,
files: [],
flags: MessageFlags.IsComponentsV2,
embeds: [],
};
}
/**
* Resolves an item URL (icon or image) for use in CV2 components.
* Handles both local assets and remote URLs.
* Pushes AttachmentBuilders to `files` array for local assets.
*/
function resolveItemUrl(url: string | null | undefined, files: AttachmentBuilder[]): string | null {
if (!url) return null;
if (isLocalAssetUrl(url)) {
const filePath = join(process.cwd(), "bot/assets/graphics", stripQuery(url).replace(/^\/?assets\//, ""));
if (existsSync(filePath)) {
const fileName = defaultName(url);
if (!files.find(f => f.name === fileName)) {
files.push(new AttachmentBuilder(filePath, { name: fileName }));
}
return `attachment://${fileName}`;
}
return null;
}
return resolveAssetUrl(url);
}
- Step 7: Verify the file compiles
Run: bunx tsc --noEmit bot/modules/inventory/inventory.view.ts
Expected: No type errors. (Note: getLootboxResultMessage remains unchanged below all the new code.)
- Step 8: Commit
git add bot/modules/inventory/inventory.view.ts
git commit -m "feat(inventory): rewrite inventory view with CV2 list and detail builders"
Task 3: Create the inventory interaction handler
Files:
-
Create:
bot/modules/inventory/inventory.interaction.ts -
Step 1: Create the interaction handler file
import type { StringSelectMenuInteraction, ButtonInteraction, MessageFlags } from "discord.js";
import { inventoryService } from "@shared/modules/inventory/inventory.service";
import { getLootboxResultMessage } from "./inventory.view";
import type { ItemUsageData } from "@shared/lib/types";
import { getGuildConfig } from "@shared/lib/config";
export interface InventoryState {
ownerId: string;
viewerId: string;
page: number;
selectedItemId: number | null;
}
/**
* Extracts the viewer user ID from an inventory custom ID.
* Custom IDs follow the format: inv_{action}_{viewerId}
*/
export function parseInventoryCustomId(customId: string): { action: string; viewerId: string } | null {
const match = customId.match(/^inv_(\w+?)_(\d+)$/);
if (!match) return null;
return { action: match[1], viewerId: match[2] };
}
/**
* Checks if a custom ID belongs to the inventory system.
*/
export function isInventoryInteraction(customId: string): boolean {
return customId.startsWith("inv_");
}
/**
* Handles the "Use" button — executes item effects.
* Returns the result messages array from inventoryService.useItem,
* plus handles role-based effects that require the guild member.
*/
export async function executeItemUse(
interaction: ButtonInteraction,
userId: string,
itemId: number,
): Promise<{ results: any[]; usageData: ItemUsageData | null; item: any }> {
const result = await inventoryService.useItem(userId, itemId);
// Handle role effects (same logic as /use command)
const usageData = result.usageData;
if (usageData) {
const guildConfig = await getGuildConfig(interaction.guildId!);
const colorRoles = guildConfig.colorRoles ?? [];
for (const effect of usageData.effects) {
if (effect.type === "TEMP_ROLE" || effect.type === "COLOR_ROLE") {
try {
const member = await interaction.guild?.members.fetch(userId);
if (member) {
if (effect.type === "TEMP_ROLE") {
await member.roles.add(effect.roleId);
} else if (effect.type === "COLOR_ROLE") {
const rolesToRemove = colorRoles.filter((r: string) => member.roles.cache.has(r));
if (rolesToRemove.length > 0) await member.roles.remove(rolesToRemove);
await member.roles.add(effect.roleId);
}
}
} catch (e) {
console.error("Failed to assign role in inventory use:", e);
result.results.push("⚠️ Failed to assign role (Check bot permissions)");
}
}
}
}
return result;
}
- Step 2: Verify the file compiles
Run: bunx tsc --noEmit bot/modules/inventory/inventory.interaction.ts
Expected: No type errors.
- Step 3: Commit
git add bot/modules/inventory/inventory.interaction.ts
git commit -m "feat(inventory): add inventory interaction handler utilities"
Task 4: Rewrite the inventory command
Files:
-
Rewrite:
bot/commands/inventory/inventory.ts -
Step 1: Rewrite the command with subcommands, collector, and interaction routing
import { createCommand } from "@shared/lib/utils";
import { SlashCommandBuilder, MessageFlags, ComponentType } from "discord.js";
import { inventoryService } from "@shared/modules/inventory/inventory.service";
import { userService } from "@shared/modules/user/user.service";
import { createWarningEmbed, createErrorEmbed } from "@lib/embeds";
import {
getInventoryListMessage,
getEmptyInventoryMessage,
getItemDetailMessage,
getDiscardConfirmMessage,
sortInventoryItems,
ITEMS_PER_PAGE,
type InventoryEntry,
} from "@/modules/inventory/inventory.view";
import { getLootboxResultMessage } from "@/modules/inventory/inventory.view";
import {
parseInventoryCustomId,
isInventoryInteraction,
executeItemUse,
} from "@/modules/inventory/inventory.interaction";
import { UserError } from "@shared/lib/errors";
export const inventory = createCommand({
data: new SlashCommandBuilder()
.setName("inventory")
.setDescription("View your or another user's inventory")
.addSubcommand(sub =>
sub.setName("list")
.setDescription("View your or another user's inventory")
.addUserOption(option =>
option.setName("user")
.setDescription("User to view")
.setRequired(false)
)
)
.addSubcommand(sub =>
sub.setName("view")
.setDescription("View details of a specific item")
.addNumberOption(option =>
option.setName("item")
.setDescription("The item to view")
.setRequired(true)
.setAutocomplete(true)
)
),
execute: async (interaction) => {
await interaction.deferReply();
const viewerId = interaction.user.id;
const subcommand = interaction.options.getSubcommand();
if (subcommand === "view") {
// Direct item detail view
const itemId = interaction.options.getNumber("item", true);
const user = await userService.getOrCreateUser(viewerId, interaction.user.username);
if (!user) {
await interaction.editReply({ embeds: [createWarningEmbed("Failed to load user data.", "Error")] });
return;
}
const entries = await inventoryService.getInventory(user.id.toString());
const entry = entries.find((e: any) => e.item.id === itemId);
if (!entry) {
await interaction.editReply({ embeds: [createWarningEmbed("Item not found in your inventory.", "Not Found")] });
return;
}
const ownerId = user.id.toString();
let currentPage = 0;
let selectedItemId: number | null = itemId;
const response = await interaction.editReply(
getItemDetailMessage(entry as InventoryEntry, viewerId, ownerId)
);
await setupCollector(interaction, response, viewerId, ownerId, user.username, currentPage, selectedItemId);
return;
}
// "list" subcommand
const targetUser = interaction.options.getUser("user") || interaction.user;
if (targetUser.bot) {
await interaction.editReply({ embeds: [createWarningEmbed("Bots do not have inventories.", "Inventory Check")] });
return;
}
const user = await userService.getOrCreateUser(targetUser.id, targetUser.username);
if (!user) {
await interaction.editReply({ embeds: [createWarningEmbed("Failed to load user data.", "Error")] });
return;
}
const ownerId = user.id.toString();
const entries = await inventoryService.getInventory(ownerId);
if (!entries || entries.length === 0) {
await interaction.editReply(getEmptyInventoryMessage(user.username));
return;
}
let currentPage = 0;
let selectedItemId: number | null = null;
const response = await interaction.editReply(
getInventoryListMessage(entries as InventoryEntry[], user.username, currentPage, viewerId, ownerId)
);
await setupCollector(interaction, response, viewerId, ownerId, user.username, currentPage, selectedItemId);
},
autocomplete: async (interaction) => {
const focusedValue = interaction.options.getFocused();
const userId = interaction.user.id;
const results = await inventoryService.getAutocompleteItems(userId, focusedValue);
await interaction.respond(results);
},
});
async function setupCollector(
interaction: any,
response: any,
viewerId: string,
ownerId: string,
username: string,
initialPage: number,
initialItemId: number | null,
) {
let currentPage = initialPage;
let selectedItemId = initialItemId;
const collector = response.createMessageComponentCollector({
time: 120_000,
});
collector.on("collect", async (i: any) => {
if (i.user.id !== viewerId) return;
const parsed = parseInventoryCustomId(i.customId);
if (!parsed) return;
try {
await i.deferUpdate();
// Re-fetch inventory for fresh data
const entries = await inventoryService.getInventory(ownerId);
const sorted = sortInventoryItems(entries as InventoryEntry[]);
switch (parsed.action) {
case "select": {
const itemId = parseInt(i.values[0]);
const entry = sorted.find(e => e.item.id === itemId);
if (!entry) break;
selectedItemId = itemId;
await interaction.editReply(
getItemDetailMessage(entry, viewerId, ownerId)
);
break;
}
case "prev": {
currentPage = Math.max(0, currentPage - 1);
selectedItemId = null;
await interaction.editReply(
getInventoryListMessage(sorted, username, currentPage, viewerId, ownerId)
);
break;
}
case "next": {
currentPage = currentPage + 1;
selectedItemId = null;
await interaction.editReply(
getInventoryListMessage(sorted, username, currentPage, viewerId, ownerId)
);
break;
}
case "back": {
selectedItemId = null;
if (sorted.length === 0) {
await interaction.editReply(getEmptyInventoryMessage(username));
} else {
await interaction.editReply(
getInventoryListMessage(sorted, username, currentPage, viewerId, ownerId)
);
}
break;
}
case "use": {
if (viewerId !== ownerId || !selectedItemId) break;
try {
const result = await executeItemUse(i, viewerId, selectedItemId);
const message = getLootboxResultMessage(result.results, result.item);
await interaction.editReply(message as any);
// After showing result, wait briefly then return to detail or list
setTimeout(async () => {
try {
const freshEntries = await inventoryService.getInventory(ownerId);
const freshSorted = sortInventoryItems(freshEntries as InventoryEntry[]);
const freshEntry = freshSorted.find(e => e.item.id === selectedItemId);
if (freshEntry) {
await interaction.editReply(
getItemDetailMessage(freshEntry, viewerId, ownerId)
);
} else {
selectedItemId = null;
if (freshSorted.length === 0) {
await interaction.editReply(getEmptyInventoryMessage(username));
} else {
await interaction.editReply(
getInventoryListMessage(freshSorted, username, currentPage, viewerId, ownerId)
);
}
}
} catch {}
}, 3000);
} catch (error) {
if (error instanceof UserError) {
await interaction.editReply({
embeds: [createErrorEmbed(error.message)],
components: [],
flags: undefined,
});
} else {
throw error;
}
}
break;
}
case "discard": {
if (viewerId !== ownerId || !selectedItemId) break;
const entry = sorted.find(e => e.item.id === selectedItemId);
if (!entry) break;
await interaction.editReply(
getDiscardConfirmMessage(entry, viewerId)
);
break;
}
case "discard_confirm": {
if (viewerId !== ownerId || !selectedItemId) break;
try {
await inventoryService.removeItem(ownerId, selectedItemId, 1n);
const freshEntries = await inventoryService.getInventory(ownerId);
const freshSorted = sortInventoryItems(freshEntries as InventoryEntry[]);
const freshEntry = freshSorted.find(e => e.item.id === selectedItemId);
if (freshEntry) {
await interaction.editReply(
getItemDetailMessage(freshEntry, viewerId, ownerId)
);
} else {
selectedItemId = null;
if (freshSorted.length === 0) {
await interaction.editReply(getEmptyInventoryMessage(username));
} else {
await interaction.editReply(
getInventoryListMessage(freshSorted, username, currentPage, viewerId, ownerId)
);
}
}
} catch (error) {
if (error instanceof UserError) {
await interaction.editReply({
embeds: [createErrorEmbed(error.message)],
components: [],
flags: undefined,
});
} else {
throw error;
}
}
break;
}
case "discard_cancel": {
if (!selectedItemId) break;
const entry = sorted.find(e => e.item.id === selectedItemId);
if (!entry) break;
await interaction.editReply(
getItemDetailMessage(entry, viewerId, ownerId)
);
break;
}
}
} catch (error) {
console.error("Inventory interaction error:", error);
}
});
collector.on("end", () => {
interaction.editReply({ components: [] }).catch(() => {});
});
}
- Step 2: Verify the file compiles
Run: bunx tsc --noEmit bot/commands/inventory/inventory.ts
Expected: No type errors.
- Step 3: Commit
git add bot/commands/inventory/inventory.ts
git commit -m "feat(inventory): rewrite command with CV2 pagination and detail view"
Task 5: Integration testing and verification
Files:
-
All modified files
-
Step 1: Run the full test suite
Run: bun test
Expected: All tests pass. The inventory service tests should still pass since we didn't change the service.
- Step 2: Verify TypeScript compiles cleanly
Run: bunx tsc --noEmit
Expected: No type errors across the entire project.
- Step 3: Verify the bot starts
Run: bun --watch bot/index.ts (start and verify no startup errors, then stop)
Expected: Bot initializes and registers commands without errors.
- Step 4: Final commit if any fixes were needed
git add -A
git commit -m "fix(inventory): address integration issues from inventory redesign"
(Only if fixes were needed in the previous steps.)