From e4096e9531613ed23a8d73448e8ef3dfaa74427f Mon Sep 17 00:00:00 2001 From: psmyrdek Date: Sat, 26 Jul 2025 09:31:17 +0200 Subject: [PATCH 1/2] feat: add gemini cli, claude code, codex, jules --- CLAUDE.md | 99 +++++++++++++ .../rule-preview/RulePreviewTopbar.tsx | 137 ++++++++++++------ src/components/rule-preview/RulesPath.tsx | 5 +- .../rule-preview/RulesPreviewActions.tsx | 14 +- src/data/ai-environments.ts | 51 ++++++- src/store/projectStore.ts | 21 ++- 6 files changed, 268 insertions(+), 59 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c6b8f0e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,99 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Common Development Commands + +### Development Server +- `npm run dev` - Start development server on port 3000 (local mode) +- `npm run dev:e2e` - Start development server in integration mode for E2E testing + +### Building and Deployment +- `npm run build` - Build the Astro application for production +- `npm run preview` - Preview the built application locally + +### Code Quality +- `npm run lint` - Lint and fix TypeScript/Astro files +- `npm run lint:check` - Check linting without fixing +- `npm run format` - Format code with Prettier +- `npm run format:check` - Check formatting without fixing + +### Testing +- `npm run test` - Run unit tests with Vitest +- `npm run test:watch` - Run tests in watch mode +- `npm run test:ui` - Run tests with UI interface +- `npm run test:coverage` - Generate test coverage report +- `npm run test:e2e` - Run end-to-end tests with Playwright +- `npm run test:e2e:ui` - Run E2E tests with UI +- `npm run test:e2e:codegen` - Generate test code with Playwright + +### Special Scripts +- `npm run generate-rules` - Generate rules JSON from TypeScript definitions + +## Architecture Overview + +### Technology Stack +- **Framework**: Astro 5 with React 18.3 integration +- **Styling**: Tailwind CSS 4 +- **State Management**: Zustand for client-side state +- **Database**: Supabase (PostgreSQL with real-time features) +- **Testing**: Vitest for unit tests, Playwright for E2E tests +- **Authentication**: Supabase Auth with email/password and password reset + +### Project Structure + +#### Core Application (`src/`) +- `pages/` - Astro pages with API routes under `api/` +- `components/` - React components organized by feature +- `data/` - Static data including AI rules definitions in `rules/` subdirectory +- `services/` - Business logic services, notably `RulesBuilderService` +- `store/` - Zustand stores for state management +- `hooks/` - Custom React hooks + +#### Key Components Architecture +- **Rules System**: Rules are organized by technology stacks (frontend, backend, database, etc.) and stored in `src/data/rules/` +- **Rules Builder Service**: Core service in `src/services/rules-builder/` that generates markdown content using strategy pattern (single-file vs multi-file output) +- **Collections System**: User can save and manage rule collections via `collectionsStore` +- **Feature Flags**: Environment-based feature toggling system in `src/features/featureFlags.ts` + +#### MCP Server (`mcp-server/`) +Standalone Cloudflare Worker implementing Model Context Protocol for programmatic access to AI rules. Provides tools: +- `listAvailableRules` - Get available rule categories +- `getRuleContent` - Fetch specific rule content + +### State Management Pattern +The application uses Zustand with multiple specialized stores: +- `techStackStore` - Manages selected libraries and tech stack +- `collectionsStore` - Handles saved rule collections with dirty state tracking +- `authStore` - Authentication state management +- `projectStore` - Project metadata (name, description) + +### Environment Configuration +- Uses Astro's environment schema for type-safe environment variables +- Supports three environments: `local`, `integration`, `prod` +- Feature flags control functionality per environment +- Requires `.env.local` with Supabase credentials and Cloudflare Turnstile keys + +### Database Integration +- Supabase integration with TypeScript types in `src/db/database.types.ts` +- Collections are stored in Supabase with user association +- Real-time capabilities available but not currently utilized + +### Testing Strategy +- Unit tests use Vitest with React Testing Library and JSDOM +- E2E tests use Playwright with Page Object Model pattern +- Test files located in `tests/` for unit tests and `e2e/` for E2E tests +- All tests run in CI/CD pipeline + +### Rules Content System +Rules are defined as TypeScript objects and exported from category-specific files in `src/data/rules/`. The system supports: +- Categorization by technology layers (frontend, backend, database, etc.) +- Library-specific rules with placeholder replacement +- Multi-file vs single-file output strategies +- Markdown generation with project context + +### Development Workflow +1. Rules contributions go in `src/data/rules/` with corresponding translations in `src/i18n/translations.ts` +2. Use feature flags to control new functionality rollout +3. Collections allow users to save and share rule combinations +4. The MCP server enables programmatic access for AI assistants \ No newline at end of file diff --git a/src/components/rule-preview/RulePreviewTopbar.tsx b/src/components/rule-preview/RulePreviewTopbar.tsx index 2ff1b8d..befc87f 100644 --- a/src/components/rule-preview/RulePreviewTopbar.tsx +++ b/src/components/rule-preview/RulePreviewTopbar.tsx @@ -1,44 +1,111 @@ -import React from 'react'; +import React, { useState, useRef, useEffect } from 'react'; +import { ChevronDown, Check } from 'lucide-react'; import { useProjectStore } from '../../store/projectStore'; import { RulesPath } from './RulesPath'; import { RulesPreviewActions } from './RulesPreviewActions'; import type { RulesContent } from '../../services/rules-builder/RulesBuilderTypes.ts'; -import { type AIEnvironment, AIEnvironmentName } from '../../data/ai-environments.ts'; +import { + type AIEnvironment, + AIEnvironmentName, + aiEnvironmentConfig, +} from '../../data/ai-environments.ts'; import RulesPreviewCopyDownloadActions from './RulesPreviewCopyDownloadActions.tsx'; interface RulePreviewTopbarProps { rulesContent: RulesContent[]; } -interface EnvButtonProps { - environment: AIEnvironment; +interface EnvironmentDropdownProps { selectedEnvironment: AIEnvironment; - isMultiFileEnvironment: boolean; onSetSelectedEnvironment: (environment: AIEnvironment) => void; } -const EnvButton: React.FC = ({ - environment, +const EnvironmentDropdown: React.FC = ({ selectedEnvironment, onSetSelectedEnvironment, }) => { + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + + // Close dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + // Handle keyboard navigation + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Escape') { + setIsOpen(false); + } else if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + setIsOpen(!isOpen); + } + }; + + const handleOptionSelect = (environment: AIEnvironment) => { + onSetSelectedEnvironment(environment); + setIsOpen(false); + }; + + const selectedConfig = aiEnvironmentConfig[selectedEnvironment]; + return ( - +
+ {/* Dropdown trigger button */} + + + {/* Dropdown menu */} + {isOpen && ( +
+
    + {Object.values(AIEnvironmentName).map((environment) => { + const config = aiEnvironmentConfig[environment]; + if (!config) return null; + + const isSelected = selectedEnvironment === environment; + + return ( +
  • + +
  • + ); + })} +
+
+ )} +
); }; export const RulePreviewTopbar: React.FC = ({ rulesContent }) => { - const { selectedEnvironment, setSelectedEnvironment, isMultiFileEnvironment, isHydrated } = - useProjectStore(); + const { selectedEnvironment, setSelectedEnvironment, isHydrated } = useProjectStore(); // If state hasn't been hydrated from storage yet, don't render the selector // This prevents the "blinking" effect when loading persisted state @@ -47,16 +114,11 @@ export const RulePreviewTopbar: React.FC = ({ rulesConte
{/* Invisible placeholder content with the same structure to prevent layout shift */} -
-
-
-
-
-
+
+
+
-
-
@@ -68,20 +130,13 @@ export const RulePreviewTopbar: React.FC = ({ rulesConte return (
- {/* Left side: Environment selector buttons and path */} -
- {/* Environment selector buttons - make them wrap on small screens */} -
- {Object.values(AIEnvironmentName).map((environment) => ( - - ))} -
+ {/* Left side: Environment selector dropdown and path */} +
+ {/* Environment selector dropdown */} + {/* Path display */} diff --git a/src/components/rule-preview/RulesPath.tsx b/src/components/rule-preview/RulesPath.tsx index 74fb524..96d8c5d 100644 --- a/src/components/rule-preview/RulesPath.tsx +++ b/src/components/rule-preview/RulesPath.tsx @@ -6,7 +6,10 @@ export const RulesPath: React.FC = () => { const { selectedEnvironment } = useProjectStore(); // Get the appropriate file path based on the selected format - const getFilePath = (): string => aiEnvironmentConfig[selectedEnvironment].filePath; + const getFilePath = (): string => { + const config = aiEnvironmentConfig[selectedEnvironment]; + return config?.filePath || 'Unknown path'; + }; return (
diff --git a/src/components/rule-preview/RulesPreviewActions.tsx b/src/components/rule-preview/RulesPreviewActions.tsx index ccdbd06..4204240 100644 --- a/src/components/rule-preview/RulesPreviewActions.tsx +++ b/src/components/rule-preview/RulesPreviewActions.tsx @@ -7,16 +7,18 @@ import Tooltip from '../ui/Tooltip.tsx'; export const RulesPreviewActions: React.FC = () => { const { selectedEnvironment } = useProjectStore(); + const config = aiEnvironmentConfig[selectedEnvironment]; + if (!config) { + return null; + } + return ( - + diff --git a/src/data/ai-environments.ts b/src/data/ai-environments.ts index 6c25e7e..5acb0bf 100644 --- a/src/data/ai-environments.ts +++ b/src/data/ai-environments.ts @@ -1,5 +1,5 @@ export enum AIEnvironmentName { - GitHub = 'github', + GitHubCopilot = 'githubcopilot', Cursor = 'cursor', Windsurf = 'windsurf', Aider = 'aider', @@ -7,6 +7,10 @@ export enum AIEnvironmentName { Junie = 'junie', RooCode = 'roocode', Zed = 'zed', + ClaudeCode = 'claudecode', + GeminiCLI = 'geminicli', + OpenAICodex = 'openaicodex', + GoogleJules = 'googlejules', } // Define the AI environment types for easier maintenance @@ -14,44 +18,83 @@ export type AIEnvironment = `${AIEnvironmentName}`; type AIEnvironmentConfig = { [key in AIEnvironmentName]: { + displayName: string; filePath: string; docsUrl: string; }; }; +// Multi-file environments that generate separate files for each rule category +export const multiFileEnvironments: ReadonlySet = new Set([ + AIEnvironmentName.Cline, + AIEnvironmentName.Cursor, + AIEnvironmentName.Windsurf, +]); + +// Default environment to use when initializing the application +export const initialEnvironment: Readonly = AIEnvironmentName.GitHubCopilot; + export const aiEnvironmentConfig: AIEnvironmentConfig = { - github: { + githubcopilot: { + displayName: 'GitHub Copilot', filePath: '.github/copilot-instructions.md', docsUrl: 'https://docs.github.com/en/copilot/customizing-copilot/adding-repository-custom-instructions-for-github-copilot', }, cursor: { + displayName: 'Cursor', filePath: '.cursor/rules/{rule}.mdc', docsUrl: 'https://docs.cursor.com/context/rules-for-ai', }, windsurf: { - filePath: '.windsurfrules', - docsUrl: 'https://docs.codeium.com/windsurf/memories#windsurfrules', + displayName: 'Windsurf', + filePath: '.windsurf/rules/{rule}.mdc', + docsUrl: 'https://docs.windsurf.com/windsurf/cascade/memories#rules', }, cline: { + displayName: 'Cline', filePath: '.clinerules/{rule}.md', docsUrl: 'https://docs.cline.bot/improving-your-prompting-skills/prompting#clinerules-file', }, aider: { + displayName: 'Aider', filePath: 'CONVENTIONS.md', docsUrl: 'https://aider.chat/docs/usage/conventions.html', }, junie: { + displayName: 'Junie', filePath: '.junie/guidelines.md', docsUrl: 'https://www.jetbrains.com/guide/ai/article/junie/intellij-idea/', }, roocode: { + displayName: 'Roo Code', filePath: '.roo/rules/{rule}.md', docsUrl: 'https://docs.roocode.com/features/custom-instructions?_highlight=rules#rules-about-rules-files', }, zed: { + displayName: 'Zed', filePath: '.rules', docsUrl: 'https://zed.dev/docs/ai/rules', }, + claudecode: { + displayName: 'Claude Code', + filePath: 'CLAUDE.md', + docsUrl: 'https://docs.anthropic.com/en/docs/claude-code', + }, + geminicli: { + displayName: 'Gemini CLI', + filePath: 'GEMINI.md', + docsUrl: 'https://ai.google.dev/gemini-api', + }, + openaicodex: { + displayName: 'OpenAI Codex', + filePath: 'AGENTS.md', + docsUrl: 'https://platform.openai.com/docs/guides/code-generation', + }, + googlejules: { + displayName: 'Google Jules', + filePath: 'AGENTS.md', + docsUrl: 'https://ai.google.dev/', + }, }; diff --git a/src/store/projectStore.ts b/src/store/projectStore.ts index f4eb514..7af9c2e 100644 --- a/src/store/projectStore.ts +++ b/src/store/projectStore.ts @@ -1,6 +1,11 @@ import { create } from 'zustand'; import { createJSONStorage, persist } from 'zustand/middleware'; -import { type AIEnvironment, AIEnvironmentName } from '../data/ai-environments.ts'; +import { + type AIEnvironment, + AIEnvironmentName, + multiFileEnvironments, + initialEnvironment, +} from '../data/ai-environments.ts'; interface ProjectState { // Project metadata @@ -19,12 +24,6 @@ interface ProjectState { setHydrated: () => void; } -export const multiFileEnvironments: ReadonlySet = new Set([ - AIEnvironmentName.Cline, - AIEnvironmentName.Cursor, -]); -export const initialEnvironment: Readonly = AIEnvironmentName.Cursor; - // Create a store with persistence export const useProjectStore = create()( persist( @@ -57,6 +56,14 @@ export const useProjectStore = create()( // Set hydration flag when storage is hydrated onRehydrateStorage: () => (state) => { if (state) { + // Ensure selectedEnvironment is valid, fallback to cursor if not + if ( + !Object.values(AIEnvironmentName).includes( + state.selectedEnvironment as AIEnvironmentName, + ) + ) { + state.selectedEnvironment = AIEnvironmentName.GitHubCopilot; + } state.setHydrated(); } }, From 3d56ba24b29011d146c61f882d64b6c779fc7c5f Mon Sep 17 00:00:00 2001 From: psmyrdek Date: Sat, 26 Jul 2025 09:45:35 +0200 Subject: [PATCH 2/2] chore: propagate cf secrets --- .github/workflows/deploy-app.yml | 2 ++ .github/workflows/pull-request.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/deploy-app.yml b/.github/workflows/deploy-app.yml index ffb1c8c..4c1f8e1 100644 --- a/.github/workflows/deploy-app.yml +++ b/.github/workflows/deploy-app.yml @@ -75,6 +75,8 @@ jobs: SUPABASE_URL: ${{ secrets.SUPABASE_URL }} SUPABASE_PUBLIC_KEY: ${{ secrets.SUPABASE_PUBLIC_KEY }} SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }} + CF_CAPTCHA_SITE_KEY: ${{ secrets.CF_CAPTCHA_SITE_KEY }} + CF_CAPTCHA_SECRET_KEY: ${{ secrets.CF_CAPTCHA_SECRET_KEY }} run: npm run build # This should create the dist/ directory - name: Deploy to Cloudflare Pages diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 689493f..36372a3 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -82,6 +82,8 @@ jobs: echo "E2E_USERNAME_ID=${{ secrets.E2E_USERNAME_ID }}" >> .env.integration echo "E2E_USERNAME=${{ secrets.E2E_USERNAME }}" >> .env.integration echo "E2E_PASSWORD=${{ secrets.E2E_PASSWORD }}" >> .env.integration + echo "CF_CAPTCHA_SITE_KEY=${{ secrets.CF_CAPTCHA_SITE_KEY }}" >> .env.integration + echo "CF_CAPTCHA_SECRET_KEY=${{ secrets.CF_CAPTCHA_SECRET_KEY }}" >> .env.integration - name: Run E2E tests run: npm run test:e2e