Skip to content

Commit 6f7fd1e

Browse files
authored
feat: add gemini cli, claude code, codex, jules (#66)
* feat: add gemini cli, claude code, codex, jules * chore: propagate cf secrets
1 parent 154385f commit 6f7fd1e

8 files changed

Lines changed: 272 additions & 59 deletions

File tree

.github/workflows/deploy-app.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ jobs:
7575
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
7676
SUPABASE_PUBLIC_KEY: ${{ secrets.SUPABASE_PUBLIC_KEY }}
7777
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
78+
CF_CAPTCHA_SITE_KEY: ${{ secrets.CF_CAPTCHA_SITE_KEY }}
79+
CF_CAPTCHA_SECRET_KEY: ${{ secrets.CF_CAPTCHA_SECRET_KEY }}
7880
run: npm run build # This should create the dist/ directory
7981

8082
- name: Deploy to Cloudflare Pages

.github/workflows/pull-request.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ jobs:
8282
echo "E2E_USERNAME_ID=${{ secrets.E2E_USERNAME_ID }}" >> .env.integration
8383
echo "E2E_USERNAME=${{ secrets.E2E_USERNAME }}" >> .env.integration
8484
echo "E2E_PASSWORD=${{ secrets.E2E_PASSWORD }}" >> .env.integration
85+
echo "CF_CAPTCHA_SITE_KEY=${{ secrets.CF_CAPTCHA_SITE_KEY }}" >> .env.integration
86+
echo "CF_CAPTCHA_SECRET_KEY=${{ secrets.CF_CAPTCHA_SECRET_KEY }}" >> .env.integration
8587
8688
- name: Run E2E tests
8789
run: npm run test:e2e

CLAUDE.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Common Development Commands
6+
7+
### Development Server
8+
- `npm run dev` - Start development server on port 3000 (local mode)
9+
- `npm run dev:e2e` - Start development server in integration mode for E2E testing
10+
11+
### Building and Deployment
12+
- `npm run build` - Build the Astro application for production
13+
- `npm run preview` - Preview the built application locally
14+
15+
### Code Quality
16+
- `npm run lint` - Lint and fix TypeScript/Astro files
17+
- `npm run lint:check` - Check linting without fixing
18+
- `npm run format` - Format code with Prettier
19+
- `npm run format:check` - Check formatting without fixing
20+
21+
### Testing
22+
- `npm run test` - Run unit tests with Vitest
23+
- `npm run test:watch` - Run tests in watch mode
24+
- `npm run test:ui` - Run tests with UI interface
25+
- `npm run test:coverage` - Generate test coverage report
26+
- `npm run test:e2e` - Run end-to-end tests with Playwright
27+
- `npm run test:e2e:ui` - Run E2E tests with UI
28+
- `npm run test:e2e:codegen` - Generate test code with Playwright
29+
30+
### Special Scripts
31+
- `npm run generate-rules` - Generate rules JSON from TypeScript definitions
32+
33+
## Architecture Overview
34+
35+
### Technology Stack
36+
- **Framework**: Astro 5 with React 18.3 integration
37+
- **Styling**: Tailwind CSS 4
38+
- **State Management**: Zustand for client-side state
39+
- **Database**: Supabase (PostgreSQL with real-time features)
40+
- **Testing**: Vitest for unit tests, Playwright for E2E tests
41+
- **Authentication**: Supabase Auth with email/password and password reset
42+
43+
### Project Structure
44+
45+
#### Core Application (`src/`)
46+
- `pages/` - Astro pages with API routes under `api/`
47+
- `components/` - React components organized by feature
48+
- `data/` - Static data including AI rules definitions in `rules/` subdirectory
49+
- `services/` - Business logic services, notably `RulesBuilderService`
50+
- `store/` - Zustand stores for state management
51+
- `hooks/` - Custom React hooks
52+
53+
#### Key Components Architecture
54+
- **Rules System**: Rules are organized by technology stacks (frontend, backend, database, etc.) and stored in `src/data/rules/`
55+
- **Rules Builder Service**: Core service in `src/services/rules-builder/` that generates markdown content using strategy pattern (single-file vs multi-file output)
56+
- **Collections System**: User can save and manage rule collections via `collectionsStore`
57+
- **Feature Flags**: Environment-based feature toggling system in `src/features/featureFlags.ts`
58+
59+
#### MCP Server (`mcp-server/`)
60+
Standalone Cloudflare Worker implementing Model Context Protocol for programmatic access to AI rules. Provides tools:
61+
- `listAvailableRules` - Get available rule categories
62+
- `getRuleContent` - Fetch specific rule content
63+
64+
### State Management Pattern
65+
The application uses Zustand with multiple specialized stores:
66+
- `techStackStore` - Manages selected libraries and tech stack
67+
- `collectionsStore` - Handles saved rule collections with dirty state tracking
68+
- `authStore` - Authentication state management
69+
- `projectStore` - Project metadata (name, description)
70+
71+
### Environment Configuration
72+
- Uses Astro's environment schema for type-safe environment variables
73+
- Supports three environments: `local`, `integration`, `prod`
74+
- Feature flags control functionality per environment
75+
- Requires `.env.local` with Supabase credentials and Cloudflare Turnstile keys
76+
77+
### Database Integration
78+
- Supabase integration with TypeScript types in `src/db/database.types.ts`
79+
- Collections are stored in Supabase with user association
80+
- Real-time capabilities available but not currently utilized
81+
82+
### Testing Strategy
83+
- Unit tests use Vitest with React Testing Library and JSDOM
84+
- E2E tests use Playwright with Page Object Model pattern
85+
- Test files located in `tests/` for unit tests and `e2e/` for E2E tests
86+
- All tests run in CI/CD pipeline
87+
88+
### Rules Content System
89+
Rules are defined as TypeScript objects and exported from category-specific files in `src/data/rules/`. The system supports:
90+
- Categorization by technology layers (frontend, backend, database, etc.)
91+
- Library-specific rules with placeholder replacement
92+
- Multi-file vs single-file output strategies
93+
- Markdown generation with project context
94+
95+
### Development Workflow
96+
1. Rules contributions go in `src/data/rules/` with corresponding translations in `src/i18n/translations.ts`
97+
2. Use feature flags to control new functionality rollout
98+
3. Collections allow users to save and share rule combinations
99+
4. The MCP server enables programmatic access for AI assistants

src/components/rule-preview/RulePreviewTopbar.tsx

Lines changed: 96 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,111 @@
1-
import React from 'react';
1+
import React, { useState, useRef, useEffect } from 'react';
2+
import { ChevronDown, Check } from 'lucide-react';
23
import { useProjectStore } from '../../store/projectStore';
34
import { RulesPath } from './RulesPath';
45
import { RulesPreviewActions } from './RulesPreviewActions';
56
import type { RulesContent } from '../../services/rules-builder/RulesBuilderTypes.ts';
6-
import { type AIEnvironment, AIEnvironmentName } from '../../data/ai-environments.ts';
7+
import {
8+
type AIEnvironment,
9+
AIEnvironmentName,
10+
aiEnvironmentConfig,
11+
} from '../../data/ai-environments.ts';
712
import RulesPreviewCopyDownloadActions from './RulesPreviewCopyDownloadActions.tsx';
813

914
interface RulePreviewTopbarProps {
1015
rulesContent: RulesContent[];
1116
}
1217

13-
interface EnvButtonProps {
14-
environment: AIEnvironment;
18+
interface EnvironmentDropdownProps {
1519
selectedEnvironment: AIEnvironment;
16-
isMultiFileEnvironment: boolean;
1720
onSetSelectedEnvironment: (environment: AIEnvironment) => void;
1821
}
1922

20-
const EnvButton: React.FC<EnvButtonProps> = ({
21-
environment,
23+
const EnvironmentDropdown: React.FC<EnvironmentDropdownProps> = ({
2224
selectedEnvironment,
2325
onSetSelectedEnvironment,
2426
}) => {
27+
const [isOpen, setIsOpen] = useState(false);
28+
const dropdownRef = useRef<HTMLDivElement>(null);
29+
30+
// Close dropdown when clicking outside
31+
useEffect(() => {
32+
const handleClickOutside = (event: MouseEvent) => {
33+
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
34+
setIsOpen(false);
35+
}
36+
};
37+
38+
document.addEventListener('mousedown', handleClickOutside);
39+
return () => document.removeEventListener('mousedown', handleClickOutside);
40+
}, []);
41+
42+
// Handle keyboard navigation
43+
const handleKeyDown = (event: React.KeyboardEvent) => {
44+
if (event.key === 'Escape') {
45+
setIsOpen(false);
46+
} else if (event.key === 'Enter' || event.key === ' ') {
47+
event.preventDefault();
48+
setIsOpen(!isOpen);
49+
}
50+
};
51+
52+
const handleOptionSelect = (environment: AIEnvironment) => {
53+
onSetSelectedEnvironment(environment);
54+
setIsOpen(false);
55+
};
56+
57+
const selectedConfig = aiEnvironmentConfig[selectedEnvironment];
58+
2559
return (
26-
<button
27-
onClick={() => onSetSelectedEnvironment(environment)}
28-
className={`px-3 py-1 text-xs rounded-md ${
29-
selectedEnvironment === environment
30-
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
31-
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
32-
}`}
33-
>
34-
{`${environment[0].toUpperCase()}${environment.slice(1)}`}
35-
</button>
60+
<div className="relative" ref={dropdownRef}>
61+
{/* Dropdown trigger button */}
62+
<button
63+
onClick={() => setIsOpen(!isOpen)}
64+
onKeyDown={handleKeyDown}
65+
className="flex items-center justify-between w-full sm:w-auto min-w-[180px] px-3 py-2 text-sm bg-gray-700 text-white rounded-md hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:ring-offset-gray-800"
66+
aria-haspopup="listbox"
67+
aria-expanded={isOpen}
68+
aria-label="Select AI environment"
69+
>
70+
<span className="truncate">{selectedConfig?.displayName || 'Select Environment'}</span>
71+
<ChevronDown
72+
className={`ml-2 h-4 w-4 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
73+
/>
74+
</button>
75+
76+
{/* Dropdown menu */}
77+
{isOpen && (
78+
<div className="absolute top-full left-0 mt-1 w-full sm:w-64 bg-gray-800 border border-gray-600 rounded-md shadow-lg z-50 max-h-64 overflow-y-auto">
79+
<ul role="listbox" className="py-1">
80+
{Object.values(AIEnvironmentName).map((environment) => {
81+
const config = aiEnvironmentConfig[environment];
82+
if (!config) return null;
83+
84+
const isSelected = selectedEnvironment === environment;
85+
86+
return (
87+
<li key={environment} role="option" aria-selected={isSelected}>
88+
<button
89+
onClick={() => handleOptionSelect(environment)}
90+
className={`w-full px-3 py-2 text-left text-sm flex items-center justify-between hover:bg-gray-700 focus:outline-none focus:bg-gray-700 ${
91+
isSelected ? 'bg-gray-700 text-white' : 'text-gray-300'
92+
}`}
93+
>
94+
<span className="truncate">{config.displayName}</span>
95+
{isSelected && <Check className="h-4 w-4 text-indigo-400 ml-2 flex-shrink-0" />}
96+
</button>
97+
</li>
98+
);
99+
})}
100+
</ul>
101+
</div>
102+
)}
103+
</div>
36104
);
37105
};
38106

39107
export const RulePreviewTopbar: React.FC<RulePreviewTopbarProps> = ({ rulesContent }) => {
40-
const { selectedEnvironment, setSelectedEnvironment, isMultiFileEnvironment, isHydrated } =
41-
useProjectStore();
108+
const { selectedEnvironment, setSelectedEnvironment, isHydrated } = useProjectStore();
42109

43110
// If state hasn't been hydrated from storage yet, don't render the selector
44111
// This prevents the "blinking" effect when loading persisted state
@@ -47,16 +114,11 @@ export const RulePreviewTopbar: React.FC<RulePreviewTopbarProps> = ({ rulesConte
47114
<div className="p-2 bg-gray-800 rounded-lg">
48115
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center space-y-3 sm:space-y-0 opacity-0">
49116
{/* Invisible placeholder content with the same structure to prevent layout shift */}
50-
<div className="flex items-center space-x-2">
51-
<div className="flex space-x-1">
52-
<div className="px-3 py-1 text-xs rounded-md bg-gray-700"></div>
53-
<div className="px-3 py-1 text-xs rounded-md bg-gray-700"></div>
54-
<div className="px-3 py-1 text-xs rounded-md bg-gray-700"></div>
55-
</div>
117+
<div className="flex flex-col sm:flex-row sm:items-center space-y-2 sm:space-y-0 sm:space-x-4 w-full sm:w-auto">
118+
<div className="min-w-[180px] px-3 py-2 text-sm bg-gray-700 rounded-md"></div>
119+
<div className="text-sm text-gray-400 w-32 h-5"></div>
56120
</div>
57121
<div className="flex items-center space-x-2 w-full sm:w-auto">
58-
<div className="text-sm flex-1 sm:flex-none"></div>
59-
<div className="px-3 py-1 rounded-md"></div>
60122
<div className="px-3 py-1 rounded-md"></div>
61123
<div className="px-3 py-1 rounded-md"></div>
62124
</div>
@@ -68,20 +130,13 @@ export const RulePreviewTopbar: React.FC<RulePreviewTopbarProps> = ({ rulesConte
68130
return (
69131
<div className="p-2 bg-gray-800 rounded-lg">
70132
<div className="flex flex-col sm:flex-row justify-between items-start space-y-3 sm:space-y-0">
71-
{/* Left side: Environment selector buttons and path */}
72-
<div className="flex flex-col space-y-2 w-full sm:w-auto">
73-
{/* Environment selector buttons - make them wrap on small screens */}
74-
<div className="flex flex-wrap gap-1">
75-
{Object.values(AIEnvironmentName).map((environment) => (
76-
<EnvButton
77-
key={'button-' + environment}
78-
environment={environment}
79-
selectedEnvironment={selectedEnvironment}
80-
isMultiFileEnvironment={isMultiFileEnvironment}
81-
onSetSelectedEnvironment={setSelectedEnvironment}
82-
/>
83-
))}
84-
</div>
133+
{/* Left side: Environment selector dropdown and path */}
134+
<div className="flex flex-col sm:flex-row sm:items-center space-y-2 sm:space-y-0 sm:space-x-4 w-full sm:w-auto">
135+
{/* Environment selector dropdown */}
136+
<EnvironmentDropdown
137+
selectedEnvironment={selectedEnvironment}
138+
onSetSelectedEnvironment={setSelectedEnvironment}
139+
/>
85140

86141
{/* Path display */}
87142
<RulesPath />

src/components/rule-preview/RulesPath.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ export const RulesPath: React.FC = () => {
66
const { selectedEnvironment } = useProjectStore();
77

88
// Get the appropriate file path based on the selected format
9-
const getFilePath = (): string => aiEnvironmentConfig[selectedEnvironment].filePath;
9+
const getFilePath = (): string => {
10+
const config = aiEnvironmentConfig[selectedEnvironment];
11+
return config?.filePath || 'Unknown path';
12+
};
1013

1114
return (
1215
<div className="text-sm text-gray-400 w-full break-all">

src/components/rule-preview/RulesPreviewActions.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,18 @@ import Tooltip from '../ui/Tooltip.tsx';
77
export const RulesPreviewActions: React.FC<unknown> = () => {
88
const { selectedEnvironment } = useProjectStore();
99

10+
const config = aiEnvironmentConfig[selectedEnvironment];
11+
if (!config) {
12+
return null;
13+
}
14+
1015
return (
11-
<Tooltip
12-
content={`Open documentation for ${selectedEnvironment.charAt(0).toUpperCase() + selectedEnvironment.slice(1)}`}
13-
position="bottom"
14-
>
16+
<Tooltip content={`Open documentation for ${config.displayName}`} position="bottom">
1517
<a
16-
href={aiEnvironmentConfig[selectedEnvironment].docsUrl}
18+
href={config.docsUrl}
1719
target="_blank"
1820
className="px-3 py-1 bg-purple-700 text-white rounded-md hover:bg-purple-600 flex items-center text-sm opacity-40 hover:opacity-100 cursor-pointer"
19-
aria-label={`Open documentation for ${selectedEnvironment}`}
21+
aria-label={`Open documentation for ${config.displayName}`}
2022
>
2123
<ExternalLink className="h-4 w-4" />
2224
</a>

0 commit comments

Comments
 (0)