Skip to content

Latest commit

 

History

History
345 lines (253 loc) · 11.5 KB

File metadata and controls

345 lines (253 loc) · 11.5 KB

AGENTS.md - Coding Agent Guidelines for SplitPro

Build/Lint/Test Commands

Development

pnpm dev           # Start dev server with Turbopack
pnpm d             # Full dev setup (install, docker, migrate, dev)
pnpm dx            # Setup dependencies (install, docker up, migrate dev)
pnpm dx:up         # Start Docker containers
pnpm dx:down       # Stop Docker containers

Worktrees

  • when using git worktrees, store them in the .worktrees directory in the root of the project folder
  • when creating a worktree, copy .env from the root project into the worktree
  • update worktree-local app urls/port to avoid conflicts with the main workspace:
    • set NEXTAUTH_URL and NEXTAUTH_URL_INTERNAL to a unique localhost port
    • run dev server with that same port (for example pnpm dev --port 3174)
  • use a separate postgres instance per worktree (different host port + db name)
    • set unique values in the worktree .env: POSTGRES_PORT, POSTGRES_DB, POSTGRES_CONTAINER_NAME, DATABASE_URL
    • start dedicated db container, for example:
      • docker run -d --name <unique-container-name> -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=strong-password -e POSTGRES_DB=<worktree-db> -p <worktree-port>:5432 ossapps/postgres:17.7-trixie postgres -c shared_preload_libraries=pg_cron -c cron.database_name=<worktree-db> -c cron.timezone=UTC
    • enable pg_cron extension once: psql "postgresql://postgres:strong-password@localhost:<worktree-port>/<worktree-db>" -c "CREATE EXTENSION IF NOT EXISTS pg_cron;"
    • initialize schema and data: pnpm exec prisma migrate reset --force --skip-seed && pnpm db:seed

Database

pnpm db:push       # Push Prisma schema to database
pnpm db:studio     # Open Prisma Studio
pnpm db:dev        # Run Prisma migrations (dev)
pnpm db:seed       # Seed the database
pnpm generate      # Generate Prisma client

Testing

pnpm test                              # Run all tests
pnpm test:watch                        # Run tests in watch mode
pnpm test src/tests/simplify.test.ts   # Run a specific test file
pnpm test -- -t "test name pattern"    # Run tests matching pattern

Linting & Formatting

pnpm lint                    # Run oxlint with type-aware checking
pnpm prettier --write .      # Format all files
pnpm prettier --check .      # Check formatting without changes
pnpm tsgo --noEmit           # Type check (used in pre-commit)

Building

pnpm build         # Build for production

CI Pipeline (runs on PRs)

  1. pnpm prettier --check . - Format check
  2. pnpm lint - Linting
  3. pnpm tsgo --noEmit - Type checking
  4. pnpm test - Unit tests
  5. pnpm build --no-lint - Build verification

Code Style Guidelines

Formatting (Prettier)

  • Semicolons: required
  • Quotes: single quotes
  • Trailing commas: all
  • Print width: 100 characters
  • Tailwind CSS classes are auto-sorted

