forked from syntaxbullet/aurorabot
6.8 KiB
6.8 KiB
AGENTS.md - AI Coding Agent Guidelines
Project Overview
AuroraBot is a Discord bot with a web dashboard built using Bun, Discord.js, React, and PostgreSQL with Drizzle ORM.
Build/Lint/Test Commands
# Development
bun --watch bot/index.ts # Run bot with hot reload
bun --hot web/src/index.ts # Run web dashboard with hot reload
# Testing
bun test # Run all tests
bun test path/to/file.test.ts # Run single test file
bun test --watch # Watch mode
bun test shared/modules/economy # Run tests in directory
# Database
bun run generate # Generate Drizzle migrations (Docker)
bun run migrate # Run migrations (Docker)
bun run db:push # Push schema changes (Docker)
bun run db:push:local # Push schema changes (local)
bun run db:studio # Open Drizzle Studio
# Web Dashboard
cd web && bun run build # Build production web assets
cd web && bun run dev # Development server
Project Structure
bot/ # Discord bot
├── commands/ # Slash commands by category
├── events/ # Discord event handlers
├── lib/ # Bot core (BotClient, handlers, loaders)
├── modules/ # Feature modules (views, interactions)
└── graphics/ # Canvas image generation
shared/ # Shared between bot and web
├── db/ # Database schema and migrations
├── lib/ # Utils, config, errors, types
└── modules/ # Domain services (economy, user, etc.)
web/ # React dashboard
├── src/pages/ # React pages
├── src/components/ # UI components (ShadCN/Radix)
└── src/hooks/ # React hooks
Import Conventions
Use path aliases defined in tsconfig.json:
// External packages first
import { SlashCommandBuilder } from "discord.js";
import { eq } from "drizzle-orm";
// Path aliases second
import { economyService } from "@shared/modules/economy/economy.service";
import { UserError } from "@shared/lib/errors";
import { users } from "@db/schema";
import { createErrorEmbed } from "@lib/embeds";
import { handleTradeInteraction } from "@modules/trade/trade.interaction";
// Relative imports last
import { localHelper } from "./helper";
Available Aliases:
@/*- bot/@shared/*- shared/@db/*- shared/db/@lib/*- bot/lib/@modules/*- bot/modules/@commands/*- bot/commands/
Naming Conventions
| Element | Convention | Example |
|---|---|---|
| Files | camelCase or kebab-case | BotClient.ts, economy.service.ts |
| Classes | PascalCase | CommandHandler, UserError |
| Functions | camelCase | createCommand, handleShopInteraction |
| Constants | UPPER_SNAKE_CASE | EVENTS, BRANDING |
| Enums | PascalCase | TimerType, TransactionType |
| Services | camelCase singleton | economyService, userService |
| Types/Interfaces | PascalCase | Command, Event, GameConfigType |
| DB tables | snake_case | users, moderation_cases |
| Custom IDs | snake_case with prefix | shop_buy_, trade_accept_ |
Code Patterns
Command Definition
export const commandName = createCommand({
data: new SlashCommandBuilder()
.setName("commandname")
.setDescription("Description"),
execute: async (interaction) => {
await interaction.deferReply();
// Implementation
}
});
Service Pattern (Singleton Object)
export const serviceName = {
methodName: async (params: ParamType): Promise<ReturnType> => {
return await withTransaction(async (tx) => {
// Database operations
});
},
};
Module File Organization
*.view.ts- Creates Discord embeds/components*.interaction.ts- Handles button/select/modal interactions*.types.ts- Module-specific TypeScript types*.service.ts- Business logic (in shared/modules/)*.test.ts- Test files (co-located with source)
Error Handling
Custom Error Classes
import { UserError, SystemError } from "@shared/lib/errors";
// User-facing errors (shown to user)
throw new UserError("You don't have enough coins!");
// System errors (logged, generic message shown)
throw new SystemError("Database connection failed");
Standard Error Pattern
try {
const result = await service.method();
await interaction.editReply({ embeds: [createSuccessEmbed(result)] });
} catch (error) {
if (error instanceof UserError) {
await interaction.editReply({ embeds: [createErrorEmbed(error.message)] });
} else {
console.error("Unexpected error:", error);
await interaction.editReply({ embeds: [createErrorEmbed("An unexpected error occurred.")] });
}
}
Database Patterns
Transaction Usage
import { withTransaction } from "@/lib/db";
return await withTransaction(async (tx) => {
const user = await tx.query.users.findFirst({
where: eq(users.id, discordId)
});
await tx.update(users).set({ coins: newBalance }).where(eq(users.id, discordId));
await tx.insert(transactions).values({ userId: discordId, amount, type });
return user;
}, existingTx); // Pass existing tx if in nested transaction
Schema Notes
- Use
bigintmode for Discord IDs and currency amounts - Relations defined separately from table definitions
- Schema location:
shared/db/schema.ts
Testing
Test File Structure
import { describe, it, expect, mock, beforeEach } from "bun:test";
// Mock modules BEFORE imports
mock.module("@shared/db/DrizzleClient", () => ({
DrizzleClient: { query: mockQuery }
}));
describe("serviceName", () => {
beforeEach(() => {
mockFn.mockClear();
});
it("should handle expected case", async () => {
// Arrange
mockFn.mockResolvedValue(testData);
// Act
const result = await service.method(input);
// Assert
expect(result).toEqual(expected);
expect(mockFn).toHaveBeenCalledWith(expectedArgs);
});
});
Tech Stack
- Runtime: Bun 1.0+
- Bot: Discord.js 14.x
- Web: React 19 + Bun HTTP Server
- Database: PostgreSQL 16+ with Drizzle ORM
- UI: Tailwind CSS v4 + ShadCN/Radix
- Validation: Zod
- Testing: Bun Test
- Container: Docker
Key Files Reference
| Purpose | File |
|---|---|
| Bot entry | bot/index.ts |
| DB schema | shared/db/schema.ts |
| Error classes | shared/lib/errors.ts |
| Config loader | shared/lib/config.ts |
| Environment | shared/lib/env.ts |
| Embed helpers | bot/lib/embeds.ts |
| Command utils | shared/lib/utils.ts |