diff --git a/agents.md b/agents.md index 03b5aab..2af4cdf 100644 --- a/agents.md +++ b/agents.md @@ -7,7 +7,7 @@ ## Core UI Flow - Entry point at `app/page.tsx` toggles between a marketing hero and the instructions wizard. - Wizard steps currently cover: - - IDE selection (`data/ides.json`). + - Instructions file selection (`data/files.json`). - Framework selection (`data/frameworks.json`) with branching into framework-specific question sets (e.g., `data/questions/react.json`). - Dynamic question sets loaded via `import()` based on the chosen framework. - User actions per question: @@ -18,7 +18,7 @@ ## Data Conventions - Every answer object may define: `value`, `label`, `icon`, `example`, `infoLines` (derived from `pros`/`cons`), `tags`, `isDefault`, `disabled`, `disabledLabel`, and `docs`. - JSON files in `data/` supply domain-specific options: - - `ides.json`, `frameworks.json`, `files.json`, `general.json`, `architecture.json`, `performance.json`, `security.json`, `commits.json`. + - `files.json`, `frameworks.json`, `general.json`, `architecture.json`, `performance.json`, `security.json`, `commits.json`. - Framework-specific questionnaires live in `data/questions/.json`. - Newly added `docs` fields should point to authoritative resources and are surfaced in tooltips as external links. diff --git a/app/api/generate/[ide]/[framework]/[fileName]/route.ts b/app/api/generate/[framework]/[fileName]/route.ts similarity index 60% rename from app/api/generate/[ide]/[framework]/[fileName]/route.ts rename to app/api/generate/[framework]/[fileName]/route.ts index 97dc025..00bd965 100644 --- a/app/api/generate/[ide]/[framework]/[fileName]/route.ts +++ b/app/api/generate/[framework]/[fileName]/route.ts @@ -1,56 +1,50 @@ import { NextRequest, NextResponse } from 'next/server' import { readFile } from 'fs/promises' import path from 'path' + import type { WizardResponses } from '@/types/wizard' import { getTemplateConfig, type TemplateKey } from '@/lib/template-config' -// Helper function to map output file types to template types function mapOutputFileToTemplateType(outputFile: string): string { const mapping: Record = { 'instructions-md': 'copilot-instructions', + 'agents-md': 'agents', 'cursor-rules': 'cursor-rules', 'json-rules': 'json-rules', - 'agents-md': 'agents' } - return mapping[outputFile] || outputFile + + return mapping[outputFile] ?? outputFile } export async function POST( request: NextRequest, - { params }: { params: { ide: string; framework: string; fileName: string } } + { params }: { params: { framework: string; fileName: string } }, ) { try { - const { ide, framework, fileName } = params - const body = await request.json() - const responses: WizardResponses = body - - // Determine template configuration based on the request - let templateConfig - - const frameworkFromPath = framework && !['general', 'none', 'undefined'].includes(framework) - ? framework - : undefined - - if (ide) { - const templateKeyFromParams: TemplateKey = { - ide, - templateType: mapOutputFileToTemplateType(fileName), - framework: frameworkFromPath - } - templateConfig = getTemplateConfig(templateKeyFromParams) + const { framework, fileName } = params + const responses = (await request.json()) as WizardResponses + + const frameworkFromPath = + framework && !['general', 'none', 'undefined'].includes(framework) + ? framework + : undefined + + const templateKeyFromParams: TemplateKey = { + templateType: mapOutputFileToTemplateType(fileName), + framework: frameworkFromPath, } - // Check if this is a combination-based request - if (!templateConfig && responses.preferredIde && responses.outputFile) { - const templateKey: TemplateKey = { - ide: responses.preferredIde, + let templateConfig = getTemplateConfig(templateKeyFromParams) + + if (!templateConfig && responses.outputFile) { + const templateKeyFromBody: TemplateKey = { templateType: mapOutputFileToTemplateType(responses.outputFile), - framework: responses.frameworkSelection || undefined + framework: responses.frameworkSelection || undefined, } - templateConfig = getTemplateConfig(templateKey) + + templateConfig = getTemplateConfig(templateKeyFromBody) } - // Fallback to legacy fileName-based approach if (!templateConfig) { templateConfig = getTemplateConfig(fileName) } @@ -58,31 +52,40 @@ export async function POST( if (!templateConfig) { return NextResponse.json( { error: `Template not found for fileName: ${fileName}` }, - { status: 404 } + { status: 404 }, ) } - // Read the template file const templatePath = path.join(process.cwd(), 'file-templates', templateConfig.template) const template = await readFile(templatePath, 'utf-8') - // Replace template variables with actual values let generatedContent = template + const isJsonTemplate = templateConfig.template.toLowerCase().endsWith('.json') - // Helper function to replace template variables gracefully - const replaceVariable = (key: keyof WizardResponses, fallback: string = 'Not specified') => { - const value = responses[key] + const escapeForJson = (value: string) => { + const escaped = JSON.stringify(value) + return escaped.slice(1, -1) + } + + const replaceVariable = (key: keyof WizardResponses, fallback = 'Not specified') => { const placeholder = `{{${key}}}` + if (!generatedContent.includes(placeholder)) { + return + } + + const value = responses[key] + if (value === null || value === undefined || value === '') { - generatedContent = generatedContent.replace(placeholder, fallback) + const replacement = isJsonTemplate ? escapeForJson(fallback) : fallback + generatedContent = generatedContent.replace(placeholder, replacement) } else { - generatedContent = generatedContent.replace(placeholder, String(value)) + const replacementValue = String(value) + const replacement = isJsonTemplate ? escapeForJson(replacementValue) : replacementValue + generatedContent = generatedContent.replace(placeholder, replacement) } } - // Replace all template variables - replaceVariable('preferredIde') replaceVariable('frameworkSelection') replaceVariable('tooling') replaceVariable('language') @@ -108,18 +111,14 @@ export async function POST( replaceVariable('logging') replaceVariable('commitStyle') replaceVariable('prRules') + replaceVariable('outputFile') - // Return the generated content return NextResponse.json({ content: generatedContent, - fileName: templateConfig.outputFileName + fileName: templateConfig.outputFileName, }) - } catch (error) { console.error('Error generating file:', error) - return NextResponse.json( - { error: 'Failed to generate file' }, - { status: 500 } - ) + return NextResponse.json({ error: 'Failed to generate file' }, { status: 500 }) } } diff --git a/app/layout.tsx b/app/layout.tsx index da9f376..886cd19 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -10,6 +10,7 @@ const geistSans = Geist({ subsets: ["latin"], }); + const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"], @@ -55,7 +56,7 @@ export const metadata: Metadata = { shortcut: "/favicon.ico", apple: "/apple-touch-icon.png", }, - themeColor: "#ffffff", + themeColor: "#09090b", }; @@ -67,14 +68,14 @@ export default function RootLayout({ }>) { return ( - + diff --git a/app/page.tsx b/app/page.tsx index 2bf2148..0793dd9 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,29 +1,45 @@ "use client" -import { useState } from "react" +import { useMemo, useState } from "react" import { Button } from "@/components/ui/button" import { InstructionsWizard } from "@/components/instructions-wizard" -import HeroIconsRow from "@/components/HeroIconsRow" -import { ThemeToggle } from "@/components/theme-toggle" -import { getHeroIconItems, getHomeMainClasses } from "@/lib/utils" +import { getHomeMainClasses } from "@/lib/utils" +import { getFormatLabel } from "@/lib/wizard-utils" import { ANALYTICS_EVENTS } from "@/lib/analytics-events" import { track } from "@/lib/mixpanel" +import type { FileOutputConfig } from "@/types/wizard" import { Github } from "lucide-react" import Link from "next/link" import Logo from "./../components/Logo" +import filesData from "@/data/files.json" export default function Home() { const [showWizard, setShowWizard] = useState(false) - const heroIcons = getHeroIconItems() + const [selectedFileId, setSelectedFileId] = useState(null) + + const fileOptions = useMemo(() => { + return (filesData as FileOutputConfig[]).filter((file) => file.enabled !== false) + }, []) + + const handleFileCtaClick = (file: FileOutputConfig) => { + setSelectedFileId(file.id) + setShowWizard(true) + track(ANALYTICS_EVENTS.CREATE_INSTRUCTIONS_FILE, { + fileId: file.id, + fileLabel: file.label, + }) + } + + const handleWizardClose = () => { + setShowWizard(false) + setSelectedFileId(null) + } return (
{/* Top utility bar */} -
- -
+ {/* File type CTAs */} +
+
+ {fileOptions.map((file) => { + const formatLabel = getFormatLabel(file.format) + return ( + + ) + })} +
- - )} diff --git a/components/HeroIconsRow/HeroIconsRow.tsx b/components/HeroIconsRow/HeroIconsRow.tsx deleted file mode 100644 index 571177d..0000000 --- a/components/HeroIconsRow/HeroIconsRow.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { HeroIconItem } from "@/lib/utils" - -type HeroIconsRowProps = { - items: HeroIconItem[] -} - -export default function HeroIconsRow({ items }: HeroIconsRowProps) { - return ( -
- {items.map(({ icon: Icon, label }) => ( -
-
- -
-

{label}

-
- ))} -
- ) -} diff --git a/components/HeroIconsRow/index.tsx b/components/HeroIconsRow/index.tsx deleted file mode 100644 index c40e065..0000000 --- a/components/HeroIconsRow/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./HeroIconsRow" diff --git a/components/instructions-wizard.tsx b/components/instructions-wizard.tsx index c0814c0..eee9267 100644 --- a/components/instructions-wizard.tsx +++ b/components/instructions-wizard.tsx @@ -6,8 +6,7 @@ import { ArrowLeft } from "lucide-react" import * as simpleIcons from "simple-icons" import type { SimpleIcon } from "simple-icons" -import rawIdes from "@/data/ides.json" -import type { DataAnswerSource, DataQuestionSource, FileOutputConfig, FrameworkConfig, IdeConfig, InstructionsWizardProps, Responses, WizardAnswer, WizardQuestion, WizardResponses, WizardStep } from "@/types/wizard" +import type { DataQuestionSource, FileOutputConfig, FrameworkConfig, InstructionsWizardProps, Responses, WizardAnswer, WizardQuestion, WizardResponses, WizardStep } from "@/types/wizard" import rawFrameworks from "@/data/frameworks.json" import generalData from "@/data/general.json" import architectureData from "@/data/architecture.json" @@ -19,6 +18,10 @@ import { InstructionsAnswerCard } from "./instructions-answer-card" import { ANALYTICS_EVENTS } from "@/lib/analytics-events" import { track } from "@/lib/mixpanel" +import { buildStepFromQuestionSet, getFormatLabel, getMimeTypeForFormat, mapAnswerSourceToWizard } from "@/lib/wizard-utils" + +const fileOptions = filesData as FileOutputConfig[] +const defaultFileOption = fileOptions.find((file) => file.enabled !== false) ?? fileOptions[0] ?? null const FRAMEWORK_STEP_ID = "frameworks" const FRAMEWORK_QUESTION_ID = "frameworkSelection" @@ -117,77 +120,6 @@ const normalizeIconSlug = (raw?: string) => { return iconSlugOverrides[cleaned] ?? cleaned } -const mapAnswerSourceToWizard = (answer: DataAnswerSource): WizardAnswer => { - const infoLines: string[] = [] - - if (answer.pros && answer.pros.length > 0) { - infoLines.push(`Pros: ${answer.pros.join(", ")}`) - } - - if (answer.cons && answer.cons.length > 0) { - infoLines.push(`Cons: ${answer.cons.join(", ")}`) - } - - return { - value: answer.value, - label: answer.label, - icon: answer.icon, - example: answer.example, - infoLines: infoLines.length > 0 ? infoLines : undefined, - docs: answer.docs, - tags: answer.tags, - isDefault: answer.isDefault, - disabled: answer.disabled, - disabledLabel: answer.disabledLabel, - skippable: answer.skippable, - } -} - -const buildStepFromQuestionSet = ( - id: string, - title: string, - questions: DataQuestionSource[] -): WizardStep => ({ - id, - title, - questions: questions.map((question) => ({ - id: question.id, - question: question.question, - allowMultiple: question.allowMultiple, - answers: question.answers.map(mapAnswerSourceToWizard), - skippable: question.skippable, - })), -}) - -const idesStep: WizardStep = { - id: "ides", - title: "Choose Your IDE", - questions: [ - { - id: "preferredIdes", - question: "Which IDEs should we prepare instructions for?", - allowMultiple: true, - skippable: false, - answers: (rawIdes as IdeConfig[]).map((ide) => ({ - value: ide.id, - label: ide.label, - icon: ide.icon, - example: - ide.outputFiles && ide.outputFiles.length > 0 - ? `We'll generate: ${ide.outputFiles.join(", ")}` - : undefined, - infoLines: ide.enabled ? ["Enabled by default"] : undefined, - tags: ide.outputFiles, - isDefault: ide.enabled, - disabled: ide.enabled === false, - disabledLabel: ide.enabled === false ? "Soon" : undefined, - docs: ide.docs, - skippable: ide.skippable, - })), - }, - ], -} - const frameworksStep: WizardStep = { id: FRAMEWORK_STEP_ID, title: "Choose Your Framework", @@ -239,52 +171,15 @@ const commitsStep = buildStepFromQuestionSet( commitsData as DataQuestionSource[] ) -const filesStep: WizardStep = { - id: "files", - title: "Output Files", - questions: [ - { - id: "outputFiles", - question: "Which instruction files should we generate?", - allowMultiple: true, - skippable: false, - answers: (filesData as FileOutputConfig[]).map((file) => { - const infoLines: string[] = [] - if (file.filename) { - infoLines.push(`Filename: ${file.filename}`) - } - if (file.format) { - infoLines.push(`Format: ${file.format}`) - } - - return { - value: file.id, - label: file.label, - icon: file.icon, - infoLines: infoLines.length > 0 ? infoLines : undefined, - docs: file.docs, - tags: file.format ? [file.format] : undefined, - disabled: file.enabled === false, - disabledLabel: file.enabled === false ? "Soon" : undefined, - skippable: file.skippable, - } - }), - }, - ], -} - -const preFrameworkSteps: WizardStep[] = [idesStep, frameworksStep] - -const postFrameworkSteps: WizardStep[] = [ +const suffixSteps: WizardStep[] = [ generalStep, architectureStep, performanceStep, securityStep, commitsStep, - filesStep, ] -export function InstructionsWizard({ onClose }: InstructionsWizardProps) { +export function InstructionsWizard({ onClose, selectedFileId }: InstructionsWizardProps) { const [currentStepIndex, setCurrentStepIndex] = useState(0) const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0) const [responses, setResponses] = useState({}) @@ -292,8 +187,24 @@ export function InstructionsWizard({ onClose }: InstructionsWizardProps) { const [isComplete, setIsComplete] = useState(false) const [showResetConfirm, setShowResetConfirm] = useState(false) + const selectedFile = useMemo(() => { + if (selectedFileId) { + const matchedFile = fileOptions.find((file) => file.id === selectedFileId) + if (matchedFile) { + return matchedFile + } + } + + return defaultFileOption + }, [selectedFileId]) + + const selectedFileFormatLabel = useMemo( + () => (selectedFile ? getFormatLabel(selectedFile.format) : null), + [selectedFile] + ) + const wizardSteps = useMemo( - () => [...preFrameworkSteps, ...dynamicSteps, ...postFrameworkSteps], + () => [frameworksStep, ...dynamicSteps, ...suffixSteps], [dynamicSteps] ) @@ -305,21 +216,6 @@ export function InstructionsWizard({ onClose }: InstructionsWizardProps) { [wizardSteps] ) - const answeredQuestionsCount = useMemo(() => { - return wizardSteps.reduce((count, step) => { - return ( - count + - step.questions.filter((question) => { - const value = responses[question.id] - if (question.allowMultiple) { - return Array.isArray(value) && value.length > 0 - } - return value !== undefined && value !== null - }).length - ) - }, 0) - }, [responses, wizardSteps]) - if (!currentStep || !currentQuestion) { return null } @@ -383,25 +279,7 @@ export function InstructionsWizard({ onClose }: InstructionsWizardProps) { question: question.question, allowMultiple: question.allowMultiple, skippable: question.skippable, - answers: question.answers.map((answer) => { - const infoLines: string[] = [] - if (answer.pros && answer.pros.length > 0) { - infoLines.push(`Pros: ${answer.pros.join(", ")}`) - } - if (answer.cons && answer.cons.length > 0) { - infoLines.push(`Cons: ${answer.cons.join(", ")}`) - } - - return { - value: answer.value, - label: answer.label, - icon: answer.icon, - example: answer.example, - infoLines: infoLines.length > 0 ? infoLines : undefined, - docs: answer.docs, - skippable: answer.skippable, - } - }), + answers: question.answers.map(mapAnswerSourceToWizard), })) setDynamicSteps([ @@ -420,7 +298,7 @@ export function InstructionsWizard({ onClose }: InstructionsWizardProps) { return next }) - setCurrentStepIndex(preFrameworkSteps.length) + setCurrentStepIndex(1) setCurrentQuestionIndex(0) setIsComplete(false) } catch (error) { @@ -521,10 +399,17 @@ export function InstructionsWizard({ onClose }: InstructionsWizardProps) { } const generateInstructionsFile = async () => { - track(ANALYTICS_EVENTS.CREATE_INSTRUCTIONS_FILE) + const outputFileId = selectedFile?.id ?? null + if (!outputFileId) { + console.error("No instructions file selected. Cannot generate output.") + return + } + + track(ANALYTICS_EVENTS.CREATE_INSTRUCTIONS_FILE, { + outputFile: outputFileId, + }) // Create a JSON object with question IDs as keys and their answers as values const questionsAndAnswers: WizardResponses = { - preferredIde: null, frameworkSelection: null, tooling: null, language: null, @@ -555,30 +440,8 @@ export function InstructionsWizard({ onClose }: InstructionsWizardProps) { wizardSteps.forEach((step) => { step.questions.forEach((question) => { - let key = question.id - let answer = responses[question.id] - - // Special handling for preferredIdes and outputFiles - if (key === "preferredIdes") { - key = "preferredIde" - if (Array.isArray(answer) && answer.length > 0) { - answer = answer[0] - } else if (typeof answer === "string") { - // already a string - } else { - answer = null - } - } - if (key === "outputFiles") { - key = "outputFile" - if (Array.isArray(answer) && answer.length > 0) { - answer = answer[0] - } else if (typeof answer === "string") { - // already a string - } else { - answer = null - } - } + const key = question.id + const answer = responses[question.id] if (answer !== null && answer !== undefined) { if (question.allowMultiple && Array.isArray(answer)) { @@ -595,10 +458,11 @@ export function InstructionsWizard({ onClose }: InstructionsWizardProps) { }) }) + questionsAndAnswers.outputFile = outputFileId + // Ensure we have the combination data for the API - // The API will now use preferredIde + outputFile + frameworkSelection to determine the template + // The API will now use outputFile + frameworkSelection to determine the template console.log('Template combination data:', { - ide: questionsAndAnswers.preferredIde, outputFile: questionsAndAnswers.outputFile, framework: questionsAndAnswers.frameworkSelection }) @@ -607,13 +471,12 @@ export function InstructionsWizard({ onClose }: InstructionsWizardProps) { // Call the API to generate the instructions file if (questionsAndAnswers.outputFile) { - const ideSegment = questionsAndAnswers.preferredIde ?? 'unknown' const frameworkSegment = questionsAndAnswers.frameworkSelection ?? 'general' const fileNameSegment = questionsAndAnswers.outputFile try { const response = await fetch( - `/api/generate/${encodeURIComponent(ideSegment)}/${encodeURIComponent(frameworkSegment)}/${encodeURIComponent(fileNameSegment)}`, + `/api/generate/${encodeURIComponent(frameworkSegment)}/${encodeURIComponent(fileNameSegment)}`, { method: 'POST', headers: { @@ -625,9 +488,11 @@ export function InstructionsWizard({ onClose }: InstructionsWizardProps) { if (response.ok) { const data = await response.json() + const fileConfig = fileOptions.find((file) => file.id === questionsAndAnswers.outputFile) + const mimeType = getMimeTypeForFormat(fileConfig?.format) // Create a downloadable file - const blob = new Blob([data.content], { type: 'text/markdown' }) + const blob = new Blob([data.content], { type: mimeType }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url @@ -648,28 +513,45 @@ export function InstructionsWizard({ onClose }: InstructionsWizardProps) { } const renderCompletion = () => { - const summary = wizardSteps.flatMap((step) => - step.questions.map((question) => { - const value = responses[question.id] - const selectedAnswers = question.answers.filter((answer) => { - if (value === null) { - return false + const summary = [ + selectedFile + ? { + question: "Instructions file", + skipped: false, + answers: [ + selectedFile.label, + selectedFile.filename ? `Filename: ${selectedFile.filename}` : null, + selectedFileFormatLabel ? `Format: ${selectedFileFormatLabel}` : null, + ].filter((entry): entry is string => Boolean(entry)), } + : { + question: "Instructions file", + skipped: true, + answers: [], + }, + ...wizardSteps.flatMap((step) => + step.questions.map((question) => { + const value = responses[question.id] + const selectedAnswers = question.answers.filter((answer) => { + if (value === null) { + return false + } - if (Array.isArray(value)) { - return value.includes(answer.value) - } + if (Array.isArray(value)) { + return value.includes(answer.value) + } - return value === answer.value - }) + return value === answer.value + }) - return { - question: question.question, - skipped: value === null, - answers: selectedAnswers.map((answer) => answer.label), - } - }) - ) + return { + question: question.question, + skipped: value === null, + answers: selectedAnswers.map((answer) => answer.label), + } + }) + ), + ] return (
@@ -728,6 +610,32 @@ export function InstructionsWizard({ onClose }: InstructionsWizardProps) { ) : null} + {selectedFile ? ( +
+
+
+

Instructions file

+

{selectedFile.label}

+ {selectedFile.filename ? ( +

{selectedFile.filename}

+ ) : null} +
+
+ {selectedFileFormatLabel ? ( + + {selectedFileFormatLabel} format + + ) : null} + {onClose ? ( + + ) : null} +
+
+
+ ) : null} + {isComplete ? ( renderCompletion() ) : ( diff --git a/components/theme-toggle.tsx b/components/theme-toggle.tsx deleted file mode 100644 index 1c5a472..0000000 --- a/components/theme-toggle.tsx +++ /dev/null @@ -1,23 +0,0 @@ -"use client" - -import * as React from "react" -import { Moon, Sun } from "lucide-react" -import { useTheme } from "next-themes" - -import { Button } from "@/components/ui/button" - -export function ThemeToggle() { - const { setTheme, theme } = useTheme() - - return ( - - ) -} diff --git a/data/files.json b/data/files.json index e0649b0..b6e0d55 100644 --- a/data/files.json +++ b/data/files.json @@ -23,19 +23,9 @@ "id": "cursor-rules", "label": "Cursor Rules", "filename": ".cursor/rules", - "format": "cursor-rules", - "enabled": false, - "docs": "https://docs.cursor.com/workflows/rules", - "skippable": false - }, - { - "id": "json-rules", - "label": "Generic JSON Config", - "filename": ".devcontext.json", "format": "json", - "enabled": false, - "icon": "json", - "docs": "https://json-schema.org/", + "enabled": true, + "docs": "https://docs.cursor.com/workflows/rules", "skippable": false } ] diff --git a/data/ides.json b/data/ides.json deleted file mode 100644 index 08527a3..0000000 --- a/data/ides.json +++ /dev/null @@ -1,49 +0,0 @@ -[ - { - "id": "vscode", - "label": "VS Code", - "icon": "visualstudiocode", - "enabled": true, - "docs": "https://code.visualstudio.com/docs", - "skippable": false, - "outputFiles": [ - "instructions-md", - "agents-md" - ] - }, - { - "id": "cursor", - "label": "Cursor", - "icon": "/icons/cursor.svg", - "enabled": false, - "docs": "https://docs.cursor.com/", - "skippable": false, - "outputFiles": [ - "cursor-rules", - "json-rules" - ] - }, - { - "id": "jetbrains", - "label": "JetBrains IDEs", - "icon": "/icons/jetbrains.svg", - "enabled": false, - "docs": "https://www.jetbrains.com/help/", - "skippable": false, - "outputFiles": [ - "instructions-md" - ] - }, - { - "id": "windsurf", - "label": "Windsurf", - "icon": "windsurf", - "enabled": false, - "docs": "https://codeium.com/windsurf", - "skippable": false, - "outputFiles": [ - "instructions-md", - "json-rules" - ] - } -] diff --git a/file-templates/agents-template.md b/file-templates/agents-template.md index 0344138..cf480db 100644 --- a/file-templates/agents-template.md +++ b/file-templates/agents-template.md @@ -1,48 +1,66 @@ # Agents Development Guide -This guide provides development conventions and best practices for building AI agent applications. +This guide provides conventions and best practices for building AI agent applications. -## Project Overview +--- + +## 1. Project Overview -- IDE: **{{preferredIde}}** - Framework/Language: **{{frameworkSelection}}** / **{{language}}** - Build tooling: **{{tooling}}** - Primary focus: **{{projectPriority}}** -## Development Standards +--- + +## 2. Development Standards ### Code Organization - File structure: **{{fileStructure}}** - Folder organization: **{{folders}}** - Naming conventions: - - Variables/functions: **{{variableNaming}}** + - Variables & functions: **{{variableNaming}}** - Files: **{{fileNaming}}** - Components/classes: **{{componentNaming}}** ### Code Style & Quality - Code style: **{{codeStyle}}** - Export style: **{{exports}}** -- Comments: **{{comments}}** -- Testing: - - Unit tests: **{{testingUT}}** - - E2E tests: **{{testingE2E}}** +- Comments/documentation: **{{comments}}** + +### Testing +- Unit tests: **{{testingUT}}** +- E2E tests: **{{testingE2E}}** -## Agent-Specific Patterns +**Additional for Agents** +- Add scenario-based tests simulating conversations or workflows. +- Validate agent fallbacks (how the agent behaves when tools or APIs fail). -### State Management +--- + +## 3. Agent-Specific Patterns + +### State & Memory - State handling: **{{stateManagement}}** - Data fetching: **{{dataFetching}}** +- Memory / context strategy: define how the agent retains conversation or state. ### API Integration - API layer: **{{apiLayer}}** - Authentication: **{{auth}}** - Validation: **{{validation}}** +- Tool usage: document which external APIs or tools the agent can call. ### Performance & Monitoring - Logging: **{{logging}}** - Performance considerations: **{{reactPerf}}** +- Additional concerns: + - Monitor token usage and cost efficiency. + - Handle API rate limits gracefully. + - Use observability tools to track agent responses and latency. + +--- -## Collaboration & Git +## 4. Collaboration & Git ### Version Control - Commit style: **{{commitStyle}}** @@ -53,4 +71,5 @@ This guide provides development conventions and best practices for building AI a --- -*This agents guide was generated based on your project configuration and can be customized for your specific agent development needs.* \ No newline at end of file +*This agents guide was auto-generated based on your project configuration. +Customize it further to align with your specific agent development workflows.* diff --git a/file-templates/copilot-instructions-template.md b/file-templates/copilot-instructions-template.md index c2b93dd..d3084aa 100644 --- a/file-templates/copilot-instructions-template.md +++ b/file-templates/copilot-instructions-template.md @@ -3,43 +3,41 @@ applyTo: "**/*.{ts,tsx,js,jsx,md}" # apply to all code files by default --- -# Project Overview -These are the conventions and guardrails Copilot should follow when generating code, tests, commits, and PRs in our project. -They reflect the decisions we made (IDE, framework, language) and real-world best practices. +# Copilot Instructions + +⚠️ This file is **auto-generated**. Do not edit manually unless overriding defaults. +Regenerate whenever your JSON configuration changes (framework, naming, testing, etc.). --- ## 1. Project Context & Priorities -- IDE: **{{preferredIde}}** - Framework: **{{frameworkSelection}}** - Build tooling: **{{tooling}}** - Language: **{{language}}** - Primary focus: **{{projectPriority}}** -> Use this context — when Copilot needs to choose between simpler vs. more optimized code, prefer what aligns with **{{projectPriority}}**. +> Use this context when Copilot suggests alternatives: prefer what aligns with **{{projectPriority}}**. --- ## 2. Naming, Style & Structure Rules ### Naming & Exports - - Variables, functions, object keys: **{{variableNaming}}** - Files & modules: **{{fileNaming}}** -- Components, types: **{{componentNaming}}** -- Always use **{{exports}}** exports style -- Comments & documentation style: **{{comments}}** +- Components & types: **{{componentNaming}}** +- Always use **{{exports}}** export style +- Comments/documentation style: **{{comments}}** - Code style: follow **{{codeStyle}}** ### File and Folder Structure - -- Component / UI layout organization: **{{fileStructure}}** +- Component / UI layout: **{{fileStructure}}** - Styling approach: **{{styling}}** -- State management: adopt **{{stateManagement}}** -- API layer organization: put remote calls in **{{apiLayer}}** -- Folder strategy: **{{folders}}** +- State management: **{{stateManagement}}** +- API layer organization: **{{apiLayer}}** +- Folder strategy: **{{folders}}** > Copilot should not generate code outside these structures or naming patterns. @@ -48,98 +46,96 @@ They reflect the decisions we made (IDE, framework, language) and real-world bes ## 3. Testing & Quality Assurance - Unit tests: **{{testingUT}}** -- E2E / integration: **{{testingE2E}}** +- E2E / integration: **{{testingE2E}}** -**Rules** +**Rules** - Use descriptive test names. -- Always include both “happy path” and edge cases. -- Avoid large tests that span too many modules. -- Tests should live alongside modules (or in designated `__tests__` folder per convention). +- Cover both “happy path” and edge cases. +- Keep tests focused and avoid spanning unrelated modules. +- Place tests alongside modules or in designated `__tests__` folders. --- ## 4. Performance & Data Loading -- Data fetching approach: **{{dataFetching}}** -- React performance optimizations: **{{reactPerf}}** +- Data fetching: **{{dataFetching}}** +- React performance optimizations: **{{reactPerf}}** -**Do** -- Use pagination or limit responses. -- Memoize computations or components when data is large. -- Lazy-load modules/components that aren’t critical at startup. +**Do** +- Use pagination or limit queries. +- Memoize expensive computations. +- Lazy-load non-critical modules. -**Don’t** -- Fetch all data at once without constraints. -- Place heavy logic in render without memoization. +**Don’t** +- Fetch all data at once. +- Put heavy logic in render without memoization. --- ## 5. Security, Validation, Logging -- Secrets / auth handling: **{{auth}}** +- Secrets/auth handling: **{{auth}}** - Input validation: **{{validation}}** -- Logging style: **{{logging}}** +- Logging: **{{logging}}** -**Rules** -- Never embed secrets in code; always use environment variables. -- Validate all incoming data (API or client side) using the chosen validation library. -- Logging messages should never reveal secrets or PII. -- Use structured or contextual logs (vs. free-form `console.log`) especially in production. +**Rules** +- Never commit secrets; use environment variables. +- Validate all incoming data (API and client). +- Do not log secrets or PII. +- Use structured/contextual logs instead of raw `console.log`. --- ## 6. Commit & PR Conventions -- Commit message style: **{{commitStyle}}** +- Commit style: **{{commitStyle}}** - PR rules: **{{prRules}}** -**Do** -- Write commit messages that follow the agreed style (e.g. `feat: add login`) -- Keep PRs small and focused -- Always link the issue or ticket -- If PR introduces new API or breaking change, update the documentation +**Do** +- Follow commit style (`feat: add login`, `fix: correct bug`). +- Keep PRs small and focused. +- Link issues/tickets. +- Update docs for new APIs or breaking changes. -**Don’t** -- Use vague commit messages like “fix stuff” -- Combine unrelated changes in one commit or PR +**Don’t** +- Use vague commit messages like “fix stuff”. +- Bundle unrelated changes. --- ## 7. Copilot Usage Guidance -- Use Copilot to scaffold boilerplate (e.g. `useQuery`, component boilerplate), not to bypass core logic. -- When writing prompts/comments for Copilot, embed **context** (e.g. expected return shape, types). -- When Copilot suggests code that violates naming, structure, or validation rules – override or reject it. -- For ambiguous design choices, ask for clarification in comments (e.g. “// Should this go in services or hooks?”). -- Prefer completions that respect folder boundaries and import paths (don’t let Copilot propose imports from “wrong” layers). +- Use Copilot for boilerplate (hooks, component scaffolds). +- Provide context in comments/prompts. +- Reject completions that break naming, structure, or validation rules. +- Ask clarifying questions in comments (e.g., “// Should this live in services?”). +- Prefer completions that respect folder boundaries and import paths. ---- +**Don’t rely on Copilot for** +- Security-critical code (auth, encryption). +- Inferring business logic without requirements. +- Blindly accepting untyped/unsafe code. -## 8. IDE-Specific Rules & Settings +--- -For **VS Code**: +## 8. Editor Setup -- Use `.editorconfig` for consistent indent / line endings -- Enable **Prettier** and **ESLint**, synced to our style rules -- Set `editor.formatOnSave = true` -- Suggested extensions: `dbaeumer.vscode-eslint`, `esbenp.prettier-vscode`, `formulahendry.auto-rename-tag` -- Avoid conflicting formatters or duplicated rules +Recommended editor configuration: -> These help Copilot suggestions align more closely with how your code will be formatted and linted. +- Use `.editorconfig` for indentation/line endings. +- Enable linting/formatting (ESLint, Prettier, or Biome). +- Set `editor.formatOnSave = true`. +- Suggested integrations: + - VS Code: `dbaeumer.vscode-eslint`, `esbenp.prettier-vscode` + - JetBrains: ESLint + Prettier plugins + - Cursor: use built-in `.instructions.md` support --- ## 9. Caveats & Overrides -- If a feature is experimental or out-of-scope, document it in comments. -- In rare cases, exceptions may be allowed — but always document why. -- Always run linters and tests on generated code before merging. +- Document exceptions with comments. +- Experimental features must be flagged. +- Always run linters and tests before merging Copilot-generated code. --- - -## Notes - -- This instructions file was **auto-generated** based on your chosen configuration. -- Regenerate it whenever your JSON configuration changes (framework, naming, testing, etc.). -- You may also split this file into domain-specific `.instructions.md` files using `applyTo` frontmatter if your project grows. - diff --git a/file-templates/cursor-rules-template.json b/file-templates/cursor-rules-template.json new file mode 100644 index 0000000..413d663 --- /dev/null +++ b/file-templates/cursor-rules-template.json @@ -0,0 +1,43 @@ +{ + "project": { + "framework": "{{frameworkSelection}}", + "language": "{{language}}", + "tooling": "{{tooling}}", + "priority": "{{projectPriority}}" + }, + "rules": { + "naming": { + "variables": "{{variableNaming}}", + "files": "{{fileNaming}}", + "components": "{{componentNaming}}" + }, + "exports": "{{exports}}", + "comments": "{{comments}}", + "codeStyle": "{{codeStyle}}", + "structure": { + "fileStructure": "{{fileStructure}}", + "styling": "{{styling}}", + "stateManagement": "{{stateManagement}}", + "apiLayer": "{{apiLayer}}", + "folders": "{{folders}}" + }, + "testing": { + "unit": "{{testingUT}}", + "e2e": "{{testingE2E}}" + }, + "performance": { + "dataFetching": "{{dataFetching}}", + "reactOptimizations": "{{reactPerf}}" + }, + "security": { + "auth": "{{auth}}", + "validation": "{{validation}}", + "logging": "{{logging}}" + }, + "git": { + "commitStyle": "{{commitStyle}}", + "prRules": "{{prRules}}", + "collaboration": "{{collaboration}}" + } + } +} \ No newline at end of file diff --git a/lib/__tests__/template-config.test.ts b/lib/__tests__/template-config.test.ts index 9fbef88..82e5ed1 100644 --- a/lib/__tests__/template-config.test.ts +++ b/lib/__tests__/template-config.test.ts @@ -5,7 +5,7 @@ describe('template-config', () => { describe('getTemplateConfig', () => { describe('with string key (legacy support)', () => { it('should return correct config for existing string key', () => { - const result = getTemplateConfig('vscode-copilot-instructions') + const result = getTemplateConfig('copilot-instructions') expect(result).toEqual({ template: 'copilot-instructions-template.md', outputFileName: 'copilot-instructions.md' @@ -13,7 +13,7 @@ describe('template-config', () => { }) it('should return correct config for agents template', () => { - const result = getTemplateConfig('vscode-agents') + const result = getTemplateConfig('agents') expect(result).toEqual({ template: 'agents-template.md', outputFileName: 'agents.md' @@ -21,10 +21,10 @@ describe('template-config', () => { }) it('should return correct config for cursor rules', () => { - const result = getTemplateConfig('cursor-cursor-rules') + const result = getTemplateConfig('cursor-rules') expect(result).toEqual({ - template: 'copilot-instructions-template.md', - outputFileName: '.cursorrules' + template: 'cursor-rules-template.json', + outputFileName: '.cursor/rules' }) }) @@ -50,85 +50,67 @@ describe('template-config', () => { describe('with TemplateKey object', () => { it('should return config for specific framework combination', () => { const key: TemplateKey = { - ide: 'vscode', templateType: 'agents', - framework: 'python' + framework: 'python', } const result = getTemplateConfig(key) expect(result).toEqual({ template: 'agents-template.md', - outputFileName: 'agents.md' + outputFileName: 'agents.md', }) }) it('should return config for specific react combination', () => { const key: TemplateKey = { - ide: 'vscode', templateType: 'agents', - framework: 'react' + framework: 'react', } const result = getTemplateConfig(key) expect(result).toEqual({ template: 'agents-template.md', - outputFileName: 'agents.md' + outputFileName: 'agents.md', }) }) it('should fallback to general combination when specific framework not found', () => { const key: TemplateKey = { - ide: 'vscode', templateType: 'copilot-instructions', - framework: 'nonexistent-framework' + framework: 'nonexistent-framework', } const result = getTemplateConfig(key) expect(result).toEqual({ template: 'copilot-instructions-template.md', - outputFileName: 'copilot-instructions.md' + outputFileName: 'copilot-instructions.md', }) }) it('should return general combination when no framework specified', () => { const key: TemplateKey = { - ide: 'vscode', - templateType: 'copilot-instructions' - } - const result = getTemplateConfig(key) - expect(result).toEqual({ - template: 'copilot-instructions-template.md', - outputFileName: 'copilot-instructions.md' - }) - }) - - it('should fallback to template type only when ide-template combination not found', () => { - const key: TemplateKey = { - ide: 'nonexistent-ide', - templateType: 'instructions-md' + templateType: 'copilot-instructions', } const result = getTemplateConfig(key) expect(result).toEqual({ template: 'copilot-instructions-template.md', - outputFileName: 'copilot-instructions.md' + outputFileName: 'copilot-instructions.md', }) }) it('should return null when no matching configuration found', () => { const key: TemplateKey = { - ide: 'nonexistent-ide', - templateType: 'nonexistent-template' + templateType: 'nonexistent-template', } const result = getTemplateConfig(key) expect(result).toBeNull() }) - it('should handle cursor IDE with rules template', () => { + it('should support cursor rules template type', () => { const key: TemplateKey = { - ide: 'cursor', - templateType: 'cursor-rules' + templateType: 'cursor-rules', } const result = getTemplateConfig(key) expect(result).toEqual({ - template: 'copilot-instructions-template.md', - outputFileName: '.cursorrules' + template: 'cursor-rules-template.json', + outputFileName: '.cursor/rules', }) }) }) @@ -136,11 +118,14 @@ describe('template-config', () => { describe('templateCombinations object', () => { it('should contain all expected template combinations', () => { const expectedKeys = [ - 'vscode-copilot-instructions', - 'vscode-agents', - 'vscode-agents-python', - 'vscode-agents-react', - 'cursor-cursor-rules', + 'copilot-instructions', + 'copilot-instructions-react', + 'copilot-instructions-nextjs', + 'agents', + 'agents-react', + 'agents-python', + 'cursor-rules', + 'json-rules', 'instructions-md' ] @@ -161,4 +146,4 @@ describe('template-config', () => { }) }) }) -}) \ No newline at end of file +}) diff --git a/lib/template-config.ts b/lib/template-config.ts index 1eb922f..38e1e12 100644 --- a/lib/template-config.ts +++ b/lib/template-config.ts @@ -4,73 +4,77 @@ export interface TemplateConfig { } export interface TemplateKey { - ide: string templateType: string framework?: string } -// Template configurations based on IDE + template type + framework combinations +// Template configurations based on template type + optional framework combinations export const templateCombinations: Record = { - // VS Code + Copilot Instructions (general) - 'vscode-copilot-instructions': { + // Copilot Instructions (general) + 'copilot-instructions': { template: 'copilot-instructions-template.md', - outputFileName: 'copilot-instructions.md' + outputFileName: 'copilot-instructions.md', }, - // VS Code + Agents guide (general) - 'vscode-agents': { + // Copilot Instructions + React (specific combination) + 'copilot-instructions-react': { + template: 'copilot-instructions-template.md', + outputFileName: 'copilot-instructions.md', + }, + // Copilot Instructions + Next.js (specific combination) + 'copilot-instructions-nextjs': { + template: 'copilot-instructions-template.md', + outputFileName: 'copilot-instructions.md', + }, + // Agents guide (general) + agents: { template: 'agents-template.md', - outputFileName: 'agents.md' + outputFileName: 'agents.md', }, - // VS Code + Agents guide + Python (specific combination) - 'vscode-agents-python': { + // Agents guide + React (specific combination) + 'agents-react': { template: 'agents-template.md', - outputFileName: 'agents.md' + outputFileName: 'agents.md', }, - // VS Code + Agents guide + React (specific combination) - 'vscode-agents-react': { + // Agents guide + Python (specific combination) + 'agents-python': { template: 'agents-template.md', - outputFileName: 'agents.md' + outputFileName: 'agents.md', }, - // Cursor + Rules - 'cursor-cursor-rules': { - template: 'copilot-instructions-template.md', // Will be replaced with cursor-specific template - outputFileName: '.cursorrules' + // Cursor rules + 'cursor-rules': { + template: 'cursor-rules-template.json', + outputFileName: '.cursor/rules', }, - // Fallback for legacy fileName-based approach + // Generic JSON rules (placeholder) + 'json-rules': { + template: 'copilot-instructions-template.md', + outputFileName: '.devcontext.json', + }, + // Legacy alias for backward compatibility 'instructions-md': { template: 'copilot-instructions-template.md', - outputFileName: 'copilot-instructions.md' + outputFileName: 'copilot-instructions.md', }, - // Add more combinations here as needed } export function getTemplateConfig(key: string | TemplateKey): TemplateConfig | null { if (typeof key === 'string') { // Legacy support for fileName-based approach - return templateCombinations[key] || null + return templateCombinations[key] ?? null } - // New combination-based approach - const { ide, templateType, framework } = key + const { templateType, framework } = key - // Try specific combination first (with framework) if (framework) { - const specificKey = `${ide}-${templateType}-${framework}` + const specificKey = `${templateType}-${framework}` if (templateCombinations[specificKey]) { return templateCombinations[specificKey] } } - // Try general combination (without framework) - const generalKey = `${ide}-${templateType}` - if (templateCombinations[generalKey]) { - return templateCombinations[generalKey] - } - - // Try fallback to just template type (for backward compatibility) if (templateCombinations[templateType]) { return templateCombinations[templateType] } return null -} \ No newline at end of file +} diff --git a/lib/utils.ts b/lib/utils.ts index f756fc0..86e8071 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,5 +1,4 @@ import { clsx, type ClassValue } from "clsx" -import { Activity, Bot, Code, Terminal, Zap, type LucideIcon } from "lucide-react" import { twMerge } from "tailwind-merge" export function cn(...inputs: ClassValue[]) { @@ -12,20 +11,3 @@ export function getHomeMainClasses(showWizard: boolean) { showWizard ? "justify-start" : "justify-center text-center" ) } - -export type HeroIconItem = { - icon: LucideIcon - label: string -} - -const HERO_ICON_ITEMS: HeroIconItem[] = [ - { icon: Code, label: "VS Code" }, - { icon: Activity, label: "React" }, - { icon: Zap, label: "Angular" }, - { icon: Terminal, label: "Cursor" }, - { icon: Bot, label: "GitHub Copilot" }, -] - -export function getHeroIconItems() { - return HERO_ICON_ITEMS -} diff --git a/lib/wizard-utils.ts b/lib/wizard-utils.ts index bdf0289..d0abc8e 100644 --- a/lib/wizard-utils.ts +++ b/lib/wizard-utils.ts @@ -25,6 +25,7 @@ export const mapAnswerSourceToWizard = (answer: DataAnswerSource): WizardAnswer isDefault: answer.isDefault, disabled: answer.disabled, disabledLabel: answer.disabledLabel, + skippable: answer.skippable, } } @@ -43,5 +44,40 @@ export const buildStepFromQuestionSet = ( question: question.question, allowMultiple: question.allowMultiple, answers: question.answers.map(mapAnswerSourceToWizard), + skippable: question.skippable, })), -}) \ No newline at end of file +}) + +const formatLabelMap: Record = { + markdown: "Markdown", + json: "JSON", + "cursor-rules-json": "JSON", +} + +const formatMimeTypeMap: Record = { + markdown: "text/markdown", + json: "application/json", + "cursor-rules-json": "application/json", +} + +/** + * Converts a stored format identifier into a human-friendly label + */ +export const getFormatLabel = (format?: string) => { + if (!format) { + return null + } + + return formatLabelMap[format] ?? format +} + +/** + * Returns the browser mime-type associated with a stored format identifier + */ +export const getMimeTypeForFormat = (format?: string) => { + if (!format) { + return "text/plain" + } + + return formatMimeTypeMap[format] ?? "text/plain" +} diff --git a/types/wizard.ts b/types/wizard.ts index 568a2c6..c216aca 100644 --- a/types/wizard.ts +++ b/types/wizard.ts @@ -1,13 +1,3 @@ -export type IdeConfig = { - id: string - label: string - icon?: string - enabled?: boolean - outputFiles?: string[] - docs?: string - skippable?: boolean -} - export type FrameworkConfig = { id: string label: string @@ -80,12 +70,12 @@ export type WizardStep = { } export type InstructionsWizardProps = { + selectedFileId?: string | null onClose?: () => void } export type Responses = Record export interface WizardResponses { - preferredIde: string | null; frameworkSelection: string | null; tooling: string | null; language: string | null;