TypeScript Configuration

  • Strict mode enabled
  • Target: ES2022
  • noUncheckedIndexedAccess: true - handle undefined for index access
  • Path aliases:
    • ~/* maps to ./src/*
    • @/* maps to ./*

Import Organization

  1. External packages first (react, next, third-party libs)
  2. Internal imports using ~/ alias
  3. Relative imports for nearby files
  4. Use type keyword for type-only imports

Environment Variables

  • Server-side vars: Use env.VAR_NAME (available only on server).
  • Client-side vars: Use env.NEXT_PUBLIC_VAR_NAME (exposed to browser, prefixed with NEXT_PUBLIC_).
  • Defaults: Hardcode in src/env.ts using Zod .default() for fallback values.
import { HeartHandshakeIcon, X } from 'lucide-react';
import { useTranslation } from 'next-i18next';
import React, { useCallback } from 'react';

import { type CurrencyCode } from '~/lib/currency';
import { useAddExpenseStore } from '~/store/addStore';
import { api } from '~/utils/api';

import { Button } from '../ui/button';

Naming Conventions

  • Components: PascalCase (AddExpensePage.tsx, Button.tsx)
  • Utility files: camelCase (utils.ts, currency.ts)
  • Test files: *.test.ts pattern
  • Pages: kebab-case for routes (import-splitwise.tsx)
  • Dynamic routes: [paramName].tsx
  • Functions: camelCase (calculateParticipantSplit)
  • Constants: UPPER_SNAKE_CASE (DEFAULT_CATEGORY)
  • Types/Interfaces: PascalCase (AddExpenseState)
  • Hooks: camelCase with use prefix (useAddExpenseStore)

Yoda Conditions

Use literal on the left side for comparisons:

if ('authenticated' === status) { ... }
if (0n === amount) { ... }
if ('' === description) { ... }

BigInt Usage

  • All monetary values are BigInt (cents/smallest unit) to prevent rounding errors
  • Use n suffix for BigInt literals: 0n, 1n, 10000n
  • Use BigMath helper (src/utils/numbers.ts) for arithmetic: abs, sign, min, max, roundDiv
  • Use getCurrencyHelpers({ currency, locale }) for display/parsing

Error Handling

tRPC API errors:

if (!group) {
  throw new TRPCError({ code: 'NOT_FOUND', message: 'Group not found' });
}

Client-side errors:

try {
  await mutation.mutateAsync(data);
} catch (error) {
  toast.error(error instanceof Error ? error.message : 'An unexpected error occurred');
}

Promise chains:

router.push('/path').catch(console.error);

Functions and Iteration

  • Prefer arrow functions over function declarations:

    // Good
    const calculateTotal = (items: Item[]) => items.reduce((sum, item) => sum + item.amount, 0n);
    
    // Avoid
    function calculateTotal(items: Item[]) { ... }
  • Prefer iterator methods over for loops (map, filter, reduce, forEach, find, some, every):

    // Good
    const totals = expenses.filter(e => !e.deletedAt).map(e => e.amount);
    
    // Avoid
    for (const expense of expenses) { ... }
  • Prefer early returns to reduce nesting. Put the shorter/simpler case first:

    // Good
    const processItem = (item: Item) => {
      if (!item.needsProcessing) {
        return item;
      }
    
      // Complex processing logic here...
      return processedItem;
    };
    
    // Avoid
    const processItem = (item: Item) => {
      if (item.needsProcessing) {
        // Complex processing logic here...
        return processedItem;
      } else {
        return item;
      }
    };

React Patterns

  • Functional components with TypeScript interfaces
  • useCallback for memoized callbacks, useMemo for computed values
  • Zustand for global state, tRPC/React Query for server state
  • cn() utility for conditional Tailwind classes
  • Use as const and satisfies for fixed arrays/objects

Test Structure

describe('functionName', () => {
  describe('ScenarioGroup', () => {
    it('should do something specific', () => {
      const result = functionName(input);
      expect(result).toBe(expected);
    });
  });
});

Linting Rules (oxlint)

  • no-unused-vars: warn
  • no-console: warn (allows info, warn, error, debug, trace in JSX/TSX)
  • sort-imports: error
  • Ignored paths: prisma/seed.ts, src/components/ui/*

Tech Stack

  • Framework: Next.js 15 (Pages Router)
  • Database: PostgreSQL + Prisma ORM
  • API: tRPC
  • State: Zustand + React Query
  • Styling: Tailwind CSS v4
  • UI: Radix UI primitives
  • Auth: NextAuth.js
  • Validation: Zod
  • i18n: next-i18next
  • Package Manager: pnpm

Key Architecture Notes

  • Double-entry balance accounting: Every expense creates two balance records (bidirectional)
  • Transaction batching: Use db.$transaction(operations) for expense mutations
  • Split types: EQUAL, PERCENTAGE, SHARE, EXACT, ADJUSTMENT, SETTLEMENT, CURRENCY_CONVERSION
  • Schema typo: firendId (not friendId) in GroupBalance - maintain for consistency

Typescript Migrations

Data migrations too complex for SQL queries run automatically during server startup via src/instrumentation.ts.

Architecture

  • src/migrations/index.ts - Migration runner that checks AppMetadata.schema_version and runs pending migrations
  • src/migrations/{name}.ts - Individual migration files exporting an async function

Adding New Migrations

  1. Create a new file src/migrations/{description}.ts:
import { db } from '~/server/db';

export async function myMigrationFunction(): Promise<void> {
  // Migration logic using the shared db connection
  const records = await db.someModel.findMany({ ... });

  for (const record of records) {
    await db.$transaction(async (tx) => {
      // Atomic operations
    });
  }

  console.log('Migration completed');
}
  1. Register the migration in src/migrations/index.ts by calling it in its respective case block.

Version Tracking

  • Schema version is stored in AppMetadata table with key schema_version
  • Versions are compared lexicographically (semver format: "2.0.0", "2.1.0", etc.)
  • Missing version (pre-2.0.0 databases) is treated as needing all migrations

Key Points

  • Migrations run on every server start but are idempotent (version check)
  • Use the shared db connection from ~/server/db
  • Wrap related operations in db.$transaction() for atomicity
  • Log progress for visibility during deployment

Localization

  • Only create English translation keys when adding features
  • Translations are handled via Weblate by the community
  • Translation files: public/locales/{lang}/*.json

Pre-commit Hooks

Husky runs on commit:

  1. Prettier formatting
  2. oxlint with auto-fix
  3. Prisma format (for .prisma files)
  4. Type checking with tsgo

Override with git commit --no-verify if needed.

Use the ctx7 CLI to fetch current documentation whenever the user asks about a library, framework, SDK, API, CLI tool, or cloud service -- even well-known ones like React, Next.js, Prisma, Express, Tailwind, Django, or Spring Boot. This includes API syntax, configuration, version migration, library-specific debugging, setup instructions, and CLI tool usage. Use even when you think you know the answer -- your training data may not reflect recent changes. Prefer this over web search for library docs.

Do not use for: refactoring, writing scripts from scratch, debugging business logic, code review, or general programming concepts.

Steps

  1. Resolve library: npx ctx7@latest library <name> "<user's question>" — use the official library name with proper punctuation (e.g., "Next.js" not "nextjs", "Customer.io" not "customerio", "Three.js" not "threejs")
  2. Pick the best match (ID format: /org/project) by: exact name match, description relevance, code snippet count, source reputation (High/Medium preferred), and benchmark score (higher is better). If results don't look right, try alternate names or queries (e.g., "next.js" not "nextjs", or rephrase the question)
  3. Fetch docs: npx ctx7@latest docs <libraryId> "<user's question>"
  4. Answer using the fetched documentation

You MUST call library first to get a valid ID unless the user provides one directly in /org/project format. Use the user's full question as the query -- specific and detailed queries return better results than vague single words. Do not run more than 3 commands per question. Do not include sensitive information (API keys, passwords, credentials) in queries.

For version-specific docs, use /org/project/version from the library output (e.g., /vercel/next.js/v14.3.0).

If a command fails with a quota error, inform the user and suggest npx ctx7@latest login or setting CONTEXT7_API_KEY env var for higher limits. Do not silently fall back to training data.