This file provides context and best practices for Claude Code when working on this project.
Agent Mart is a marketplace directory for Claude Code plugins. It consists of:
- ETL Pipeline (Node.js) - Crawls GitHub for marketplace definitions
- Web Frontend (Next.js) - Displays the plugin directory
agent-mart/
├── src/lib/ # Core utilities (JS, ES modules)
├── src/pipeline/ # 8-step ETL pipeline
├── scripts/ # Build tools and visualizer
├── tests/ # Node.js test runner tests
├── data/ # Pipeline intermediate data and cache
├── web/ # Next.js 16 frontend (TypeScript)
│ ├── src/app/ # App Router pages
│ ├── src/components/
│ ├── src/hooks/
│ └── src/lib/
└── web/public/data/ # Generated JSON output
- Single Responsibility: Each function/component does one thing well
- DRY (Don't Repeat Yourself): Extract repeated logic into shared utilities
- KISS (Keep It Simple): Prefer simple solutions over clever ones
- Early Returns: Exit functions early to reduce nesting
// Good - early return
function processUser(user) {
if (!user) return null;
if (!user.isActive) return null;
return transformUser(user);
}
// Bad - deeply nested
function processUser(user) {
if (user) {
if (user.isActive) {
return transformUser(user);
}
}
return null;
}- Variables/Functions: camelCase, descriptive verbs for functions
- Constants: SCREAMING_SNAKE_CASE for true constants
- Classes/Components: PascalCase
- Files: Match export name (PascalCase for components, camelCase for utilities)
- Boolean variables: Use
is,has,shouldprefixes
// Good
const isLoading = true;
const hasPermission = false;
const userCount = 42;
function fetchUserData() {}
// Bad
const loading = true;
const permission = false;
const users = 42; // Ambiguous - is it count or array?
function userData() {} // Is this a getter or fetcher?- Always throw
Errorobjects, never strings or other types - Handle errors at appropriate boundaries
- Provide meaningful error messages
- Log errors with context
// Good
throw new Error(`Failed to fetch user ${userId}: ${response.status}`);
// Bad
throw 'fetch failed';
throw { error: true };- Code should be self-documenting; avoid redundant comments
- Use comments to explain "why", not "what"
- JSDoc for public APIs and complex functions
- TODO comments should include context
// Good - explains why
// GraphQL has a 100-item limit per query, so we batch requests
const BATCH_SIZE = 100;
// Bad - explains what (obvious from code)
// Set batch size to 100
const BATCH_SIZE = 100;- ES modules (
import/export) over CommonJS constby default,letwhen reassignment needed, nevervar- Arrow functions for callbacks and short functions
- Template literals for string interpolation
- Destructuring for cleaner code
- Optional chaining (
?.) and nullish coalescing (??)
// Good
const { name, email } = user;
const displayName = user?.profile?.displayName ?? 'Anonymous';
const message = `Hello, ${name}!`;
// Bad
const name = user.name;
const email = user.email;
const displayName = user && user.profile && user.profile.displayName || 'Anonymous';
const message = 'Hello, ' + name + '!';- Prefer async/await over
.then()chains - Use
Promise.all()for parallel operations - Always handle promise rejections
- Avoid mixing async/await with callbacks
// Good
async function fetchData() {
const [users, posts] = await Promise.all([
fetchUsers(),
fetchPosts()
]);
return { users, posts };
}
// Bad
function fetchData() {
return fetchUsers().then(users => {
return fetchPosts().then(posts => {
return { users, posts };
});
});
}- Use
.map(),.filter(),.reduce()over manual loops - Chain methods for readability
- Avoid mutating arrays; create new ones
// Good
const activeUserNames = users
.filter(user => user.isActive)
.map(user => user.name);
// Bad
const activeUserNames = [];
for (let i = 0; i < users.length; i++) {
if (users[i].isActive) {
activeUserNames.push(users[i].name);
}
}- Avoid
any; useunknownand narrow types when needed - Define interfaces for all data structures
- Use union types for constrained values
- Leverage type inference; don't over-annotate
// Good
interface User {
id: string;
name: string;
role: 'admin' | 'user' | 'guest';
}
function processData(data: unknown): User {
if (!isUser(data)) {
throw new Error('Invalid user data');
}
return data;
}
// Bad
function processData(data: any): any {
return data;
}- Use
import typefor type-only imports - Keep types in dedicated files (
types.ts) - Export types from barrel files
// Good
import type { User, Post } from './types';
import { fetchUser } from './api';
// Bad
import { User, Post, fetchUser } from './api';- Prefer
undefinedovernullfor optional values - Use optional properties (
prop?:) over| undefined - Handle nullish values explicitly
// Good
interface Config {
timeout?: number; // Optional
retries: number; // Required
}
// Bad
interface Config {
timeout: number | undefined;
retries: number;
}- Prefer functional components with hooks
- Keep components small and focused
- Extract logic into custom hooks
- Use composition over prop drilling
// Good - composed, focused
function UserCard({ user }: { user: User }) {
return (
<Card>
<UserAvatar user={user} />
<UserInfo user={user} />
</Card>
);
}
// Bad - monolithic
function UserCard({ user }: { user: User }) {
return (
<div className="card">
<img src={user.avatar} className="..." />
<div className="info">
<h3>{user.name}</h3>
<p>{user.email}</p>
{/* ... 50 more lines */}
</div>
</div>
);
}- Use TypeScript interfaces for props
- Destructure props in function signature
- Provide sensible defaults
- Keep prop count reasonable (< 7)
// Good
interface ButtonProps {
label: string;
onClick: () => void;
variant?: 'primary' | 'secondary';
disabled?: boolean;
}
function Button({
label,
onClick,
variant = 'primary',
disabled = false
}: ButtonProps) {
// ...
}- Follow the Rules of Hooks (top level, React functions only)
- Use dependency arrays correctly in
useEffect - Extract complex logic into custom hooks
- Memoize expensive computations with
useMemo - Memoize callbacks with
useCallbackwhen passed to children
// Good
const sortedItems = useMemo(
() => items.sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
const handleClick = useCallback(() => {
onSelect(item.id);
}, [item.id, onSelect]);
// Bad - recalculates on every render
const sortedItems = items.sort((a, b) => a.name.localeCompare(b.name));- Keep state as local as possible
- Lift state only when needed
- Use URL state for shareable UI state (filters, pagination)
- Avoid unnecessary re-renders
// Good - state close to where it's used
function SearchInput() {
const [query, setQuery] = useState('');
// ...
}
// Bad - state lifted unnecessarily
function App() {
const [searchQuery, setSearchQuery] = useState('');
return <SearchInput query={searchQuery} setQuery={setSearchQuery} />;
}- Name handlers with
handleprefix - Use
onprefix for callback props - Prevent default when needed
- Stop propagation intentionally
interface ButtonProps {
onClick: () => void; // Callback prop with "on" prefix
}
function Form() {
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// ...
};
return <form onSubmit={handleSubmit}>...</form>;
}- Use Server Components by default
- Add
"use client"only when needed (interactivity, hooks) - Colocate related files (page, loading, error)
- Use route groups for organization
- Fetch data in Server Components when possible
- Use
Suspenseboundaries for loading states - Handle errors with error boundaries
- Cache and dedupe requests
- Use
next/imagefor optimized images - Use
next/linkfor client-side navigation - Implement loading and error states
- Consider static generation for public pages
- Arrange-Act-Assert pattern
- One assertion per test when practical
- Descriptive test names that explain behavior
- Test behavior, not implementation
// Good
describe('calculateTotal', () => {
it('returns sum of item prices', () => {
// Arrange
const items = [{ price: 10 }, { price: 20 }];
// Act
const result = calculateTotal(items);
// Assert
expect(result).toBe(30);
});
it('returns 0 for empty array', () => {
expect(calculateTotal([])).toBe(0);
});
});
// Bad
it('test1', () => {
expect(calculateTotal([{ price: 10 }])).toBe(10);
expect(calculateTotal([{ price: 10 }, { price: 20 }])).toBe(30);
expect(calculateTotal([])).toBe(0);
});- Critical business logic
- Edge cases and error conditions
- User interactions (clicks, input)
- Component rendering with different props
- Implementation details
- Third-party libraries
- Trivial code (simple getters/setters)
- Never commit secrets (
.env, tokens, keys) - Validate and sanitize all external input
- Protect against path traversal in file operations
- Escape output to prevent XSS
- Use parameterized queries (no string concatenation)
npm run pipeline # Run ETL pipeline
npm run pipeline:dev # Run ETL with visualizer
npm run lint # Check linting
npm run lint:fix # Auto-fix issues
npm test # Run ETL tests
npm run test:unit # Run unit tests only
npm run test:data # Run data quality tests only
npm run test:web # Run frontend tests
npm run test:all # Run all testsFollow conventional commits:
feat:new featurefix:bug fixdocs:documentationrefactor:code change without feature/fixtest:adding/updating testschore:maintenance tasks
- Use Tailwind utilities exclusively - No raw CSS for component styling
- CSS variables are defined in
globals.cssand exposed via@theme inline - Use design system colors:
text-foreground,bg-background,border-border, etc. - For custom values, use arbitrary value syntax:
w-[4px],text-[10px]
// Good - Tailwind utilities with design system colors
<div className="bg-card border border-border rounded-lg p-4 hover:border-border-hover">
// Bad - Raw CSS or hardcoded colors
<div style={{ backgroundColor: '#fff', border: '1px solid #e5e5e5' }}>- Always include dark mode variants for colors
- Use semantic color names that adapt:
bg-background,text-foreground - For hardcoded colors, add
dark:variants:bg-white dark:bg-gray-800 - Test both light and dark modes when making UI changes
// Good - adapts to theme
<button className="bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-gray-100">
// Bad - only works in light mode
<button className="bg-gray-200 text-gray-900">Key CSS variables (defined in globals.css):
--background,--foreground- Base page colors--card,--card-hover- Card backgrounds--border,--border-hover- Border colors--accent,--accent-foreground- Accent/brand color (mint green)--foreground-muted,--foreground-secondary- Text hierarchy
- Use
"use client"directive only when needed (hooks, interactivity) - Prefer composition over large monolithic components
- Use Radix UI primitives for accessible dropdowns, popovers, etc.
- Handle loading and error states explicitly
- Mobile-first approach with Tailwind breakpoints
- Use
sm:,md:,lg:prefixes for responsive styles - Test at common breakpoints: 375px (mobile), 768px (tablet), 1024px+ (desktop)
| Directory | Convention | Example |
|---|---|---|
src/pipeline/ |
NN-step-name.js |
01-discover.js |
src/lib/ |
camelCase.js |
cache.js |
web/src/components/ |
PascalCase.tsx |
MarketplaceCard.tsx |
web/src/hooks/ |
useCamelCase.ts |
useFetch.ts |
web/src/lib/ |
camelCase.ts |
types.ts |
tests/ |
feature.test.js |
categorizer.test.js |