diff --git a/app/api/generate/[framework]/[fileName]/route.ts b/app/api/generate/[framework]/[fileName]/route.ts index c2590b3..a2d2c8e 100644 --- a/app/api/generate/[framework]/[fileName]/route.ts +++ b/app/api/generate/[framework]/[fileName]/route.ts @@ -4,6 +4,7 @@ import path from 'path' import type { WizardResponses } from '@/types/wizard' import { getTemplateConfig, type TemplateKey } from '@/lib/template-config' +import { getStackGuidance } from '@/lib/stack-guidance' function mapOutputFileToTemplateType(outputFile: string): string { const mapping: Record = { @@ -111,8 +112,23 @@ export async function POST( replaceVariable('logging') replaceVariable('commitStyle') replaceVariable('prRules') + const replaceStaticPlaceholder = (placeholderKey: string, value: string) => { + const placeholder = `{{${placeholderKey}}}` + + if (!generatedContent.includes(placeholder)) { + return + } + + const replacement = isJsonTemplate ? escapeForJson(value) : value + generatedContent = generatedContent.replace(placeholder, replacement) + } + replaceVariable('outputFile') + const stackGuidanceSlug = responses.stackSelection || frameworkFromPath + const stackGuidance = getStackGuidance(stackGuidanceSlug) + replaceStaticPlaceholder('stackGuidance', stackGuidance) + return NextResponse.json({ content: generatedContent, fileName: templateConfig.outputFileName, diff --git a/app/new/page.tsx b/app/new/page.tsx index 6945509..070d6d5 100644 --- a/app/new/page.tsx +++ b/app/new/page.tsx @@ -11,6 +11,7 @@ import { track } from "@/lib/mixpanel" import type { DataQuestionSource, FileOutputConfig } from "@/types/wizard" import { Github } from "lucide-react" import Link from "next/link" +import { useSearchParams } from "next/navigation" import filesData from "@/data/files.json" import { buildFileOptionsFromQuestion } from "@/lib/wizard-utils" @@ -20,10 +21,12 @@ const fileQuestion = fileQuestionSet[0] ?? null const fileOptionsFromData = buildFileOptionsFromQuestion(fileQuestion) export default function NewInstructionsPage() { + const searchParams = useSearchParams() const [showWizard, setShowWizard] = useState(false) const [selectedFileId, setSelectedFileId] = useState(null) const fileOptions = useMemo(() => fileOptionsFromData, []) + const preferredStackId = searchParams.get("stack")?.toLowerCase() ?? null const handleFileCtaClick = (file: FileOutputConfig) => { setSelectedFileId(file.id) @@ -72,7 +75,11 @@ export default function NewInstructionsPage() { {/* Hero Section */}
{showWizard && selectedFileId ? ( - + ) : ( <>
diff --git a/app/page.tsx b/app/page.tsx index be0d541..b467336 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -12,6 +12,9 @@ export default function LandingPage() {
+ diff --git a/app/stacks/[stack]/page.tsx b/app/stacks/[stack]/page.tsx new file mode 100644 index 0000000..ae0f3a1 --- /dev/null +++ b/app/stacks/[stack]/page.tsx @@ -0,0 +1,189 @@ +import Link from "next/link" +import type { Metadata } from "next" +import { notFound } from "next/navigation" + +import stacksData from "@/data/stacks.json" +import type { DataQuestionSource } from "@/types/wizard" + +const stackQuestionSet = stacksData as DataQuestionSource[] +const stackQuestion = stackQuestionSet[0] +const stackAnswers = stackQuestion?.answers ?? [] + +const STACK_PAGE_DETAILS: Record = { + react: { + title: "React instructions file", + description: + "Ship consistent React instructions that cover hooks usage, component structures, and testing expectations before you open the wizard.", + highlights: [ + "Bundle instructions for hooks, components, and state tooling", + "Document styling decisions across Tailwind or CSS Modules", + "Share testing policies with Jest and React Testing Library", + ], + }, + nextjs: { + title: "Next.js instructions file", + description: "Clarify App Router patterns, rendering modes, and data fetching strategies for your Next.js project.", + highlights: [ + "Capture SSR, SSG, or ISR defaults for routes", + "Explain data fetching via server actions or route handlers", + "Align styling and component conventions across the monorepo", + ], + }, + angular: { + title: "Angular instructions file", + description: "Outline Angular module structure, RxJS usage, and testing defaults for teams shipping with Angular.", + highlights: [ + "Define standalone components vs. NgModule structure", + "Document reactive forms, signals, and service patterns", + "Capture Jest or Karma coverage expectations", + ], + }, + vue: { + title: "Vue instructions file", + description: "Guide your team on Composition API, Pinia state, and the Vue testing stack before exporting guidance.", + highlights: [ + "Document whether components use the Composition or Options API", + "Agree on Pinia, Vuex, or lightweight store helpers", + "Share styling choices from SFC scoped CSS to Tailwind", + ], + }, + nuxt: { + title: "Nuxt instructions file", + description: "Capture Nuxt-specific rendering, data fetching, and deployment strategies to feed into AI assistants.", + highlights: [ + "Set expectations for SSR, SSG, or hybrid rendering", + "List how you use runtime config, server routes, and Nitro", + "Clarify deployment adapters whether Vercel, Netlify, or custom", + ], + }, + svelte: { + title: "Svelte instructions file", + description: "Align on SvelteKit tooling, store patterns, and styling so generated instructions match your stack.", + highlights: [ + "Capture whether you build with SvelteKit or bare Vite", + "Explain store usage and when to reach for external libs", + "Define styling guidance across scoped CSS or Tailwind", + ], + }, + astro: { + title: "Astro instructions file", + description: "Describe islands architecture, content collections, and deployment workflows for Astro projects.", + highlights: [ + "Identify which integration (React, Vue, Svelte) powers islands", + "Explain your rendering defaults and revalidation windows", + "Document CMS or content-collection structure for writers", + ], + }, + remix: { + title: "Remix instructions file", + description: "Share data loader patterns, runtime choices, and styling so AI agents stay true to your Remix app.", + highlights: [ + "Capture whether loaders, resource routes, or client fetch power data", + "Document hosting choices like Vercel, Fly.io, or Express", + "Signal styling conventions across Tailwind or CSS Modules", + ], + }, + python: { + title: "Python instructions file", + description: "Detail your Python framework, typing policy, and packaging so Copilot and agents stay in sync.", + highlights: [ + "State whether FastAPI, Django, or Flask powers the API", + "Explain typing expectations and lint tooling like Ruff", + "Document package and testing workflows across Poetry or pytest", + ], + docsNote: "Need a framework not listed? Pick Python, then document it in the first question.", + }, +} + +export function generateStaticParams() { + return stackAnswers + .filter((answer) => typeof answer.value === "string" && answer.value.length > 0) + .map((answer) => ({ stack: answer.value })) +} + +export function generateMetadata({ params }: { params: { stack: string } }): Metadata { + const slug = params.stack.toLowerCase() + const stackEntry = stackAnswers.find((answer) => answer.value === slug) + + if (!stackEntry) { + return { + title: "DevContext instructions", + description: "Generate clear AI assistant instructions tailored to your framework.", + } + } + + const details = STACK_PAGE_DETAILS[slug] + const title = details ? `${details.title} | DevContext` : `${stackEntry.label} instructions file | DevContext` + const description = details?.description ?? `Create an instructions file for ${stackEntry.label} with DevContext.` + + return { + title, + description, + alternates: { + canonical: `https://devcontext.xyz/stacks/${slug}`, + }, + } +} + +export default function StackLandingPage({ params }: { params: { stack: string } }) { + const slug = params.stack.toLowerCase() + const stackEntry = stackAnswers.find((answer) => answer.value === slug) + + if (!stackEntry) { + notFound() + } + + const details = STACK_PAGE_DETAILS[slug] + const highlights = details?.highlights ?? [] + const pageTitle = details?.title ?? `${stackEntry.label} instructions file` + const description = details?.description ?? `Start a ${stackEntry.label} instructions wizard with DevContext.` + const targetUrl = `/new?stack=${slug}` + + return ( +
+
+

Stack presets

+

{pageTitle}

+

{description}

+ {stackEntry.docs ? ( +

+ Source docs: {" "} + + {stackEntry.label} documentation + +

+ ) : null} + {details?.docsNote ? ( +

{details.docsNote}

+ ) : null} +
+ + {highlights.length > 0 ? ( +
+

What this preset covers

+
    + {highlights.map((bullet) => ( +
  • {bullet}
  • + ))} +
+
+ ) : null} + +
+

Ready to build your instructions?

+

+ Launch the DevContext wizard with {stackEntry.label} pre-selected. You can review every question, accept defaults, and export a + stack-aware instructions file when each section is complete. +

+
+ + Start the {stackEntry.label} wizard + +
+
+
+ ) +} diff --git a/app/stacks/page.tsx b/app/stacks/page.tsx new file mode 100644 index 0000000..361077a --- /dev/null +++ b/app/stacks/page.tsx @@ -0,0 +1,59 @@ +import Link from "next/link" +import type { Metadata } from "next" + +import stacksData from "@/data/stacks.json" +import type { DataQuestionSource } from "@/types/wizard" + +const stackQuestionSet = stacksData as DataQuestionSource[] +const stackQuestion = stackQuestionSet[0] +const stackAnswers = stackQuestion?.answers ?? [] + +export const metadata: Metadata = { + title: "Choose Your Stack | DevContext", + description: "Explore framework-specific instructions flows for React, Vue, Svelte, Python, and more.", +} + +export default function StacksIndexPage() { + return ( +
+
+

Framework instructions presets

+

+ Jump straight into the DevContext wizard with copy tailored to your stack. Each page outlines what we cover and links directly + into the guided flow. +

+
+ +
+ {stackAnswers.map((answer) => { + const href = answer.value ? `/stacks/${answer.value}` : "/new" + return ( + + ) + })} +
+
+ ) +} diff --git a/components/instructions-wizard.tsx b/components/instructions-wizard.tsx index b7c03e3..051daf5 100644 --- a/components/instructions-wizard.tsx +++ b/components/instructions-wizard.tsx @@ -1,10 +1,11 @@ "use client" -import { useCallback, useEffect, useMemo, useState } from "react" +import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { Button } from "@/components/ui/button" import { Undo2 } from "lucide-react" import type { DataQuestionSource, FileOutputConfig, InstructionsWizardProps, Responses, WizardAnswer, WizardConfirmationIntent, WizardQuestion, WizardStep } from "@/types/wizard" +import { buildFilterPlaceholder, useAnswerFilter } from "@/hooks/use-answer-filter" import stacksData from "@/data/stacks.json" import generalData from "@/data/general.json" import architectureData from "@/data/architecture.json" @@ -89,7 +90,7 @@ const suffixSteps: WizardStep[] = [ commitsStep, ] -export function InstructionsWizard({ onClose, selectedFileId }: InstructionsWizardProps) { +export function InstructionsWizard({ onClose, selectedFileId, initialStackId }: InstructionsWizardProps) { const [currentStepIndex, setCurrentStepIndex] = useState(0) const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0) const [responses, setResponses] = useState({}) @@ -102,6 +103,7 @@ export function InstructionsWizard({ onClose, selectedFileId }: InstructionsWiza const [autoFilledQuestionMap, setAutoFilledQuestionMap] = useState>({}) const [autoFillNotice, setAutoFillNotice] = useState(null) const [activeEditQuestionId, setActiveEditQuestionId] = useState(null) + const hasAppliedInitialStack = useRef(null) useEffect(() => { if (!isComplete && activeEditQuestionId) { @@ -153,6 +155,20 @@ export function InstructionsWizard({ onClose, selectedFileId }: InstructionsWiza const currentStep = wizardSteps[currentStepIndex] ?? null const currentQuestion = currentStep?.questions[currentQuestionIndex] ?? null + const { + answers: filteredAnswers, + query: answerFilterQuery, + setQuery: setAnswerFilterQuery, + isFiltering: isFilteringAnswers, + } = useAnswerFilter(currentQuestion ?? null) + const filterPlaceholder = buildFilterPlaceholder(currentQuestion ?? null) + const showNoFilterMatches = Boolean( + currentQuestion?.enableFilter && + filteredAnswers.length === 0 && + answerFilterQuery.trim().length > 0 + ) + const filterInputId = currentQuestion ? `answer-filter-${currentQuestion.id}` : "answer-filter" + const totalQuestions = useMemo( () => wizardSteps.reduce((count, step) => count + step.questions.length, 0), [wizardSteps] @@ -297,7 +313,7 @@ export function InstructionsWizard({ onClose, selectedFileId }: InstructionsWiza setIsComplete(false) } - const loadStackQuestions = async (stackId: string, stackLabel?: string) => { + const loadStackQuestions = useCallback(async (stackId: string, stackLabel?: string) => { try { setGeneratedFile(null) @@ -346,9 +362,33 @@ export function InstructionsWizard({ onClose, selectedFileId }: InstructionsWiza console.error(`Unable to load questions for stack "${stackId}"`, error) setDynamicSteps([]) setIsStackFastTrackPromptVisible(false) - } finally { } - } + }, []) + + useEffect(() => { + if (!initialStackId) { + return + } + + if (hasAppliedInitialStack.current === initialStackId) { + return + } + + const stackAnswer = stackQuestion?.answers.find((answer) => answer.value === initialStackId) + + if (!stackAnswer) { + return + } + + hasAppliedInitialStack.current = initialStackId + + setResponses((prev) => ({ + ...prev, + [STACK_QUESTION_ID]: stackAnswer.value, + })) + + void loadStackQuestions(stackAnswer.value, stackAnswer.label) + }, [initialStackId, loadStackQuestions]) const applyDefaultsAcrossWizard = () => { setGeneratedFile(null) @@ -439,35 +479,32 @@ export function InstructionsWizard({ onClose, selectedFileId }: InstructionsWiza setGeneratedFile(null) + const prevValue = responses[question.id] let nextValue: Responses[keyof Responses] let didAddSelection = false - setResponses((prev) => { - const prevValue = prev[question.id] + if (question.allowMultiple) { + const prevArray = Array.isArray(prevValue) ? prevValue : [] - if (question.allowMultiple) { - const prevArray = Array.isArray(prevValue) ? prevValue : [] - - if (prevArray.includes(answer.value)) { - nextValue = prevArray.filter((item) => item !== answer.value) - } else { - nextValue = [...prevArray, answer.value] - didAddSelection = true - } + if (prevArray.includes(answer.value)) { + nextValue = prevArray.filter((item) => item !== answer.value) } else { - if (prevValue === answer.value) { - nextValue = undefined - } else { - nextValue = answer.value - didAddSelection = true - } + nextValue = [...prevArray, answer.value] + didAddSelection = true } - - return { - ...prev, - [question.id]: nextValue, + } else { + if (prevValue === answer.value) { + nextValue = undefined + } else { + nextValue = answer.value + didAddSelection = true } - }) + } + + setResponses((prev) => ({ + ...prev, + [question.id]: nextValue, + })) clearAutoFilledFlag(question.id) @@ -543,6 +580,7 @@ export function InstructionsWizard({ onClose, selectedFileId }: InstructionsWiza setIsStackFastTrackPromptVisible(false) setAutoFilledQuestionMap({}) setAutoFillNotice(null) + hasAppliedInitialStack.current = null } const resetWizard = () => { @@ -739,13 +777,45 @@ export function InstructionsWizard({ onClose, selectedFileId }: InstructionsWiza

{currentQuestion.question}

+ + {currentQuestion.enableFilter ? ( +
+ +
+ setAnswerFilterQuery(event.target.value)} + placeholder={filterPlaceholder} + className="w-full rounded-lg border border-border/70 bg-background/80 px-3 py-2 text-sm text-foreground shadow-sm transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/60" + /> +
+ {isFilteringAnswers && !showNoFilterMatches ? ( +

+ Showing {filteredAnswers.length} of {currentQuestion.answers.length} +

+ ) : null} +
+ ) : null}
- + {showNoFilterMatches ? ( +

+ No options match "{answerFilterQuery}". Try a different search. +

+ ) : ( + + )}
diff --git a/components/wizard-edit-answer-dialog.tsx b/components/wizard-edit-answer-dialog.tsx index e477b23..af8be3a 100644 --- a/components/wizard-edit-answer-dialog.tsx +++ b/components/wizard-edit-answer-dialog.tsx @@ -1,6 +1,7 @@ import { Button } from "@/components/ui/button" import { WizardAnswerGrid } from "./wizard-answer-grid" import type { Responses, WizardAnswer, WizardQuestion } from "@/types/wizard" +import { buildFilterPlaceholder, useAnswerFilter } from "@/hooks/use-answer-filter" type WizardEditAnswerDialogProps = { question: WizardQuestion @@ -10,6 +11,11 @@ type WizardEditAnswerDialogProps = { } export function WizardEditAnswerDialog({ question, value, onAnswerSelect, onClose }: WizardEditAnswerDialogProps) { + const { answers, query, setQuery, isFiltering } = useAnswerFilter(question) + const filterPlaceholder = buildFilterPlaceholder(question) + const showNoMatches = Boolean(question.enableFilter && answers.length === 0 && query.trim().length > 0) + const filterInputId = `edit-answer-filter-${question.id}` + const isSelected = (candidate: string) => { if (question.allowMultiple) { return Array.isArray(value) && value.includes(candidate) @@ -34,7 +40,36 @@ export function WizardEditAnswerDialog({ question, value, onAnswerSelect, onClos
- + {question.enableFilter ? ( +
+ +
+ setQuery(event.target.value)} + placeholder={filterPlaceholder} + className="w-full rounded-lg border border-border/70 bg-background/80 px-3 py-2 text-sm text-foreground shadow-sm transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/60" + /> +
+ {isFiltering && !showNoMatches ? ( +

+ Showing {answers.length} of {question.answers.length} +

+ ) : null} +
+ ) : null} + + {showNoMatches ? ( +

+ No options match "{query}". Try a different search. +

+ ) : ( + + )}
) diff --git a/data/questions/astro.json b/data/questions/astro.json new file mode 100644 index 0000000..2325788 --- /dev/null +++ b/data/questions/astro.json @@ -0,0 +1,140 @@ +[ + { + "id": "astro-integrations", + "question": "Which frontend integration do you render islands with?", + "responseKey": "tooling", + "answers": [ + { + "value": "react", + "label": "React components", + "icon": "react", + "docs": "https://docs.astro.build/en/guides/integrations-guide/react/", + "pros": ["Huge ecosystem", "JSX familiarity"], + "cons": ["Larger bundle"], + "example": "npx astro add react", + "isDefault": true + }, + { + "value": "vue", + "label": "Vue components", + "icon": "vuedotjs", + "docs": "https://docs.astro.build/en/guides/integrations-guide/vue/", + "pros": ["Reactivity", "SFC ergonomics"], + "cons": ["Need to manage hydration"], + "example": "npx astro add vue" + }, + { + "value": "svelte", + "label": "Svelte components", + "icon": "svelte", + "docs": "https://docs.astro.build/en/guides/integrations-guide/svelte/", + "pros": ["Compile-time", "Small output"], + "cons": ["Less SSR libs"], + "example": "npx astro add svelte" + } + ], + "explanation": "Primary integration influences hydration directives and linting." + }, + { + "id": "astro-rendering", + "question": "How do you prefer Astro to render pages?", + "responseKey": "dataFetching", + "answers": [ + { + "value": "ssg", + "label": "Static (SSG)", + "docs": "https://docs.astro.build/en/concepts/islands/", + "pros": ["Ultra fast delivery", "No server costs"], + "cons": ["Rebuild to refresh data"], + "example": "astro build", + "isDefault": true + }, + { + "value": "ssr", + "label": "Server-side rendering", + "docs": "https://docs.astro.build/en/guides/server-side-rendering/", + "pros": ["Personalized content"], + "cons": ["Need hosting runtime"], + "example": "export const prerender = false" + }, + { + "value": "hybrid", + "label": "Hybrid (SSG + ISR)", + "docs": "https://docs.astro.build/en/guides/content-collections/", + "pros": ["Control freshness", "Cache friendly"], + "cons": ["More ops complexity"], + "example": "export const revalidate = 60" + } + ], + "explanation": "Rendering model informs content strategy and hosting provider." + }, + { + "id": "astro-content", + "question": "Where does your content live?", + "responseKey": "folders", + "answers": [ + { + "value": "content-collections", + "label": "Content collections", + "docs": "https://docs.astro.build/en/guides/content-collections/", + "pros": ["Type-safe frontmatter", "MDX ready"], + "cons": ["Needs schema definitions"], + "example": "defineCollection({ type: 'content', schema })", + "isDefault": true + }, + { + "value": "cms", + "label": "External CMS", + "docs": "https://docs.astro.build/en/guides/cms/", + "pros": ["Editorial workflows"], + "cons": ["Auth tokens"], + "example": "const posts = await fetchCMSEntries()" + }, + { + "value": "filesystem", + "label": "Filesystem markdown", + "docs": "https://docs.astro.build/en/core-concepts/astro-pages/", + "pros": ["Quick start"], + "cons": ["Manual organization"], + "example": "src/pages/posts/post-1.md" + } + ], + "explanation": "Content strategy shapes file organization and build scripts." + }, + { + "id": "astro-deployment", + "question": "Where do you deploy Astro?", + "responseKey": "collaboration", + "answers": [ + { + "value": "vercel", + "label": "Vercel", + "icon": "vercel", + "docs": "https://docs.astro.build/en/guides/deploy/vercel/", + "pros": ["Edge functions", "Git integration"], + "cons": ["SSR quotas"], + "example": "vercel deploy", + "isDefault": true + }, + { + "value": "netlify", + "label": "Netlify", + "icon": "netlify", + "docs": "https://docs.astro.build/en/guides/deploy/netlify/", + "pros": ["Forms and edge"], + "cons": ["Build minutes"], + "example": "netlify deploy" + }, + { + "value": "cloudflare", + "label": "Cloudflare Pages", + "icon": "cloudflare", + "docs": "https://docs.astro.build/en/guides/deploy/cloudflare/", + "pros": ["Global edge"], + "cons": ["Limited Node APIs"], + "example": "wrangler pages deploy dist" + } + ], + "explanation": "Hosting target sets adapter, environment variables, and CI strategy." + } +] diff --git a/data/questions/nuxt.json b/data/questions/nuxt.json new file mode 100644 index 0000000..9c86f9f --- /dev/null +++ b/data/questions/nuxt.json @@ -0,0 +1,137 @@ +[ + { + "id": "nuxt-rendering", + "question": "Which rendering mode do you rely on?", + "responseKey": "dataFetching", + "answers": [ + { + "value": "server-side", + "label": "Server-rendered (SSR)", + "docs": "https://nuxt.com/docs/guide/concepts/rendering#server-side-rendering", + "pros": ["Great for SEO", "Dynamic data per request"], + "cons": ["Higher server cost"], + "example": "export default defineNuxtConfig({ ssr: true })", + "isDefault": true + }, + { + "value": "static", + "label": "Static site generation (SSG)", + "docs": "https://nuxt.com/docs/guide/concepts/rendering#static-site-generation", + "pros": ["Fast edge delivery", "Easy CDN caching"], + "cons": ["Requires rebuild to refresh data"], + "example": "nuxi generate" + }, + { + "value": "hybrid", + "label": "Hybrid (ISR / SWR)", + "docs": "https://nuxt.com/docs/guide/concepts/rendering#hybrid-rendering", + "pros": ["Cache control", "Partial regeneration"], + "cons": ["More edge configuration"], + "example": "export default defineCachedEventHandler(handler, { swr: true })" + } + ], + "explanation": "Rendering mode determines deployment targets and caching patterns." + }, + { + "id": "nuxt-data", + "question": "How do you fetch data inside pages?", + "responseKey": "apiLayer", + "answers": [ + { + "value": "use-fetch", + "label": "useFetch / $fetch", + "docs": "https://nuxt.com/docs/getting-started/data-fetching", + "pros": ["Simple", "SSR-aware"], + "cons": ["Limited customization"], + "example": "const { data } = await useFetch('/api/products')", + "isDefault": true + }, + { + "value": "nitro-server", + "label": "Nuxt server routes", + "docs": "https://nuxt.com/docs/guide/going-further/server", + "pros": ["Single language", "Great for BFF"], + "cons": ["Requires runtime deployment"], + "example": "export default defineEventHandler(event => { ... })" + }, + { + "value": "external-client", + "label": "External REST/GraphQL client", + "docs": "https://nuxt.com/docs/guide/going-further/runtime-config", + "pros": ["Reuse existing SDKs"], + "cons": ["Client secrets management"], + "example": "const client = new GraphQLClient(endpoint)" + } + ], + "explanation": "Fetching strategy affects runtime config and environment variables." + }, + { + "id": "nuxt-styling", + "question": "How do you approach styling?", + "responseKey": "styling", + "answers": [ + { + "value": "tailwind", + "label": "Tailwind CSS", + "icon": "/icons/tailwindcss.svg", + "docs": "https://nuxt.com/modules/tailwindcss", + "pros": ["Official module", "Fast iteration"], + "cons": ["Utility-first learning curve"], + "example": "npm install -D @nuxtjs/tailwindcss", + "isDefault": true + }, + { + "value": "uno", + "label": "UnoCSS", + "docs": "https://github.com/antfu/unocss", + "pros": ["On-demand utilities", "Preset flex"], + "cons": ["Less docs"], + "example": "npm install -D @unocss/nuxt" + }, + { + "value": "scoped-css", + "label": "Scoped CSS in SFCs", + "docs": "https://vue-loader.vuejs.org/guide/scoped-css.html", + "pros": ["Zero deps", "Native to Vue"], + "cons": ["No shared design tokens"], + "example": "" + } + ], + "explanation": "Styling choice influences runtime head bleed and bundle size." + }, + { + "id": "nuxt-deployment", + "question": "Where do you deploy Nuxt?", + "responseKey": "folders", + "answers": [ + { + "value": "vercel", + "label": "Vercel", + "icon": "vercel", + "docs": "https://nuxt.com/docs/getting-started/deployment#vercel", + "pros": ["Zero-config", "Edge rendering"], + "cons": ["Cold start quotas"], + "example": "vercel --prod", + "isDefault": true + }, + { + "value": "netlify", + "label": "Netlify", + "icon": "netlify", + "docs": "https://nuxt.com/docs/getting-started/deployment#netlify", + "pros": ["Functions included", "Atomic deploys"], + "cons": ["ISR configuration"], + "example": "netlify deploy --build" + }, + { + "value": "custom-node", + "label": "Custom Node or Docker", + "docs": "https://nuxt.com/docs/getting-started/deployment#node-server", + "pros": ["Full control", "Reuse company infra"], + "cons": ["Requires provisioning"], + "example": "node .output/server/index.mjs" + } + ], + "explanation": "Deployment target signals adapter, environment variables, and caching policies." + } +] diff --git a/data/questions/python.json b/data/questions/python.json new file mode 100644 index 0000000..c68d739 --- /dev/null +++ b/data/questions/python.json @@ -0,0 +1,170 @@ +[ + { + "id": "python-framework", + "question": "Which primary Python framework are you using?", + "responseKey": "apiLayer", + "answers": [ + { + "value": "fastapi", + "label": "FastAPI", + "icon": "fastapi", + "docs": "https://fastapi.tiangolo.com/", + "pros": ["Async-first", "Great docs"], + "cons": ["Requires uvicorn"], + "example": "app = FastAPI()", + "isDefault": true + }, + { + "value": "django", + "label": "Django", + "icon": "django", + "docs": "https://docs.djangoproject.com/en/stable/", + "pros": ["Batteries included", "Admin UI"], + "cons": ["Heavier"], + "example": "django-admin startproject mysite" + }, + { + "value": "flask", + "label": "Flask", + "icon": "flask", + "docs": "https://flask.palletsprojects.com/", + "pros": ["Lightweight", "Flexible"], + "cons": ["Manual decisions"], + "example": "app = Flask(__name__)" + } + ], + "explanation": "Framework choice informs project structure, CLI commands, and deployment guidance." + }, + { + "id": "python-language", + "question": "How strict are your Python type hints?", + "responseKey": "language", + "answers": [ + { + "value": "typing-required", + "label": "PEP 484 everywhere", + "docs": "https://peps.python.org/pep-0484/", + "pros": ["Strong tooling", "Great for large teams"], + "cons": ["Upfront effort"], + "example": "def create_user(payload: UserIn) -> UserOut:", + "isDefault": true + }, + { + "value": "typing-gradual", + "label": "Gradual typing", + "docs": "https://typing.readthedocs.io/en/latest/", + "pros": ["Balance speed and safety"], + "cons": ["Mixed consistency"], + "example": "name: str | None = None" + }, + { + "value": "dynamic", + "label": "Dynamic (no hints)", + "docs": "https://docs.python.org/3/tutorial/controlflow.html#defining-functions", + "pros": ["Fast iteration"], + "cons": ["Harder refactors"], + "example": "def create_user(payload): ..." + } + ], + "explanation": "Typing policy guides lint rules, CI checks, and editor setup." + }, + { + "id": "python-packages", + "question": "Which package manager do you standardize on?", + "responseKey": "tooling", + "answers": [ + { + "value": "poetry", + "label": "Poetry", + "docs": "https://python-poetry.org/docs/", + "pros": ["Lockfile", "Project metadata"], + "cons": ["Learns new CLI"], + "example": "poetry add fastapi", + "isDefault": true + }, + { + "value": "pip-tools", + "label": "pip + pip-tools", + "docs": "https://pip-tools.readthedocs.io/en/latest/", + "pros": ["Deterministic builds"], + "cons": ["Split requirements files"], + "example": "pip-compile --generate-hashes" + }, + { + "value": "uv", + "label": "uv (Astral)", + "docs": "https://docs.astral.sh/uv/", + "pros": ["Very fast", "Works with pyproject"], + "cons": ["Newer tool"], + "example": "uv pip install fastapi" + } + ], + "explanation": "Package tooling defines scripts, lockfiles, and reproducibility guidance." + }, + { + "id": "python-testing", + "question": "Which testing approach do you use?", + "responseKey": "testingUT", + "answers": [ + { + "value": "pytest", + "label": "pytest", + "docs": "https://docs.pytest.org/en/stable/", + "pros": ["Fixtures", "Plugins"], + "cons": ["Magic can hide errors"], + "example": "pytest tests/api --maxfail=1", + "isDefault": true + }, + { + "value": "unittest", + "label": "unittest", + "docs": "https://docs.python.org/3/library/unittest.html", + "pros": ["Standard library"], + "cons": ["Verbose"], + "example": "python -m unittest" + }, + { + "value": "behave", + "label": "Behave", + "docs": "https://behave.readthedocs.io/en/stable/", + "pros": ["BDD scenarios"], + "cons": ["More overhead"], + "example": "behave features/" + } + ], + "explanation": "Testing strategy controls fixtures, assertions, and coverage expectations." + }, + { + "id": "python-formatting", + "question": "How do you enforce formatting and linting?", + "responseKey": "codeStyle", + "answers": [ + { + "value": "ruff", + "label": "Ruff", + "docs": "https://docs.astral.sh/ruff/", + "pros": ["All-in-one lint+format", "Fast"], + "cons": ["Newer tool"], + "example": "ruff check .", + "isDefault": true + }, + { + "value": "black-isort-flake8", + "label": "Black + isort + Flake8", + "docs": "https://black.readthedocs.io/en/stable/", + "pros": ["Battle tested"], + "cons": ["Multiple configs"], + "example": "black . && isort . && flake8" + }, + { + "value": "none", + "label": "Ad-hoc per project", + "docs": "https://docs.python.org/3/library/", + "pros": ["Flexible"], + "cons": ["Inconsistent"], + "example": "# Document conventions in README" + } + ], + "explanation": "Lint command shapes pre-commit hooks and CI tasks." + } +] diff --git a/data/questions/remix.json b/data/questions/remix.json new file mode 100644 index 0000000..abd5f65 --- /dev/null +++ b/data/questions/remix.json @@ -0,0 +1,130 @@ +[ + { + "id": "remix-deployment", + "question": "Which Remix runtime do you target?", + "responseKey": "folders", + "answers": [ + { + "value": "vercel", + "label": "Vercel", + "icon": "vercel", + "docs": "https://remix.run/docs/en/main/guides/deployment/vercel", + "pros": ["Edge runtime", "Zero config"], + "cons": ["Streaming nuances"], + "example": "npx create-remix@latest --template remix-run/remix/templates/vercel", + "isDefault": true + }, + { + "value": "flyio", + "label": "Fly.io", + "docs": "https://remix.run/docs/en/main/guides/deployment/fly", + "pros": ["Regional DB close to app"], + "cons": ["More ops"], + "example": "fly deploy" + }, + { + "value": "express", + "label": "Custom Node/Express", + "docs": "https://remix.run/docs/en/main/guides/deployment/node", + "pros": ["Full control"], + "cons": ["Manage your own infra"], + "example": "import { createRequestHandler } from '@remix-run/express'" + } + ], + "explanation": "Runtime influences adapter config and streaming support." + }, + { + "id": "remix-data", + "question": "How do you load data in routes?", + "responseKey": "dataFetching", + "answers": [ + { + "value": "route-loaders", + "label": "Route loaders + actions", + "docs": "https://remix.run/docs/en/main/file-conventions/route-files-v2", + "pros": ["Parallel loading", "Form handling"], + "cons": ["Requires server runtime"], + "example": "export async function loader({ params }) { ... }", + "isDefault": true + }, + { + "value": "resource-routes", + "label": "Resource routes", + "docs": "https://remix.run/docs/en/main/file-conventions/resource-routes", + "pros": ["Shareable endpoints"], + "cons": ["More files"] + }, + { + "value": "client-fetch", + "label": "Client-side fetch", + "docs": "https://remix.run/docs/en/main/guides/client-data", + "pros": ["Works on static hosts"], + "cons": ["Delayed data", "Harder SEO"], + "example": "useEffect(() => fetch('/api/data'))" + } + ], + "explanation": "Data loaders determine cache headers and request shape." + }, + { + "id": "remix-styling", + "question": "How do you style Remix routes?", + "responseKey": "styling", + "answers": [ + { + "value": "tailwind", + "label": "Tailwind CSS", + "icon": "/icons/tailwindcss.svg", + "docs": "https://remix.run/docs/en/main/styling/tailwind", + "pros": ["Just-in-time classes"], + "cons": ["Requires plugin setup"], + "example": "export const links = () => [{ rel: 'stylesheet', href: styles }]", + "isDefault": true + }, + { + "value": "css-modules", + "label": "CSS Modules", + "icon": "/icons/css3.svg", + "docs": "https://remix.run/docs/en/main/styling/css-modules", + "pros": ["Scoped styles"], + "cons": ["Compilation step"], + "example": "import styles from './route.module.css'" + }, + { + "value": "styled-components", + "label": "Styled Components", + "docs": "https://remix.run/docs/en/main/styling/styled-components", + "pros": ["Dynamic theming"], + "cons": ["Runtime overhead"], + "example": "const Button = styled.button`...`" + } + ], + "explanation": "Styling impacts how you emit tags and manage critical CSS." + }, + { + "id": "remix-testing", + "question": "What testing setup do you rely on?", + "responseKey": "testingUT", + "answers": [ + { + "value": "vitest", + "label": "Vitest + Testing Library", + "icon": "/icons/vitest.svg", + "docs": "https://testing-library.com/docs/remix-testing-library/intro/", + "pros": ["Lightweight", "Remix-aware"], + "cons": ["Needs polyfills"], + "example": "import { createRemixStub } from '@remix-run/testing'", + "isDefault": true + }, + { + "value": "jest", + "label": "Jest", + "icon": "/icons/jest.svg", + "docs": "https://remix.run/docs/en/main/guides/testing", + "pros": ["Mature mocks"], + "cons": ["Slower cold start"], + "example": "jest.mock('@remix-run/react')" + } + ], + "explanation": "Testing toolkit influences route mocks and fixture structure." + } +] diff --git a/data/questions/svelte.json b/data/questions/svelte.json new file mode 100644 index 0000000..62f4dc9 --- /dev/null +++ b/data/questions/svelte.json @@ -0,0 +1,158 @@ +[ + { + "id": "svelte-tooling", + "question": "Which Svelte tooling do you use?", + "responseKey": "tooling", + "answers": [ + { + "value": "sveltekit", + "label": "SvelteKit", + "icon": "svelte", + "docs": "https://kit.svelte.dev/docs/introduction", + "pros": ["Full-stack routing", "Adapters"], + "cons": ["Requires adapter config"], + "example": "npm create svelte@latest my-app", + "isDefault": true + }, + { + "value": "vite", + "label": "Vite + Svelte plugin", + "icon": "/icons/vite.svg", + "docs": "https://github.com/sveltejs/vite-plugin-svelte", + "pros": ["Fast HMR", "Barebones"], + "cons": ["Handle routing yourself"], + "example": "npm create vite@latest my-app -- --template svelte" + }, + { + "value": "elderjs", + "label": "Elder.js", + "docs": "https://elderguide.com/tech/elderjs/", + "pros": ["Content-first", "SEO oriented"], + "cons": ["Small community"], + "example": "npx create-elder-app my-app" + } + ], + "explanation": "Tooling choice affects routing, adapters, and default file layout." + }, + { + "id": "svelte-language", + "question": "Which language mode do you prefer?", + "responseKey": "language", + "answers": [ + { + "value": "typescript", + "label": "TypeScript", + "icon": "/icons/typescript.svg", + "docs": "https://kit.svelte.dev/docs/typescript", + "pros": ["Generics in stores", "Autocomplete"], + "cons": ["Requires ambient types"], + "example": "", + "isDefault": true + }, + { + "value": "javascript", + "label": "JavaScript", + "icon": "/icons/javascript.svg", + "docs": "https://developer.mozilla.org/en-US/docs/Web/JavaScript", + "pros": ["Less boilerplate"], + "cons": ["No type metadata"], + "example": "" + } + ], + "explanation": "Language selection informs linting and generated types." + }, + { + "id": "svelte-state", + "question": "How do you manage cross-component state?", + "responseKey": "stateManagement", + "answers": [ + { + "value": "svelte-stores", + "label": "Writable stores", + "docs": "https://svelte.dev/docs/svelte-store", + "pros": ["No dependencies", "Reactive"], + "cons": ["Manual structure"], + "example": "export const cart = writable([])", + "isDefault": true + }, + { + "value": "zustand", + "label": "Zustand", + "docs": "https://docs.pmnd.rs/zustand/getting-started/introduction", + "pros": ["Tiny API", "Immer support"], + "cons": ["Requires adapter"], + "example": "const useStore = create(set => ({ count: 0 }))" + }, + { + "value": "redux-toolkit", + "label": "Redux Toolkit", + "docs": "https://redux-toolkit.js.org/", + "pros": ["Predictable", "Middleware"], + "cons": ["Verbose"], + "example": "configureStore({ reducer })" + } + ], + "explanation": "State pick influences helper imports and best practices." + }, + { + "id": "svelte-styling", + "question": "How do you style Svelte components?", + "responseKey": "styling", + "answers": [ + { + "value": "scoped-css", + "label": "Scoped ", + "isDefault": true + }, + { + "value": "tailwind", + "label": "Tailwind CSS", + "icon": "/icons/tailwindcss.svg", + "docs": "https://tailwindcss.com/docs/guides/sveltekit", + "pros": ["Utility-first", "Design tokens"], + "cons": ["Class soup"], + "example": "" + }, + { + "value": "vanilla-extract", + "label": "Vanilla Extract", + "docs": "https://vanilla-extract.style/documentation/getting-started/sveltekit", + "pros": ["Type-safe", "Static CSS"], + "cons": ["Build step"], + "example": "export const button = style({ background: vars.color.primary })" + } + ], + "explanation": "Styling approach impacts bundler config and component ergonomics." + }, + { + "id": "svelte-testing", + "question": "What do you use for testing?", + "responseKey": "testingUT", + "answers": [ + { + "value": "vitest", + "label": "Vitest + Testing Library", + "icon": "/icons/vitest.svg", + "docs": "https://testing-library.com/docs/svelte-testing-library/intro/", + "pros": ["Fast", "Svelte-focused"], + "cons": ["Needs environment setup"], + "example": "import { render } from '@testing-library/svelte'", + "isDefault": true + }, + { + "value": "playwright", + "label": "Playwright", + "icon": "/icons/playwright.svg", + "docs": "https://playwright.dev/docs/test-intro", + "pros": ["Full browser", "Trace viewer"], + "cons": ["Slower"], + "example": "test('homepage loads', async ({ page }) => {...})" + } + ], + "explanation": "Testing setup defines scripts, globals, and CI expectations." + } +] diff --git a/data/questions/vue.json b/data/questions/vue.json new file mode 100644 index 0000000..e85e7de --- /dev/null +++ b/data/questions/vue.json @@ -0,0 +1,224 @@ +[ + { + "id": "vue-tooling", + "question": "Which build tooling do you use for Vue?", + "responseKey": "tooling", + "answers": [ + { + "value": "vite", + "label": "Vite", + "icon": "/icons/vite.svg", + "docs": "https://vitejs.dev/guide/", + "pros": [ + "Instant HMR", + "Stable defaults" + ], + "cons": [ + "Requires plugin for legacy browsers" + ], + "example": "npm create vite@latest my-app -- --template vue", + "isDefault": true + }, + { + "value": "create-vue", + "label": "Create Vue", + "icon": "vuedotjs", + "docs": "https://cli.vuejs.org/guide/", + "pros": [ + "Official scaffolder", + "Vue CLI UI" + ], + "cons": [ + "Heavier config" + ], + "example": "npm init vue@latest" + }, + { + "value": "quasar", + "label": "Quasar CLI", + "docs": "https://quasar.dev/start/installation", + "pros": [ + "Prebuilt components", + "Cross-platform" + ], + "cons": [ + "Opinionated structure" + ], + "example": "npm init quasar" + } + ], + "explanation": "Choose the bundler or CLI that created your Vue project." + }, + { + "id": "vue-language", + "question": "Do you author Vue with TypeScript or JavaScript?", + "responseKey": "language", + "answers": [ + { + "value": "typescript", + "label": "TypeScript", + "icon": "/icons/typescript.svg", + "docs": "https://vuejs.org/guide/typescript/overview.html", + "pros": [ + "Stronger tooling", + "Better editor DX" + ], + "cons": [ + "Requires type annotations" + ], + "example": "", + "isDefault": true + }, + { + "value": "javascript", + "label": "JavaScript", + "icon": "/icons/javascript.svg", + "docs": "https://developer.mozilla.org/en-US/docs/Web/JavaScript", + "pros": [ + "Lower barrier", + "Less setup" + ], + "cons": [ + "No static checks" + ], + "example": "" + } + ], + "explanation": "Language choice affects linting rules and code completion hints." + }, + { + "id": "vue-components", + "question": "How do you organize Vue components?", + "responseKey": "fileStructure", + "answers": [ + { + "value": "feature-folders", + "label": "Feature folders", + "icon": "/icons/folder-tree.svg", + "pros": [ + "Domain boundaries", + "Scales with teams" + ], + "cons": [ + "Requires discipline" + ], + "example": "src/features/auth/components/LoginForm.vue", + "isDefault": true + }, + { + "value": "flat", + "label": "Flat components", + "icon": "/icons/layout.svg", + "pros": [ + "Simple", + "Easy to scan" + ], + "cons": [ + "Becomes cluttered" + ], + "example": "src/components/Button.vue" + }, + { + "value": "single-file-modules", + "label": "SFC modules with index", + "icon": "/icons/layout.svg", + "pros": [ + "Encapsulates logic", + "Enables barrel exports" + ], + "cons": [ + "Extra files" + ], + "example": "src/components/Button/index.ts" + } + ], + "explanation": "Component structure informs import paths and refactors." + }, + { + "id": "vue-state", + "question": "How do you manage global state?", + "responseKey": "stateManagement", + "answers": [ + { + "value": "pinia", + "label": "Pinia", + "docs": "https://pinia.vuejs.org/introduction.html", + "pros": [ + "Officially recommended", + "Type-first APIs" + ], + "cons": [ + "Requires plugin setup" + ], + "example": "const store = defineStore('cart', {...})", + "isDefault": true + }, + { + "value": "vuex", + "label": "Vuex", + "docs": "https://vuex.vuejs.org/", + "pros": [ + "Mature tooling", + "Time-travel devtools" + ], + "cons": [ + "Verbosity", + "Mutations boilerplate" + ], + "example": "store.commit('cart/addItem', payload)" + }, + { + "value": "composition-api", + "label": "Composition API", + "icon": "vuedotjs", + "docs": "https://vuejs.org/guide/extras/composition-api-faq.html", + "pros": [ + "No external dependency", + "Tree-shakeable" + ], + "cons": [ + "Manual patterns" + ], + "example": "const count = useCounterStore()" + } + ], + "explanation": "State strategy affects reactivity helpers and file imports." + }, + { + "id": "vue-testing", + "question": "Which Vue testing toolkit do you prefer?", + "responseKey": "testingUT", + "answers": [ + { + "value": "vitest", + "label": "Vitest + Vue Testing Library", + "icon": "/icons/vitest.svg", + "docs": "https://vitest.dev/guide/", + "pros": [ + "Fast runs", + "Great DX" + ], + "cons": [ + "Newer ecosystem" + ], + "example": "import { render } from '@testing-library/vue'", + "isDefault": true + }, + { + "value": "jest", + "label": "Jest + Vue Test Utils", + "icon": "/icons/jest.svg", + "docs": "https://test-utils.vuejs.org/", + "pros": [ + "Battle tested", + "Wide plugin support" + ], + "cons": [ + "Slower cold start" + ], + "example": "const wrapper = shallowMount(MyComponent)" + } + ], + "explanation": "Testing stack drives available helpers and globals." + } +] diff --git a/data/stacks.json b/data/stacks.json index 35c3bfb..b2830ba 100644 --- a/data/stacks.json +++ b/data/stacks.json @@ -4,26 +4,108 @@ "question": "Which stack are you working with?", "responseKey": "stackSelection", "isReadOnlyOnSummary": true, + "enableFilter": true, "answers": [ { "value": "react", "label": "React", "icon": "react", "docs": "https://react.dev/learn", - "isDefault": true + "isDefault": true, + "tags": [ + "frontend", + "spa", + "jsx" + ] }, { "value": "nextjs", "label": "Next.js", "icon": "nextdotjs", - "docs": "https://nextjs.org/docs" + "docs": "https://nextjs.org/docs", + "tags": [ + "react", + "ssr", + "app-router" + ] }, { "value": "angular", "label": "Angular", "icon": "angular", - "docs": "https://angular.io/docs" + "docs": "https://angular.io/docs", + "tags": [ + "frontend", + "typescript", + "spa" + ] + }, + { + "value": "python", + "label": "Python", + "icon": "python", + "docs": "https://docs.python.org/3/", + "tags": [ + "backend", + "scripting", + "language" + ] + }, + { + "value": "vue", + "label": "Vue", + "icon": "vuedotjs", + "docs": "https://vuejs.org/guide/introduction.html", + "tags": [ + "frontend", + "composition-api", + "spa" + ] + }, + { + "value": "nuxt", + "label": "Nuxt", + "icon": "nuxtdotjs", + "docs": "https://nuxt.com/docs", + "tags": [ + "vue", + "full-stack", + "ssr" + ] + }, + { + "value": "svelte", + "label": "Svelte", + "icon": "svelte", + "docs": "https://svelte.dev/docs", + "tags": [ + "frontend", + "compiler", + "lightweight" + ] + }, + { + "value": "astro", + "label": "Astro", + "icon": "astro", + "docs": "https://docs.astro.build", + "tags": [ + "islands", + "content", + "ssg" + ] + }, + { + "value": "remix", + "label": "Remix", + "icon": "remix", + "docs": "https://remix.run/docs", + "tags": [ + "react", + "routing", + "full-stack" + ] } ] } -] +] \ No newline at end of file diff --git a/file-templates/agents-template.md b/file-templates/agents-template.md index 05e4192..d22f8e9 100644 --- a/file-templates/agents-template.md +++ b/file-templates/agents-template.md @@ -12,7 +12,13 @@ This guide provides conventions and best practices for building AI agent applica --- -## 2. Development Standards +## 2. Stack Playbook + +{{stackGuidance}} + +--- + +## 3. Development Standards ### Code Organization - File structure: **{{fileStructure}}** @@ -37,7 +43,7 @@ This guide provides conventions and best practices for building AI agent applica --- -## 3. Agent-Specific Patterns +## 4. Agent-Specific Patterns ### State & Memory - State handling: **{{stateManagement}}** @@ -60,7 +66,7 @@ This guide provides conventions and best practices for building AI agent applica --- -## 4. Collaboration & Git +## 5. Collaboration & Git ### Version Control - Commit style: **{{commitStyle}}** diff --git a/file-templates/copilot-instructions-template.md b/file-templates/copilot-instructions-template.md index b1f13cb..ac0e0d6 100644 --- a/file-templates/copilot-instructions-template.md +++ b/file-templates/copilot-instructions-template.md @@ -22,7 +22,13 @@ Regenerate whenever your JSON configuration changes (stack, naming, testing, etc --- -## 2. Naming, Style & Structure Rules +## 2. Stack Playbook + +{{stackGuidance}} + +--- + +## 3. Naming, Style & Structure Rules ### Naming & Exports - Variables, functions, object keys: **{{variableNaming}}** @@ -39,11 +45,12 @@ Regenerate whenever your JSON configuration changes (stack, naming, testing, etc - API layer organization: **{{apiLayer}}** - Folder strategy: **{{folders}}** + > Copilot should not generate code outside these structures or naming patterns. --- -## 3. Testing & Quality Assurance +## 4. Testing & Quality Assurance - Unit tests: **{{testingUT}}** - E2E / integration: **{{testingE2E}}** @@ -56,7 +63,7 @@ Regenerate whenever your JSON configuration changes (stack, naming, testing, etc --- -## 4. Performance & Data Loading +## 5. Performance & Data Loading - Data fetching: **{{dataFetching}}** - React performance optimizations: **{{reactPerf}}** @@ -72,7 +79,7 @@ Regenerate whenever your JSON configuration changes (stack, naming, testing, etc --- -## 5. Security, Validation, Logging +## 6. Security, Validation, Logging - Secrets/auth handling: **{{auth}}** - Input validation: **{{validation}}** @@ -86,7 +93,7 @@ Regenerate whenever your JSON configuration changes (stack, naming, testing, etc --- -## 6. Commit & PR Conventions +## 7. Commit & PR Conventions - Commit style: **{{commitStyle}}** - PR rules: **{{prRules}}** @@ -103,7 +110,7 @@ Regenerate whenever your JSON configuration changes (stack, naming, testing, etc --- -## 7. Copilot Usage Guidance +## 8. Copilot Usage Guidance - Use Copilot for boilerplate (hooks, component scaffolds). - Provide context in comments/prompts. @@ -118,7 +125,7 @@ Regenerate whenever your JSON configuration changes (stack, naming, testing, etc --- -## 8. Editor Setup +## 9. Editor Setup Recommended editor configuration: @@ -132,7 +139,7 @@ Recommended editor configuration: --- -## 9. Caveats & Overrides +## 10. Caveats & Overrides - Document exceptions with comments. - Experimental features must be flagged. diff --git a/hooks/__tests__/use-answer-filter.test.tsx b/hooks/__tests__/use-answer-filter.test.tsx new file mode 100644 index 0000000..d28ce69 --- /dev/null +++ b/hooks/__tests__/use-answer-filter.test.tsx @@ -0,0 +1,59 @@ +import { act, renderHook } from "@testing-library/react" +import { describe, expect, it } from "vitest" + +import { useAnswerFilter } from "@/hooks/use-answer-filter" +import type { WizardQuestion } from "@/types/wizard" + +const buildQuestion = (overrides: Partial = {}): WizardQuestion => ({ + id: "stackSelection", + question: "Which stack are you using?", + enableFilter: true, + answers: [ + { value: "react", label: "React", tags: ["frontend", "spa"] }, + { value: "vue", label: "Vue", tags: ["frontend", "composition"] }, + { value: "python", label: "Python", tags: ["backend", "language"] }, + ], + ...overrides, +}) + +describe("useAnswerFilter", () => { + it("returns all answers when filter disabled", () => { + const question = buildQuestion({ enableFilter: false }) + const { result } = renderHook(() => useAnswerFilter(question)) + + expect(result.current.answers).toHaveLength(3) + expect(result.current.isFiltering).toBe(false) + }) + + it("filters answers by label, value, or tags", () => { + const question = buildQuestion() + const { result } = renderHook(() => useAnswerFilter(question)) + + expect(result.current.answers.map((answer) => answer.value)).toEqual(["react", "vue", "python"]) + + act(() => { + result.current.setQuery("vue") + }) + + expect(result.current.isFiltering).toBe(true) + expect(result.current.answers.map((answer) => answer.value)).toEqual(["vue"]) + + act(() => { + result.current.setQuery("front") + }) + + expect(result.current.answers.map((answer) => answer.value)).toEqual(["react", "vue"]) + }) + + it("returns empty list when no matches", () => { + const question = buildQuestion() + const { result } = renderHook(() => useAnswerFilter(question)) + + act(() => { + result.current.setQuery("svelte") + }) + + expect(result.current.answers).toHaveLength(0) + expect(result.current.isFiltering).toBe(true) + }) +}) diff --git a/hooks/use-answer-filter.ts b/hooks/use-answer-filter.ts new file mode 100644 index 0000000..846f4f1 --- /dev/null +++ b/hooks/use-answer-filter.ts @@ -0,0 +1,73 @@ +import { useEffect, useMemo, useState } from "react" + +import type { WizardAnswer, WizardQuestion } from "@/types/wizard" + +type UseAnswerFilterResult = { + query: string + setQuery: (value: string) => void + answers: WizardAnswer[] + isFiltering: boolean +} + +const buildSearchableText = (answer: WizardAnswer) => { + const parts = [answer.label, answer.value] + + if (answer.tags && answer.tags.length > 0) { + parts.push(answer.tags.join(" ")) + } + + if (answer.example) { + parts.push(answer.example) + } + + return parts + .filter((part) => typeof part === "string" && part.length > 0) + .join(" ") + .toLowerCase() +} + +export function useAnswerFilter(question: WizardQuestion | null): UseAnswerFilterResult { + const [query, setQuery] = useState("") + + useEffect(() => { + setQuery("") + }, [question?.id]) + + const answers = useMemo(() => { + if (!question) { + return [] + } + + if (!question.enableFilter) { + return question.answers + } + + const trimmed = query.trim().toLowerCase() + + if (trimmed.length === 0) { + return question.answers + } + + return question.answers.filter((answer) => { + const searchable = buildSearchableText(answer) + return searchable.includes(trimmed) + }) + }, [question, query]) + + const isFiltering = Boolean(question?.enableFilter && query.trim().length > 0) + + return { + query, + setQuery, + answers, + isFiltering, + } +} + +export function buildFilterPlaceholder(question: WizardQuestion | null) { + if (!question?.enableFilter) { + return "" + } + + return `Filter ${question.answers.length} options` +} diff --git a/lib/__tests__/stack-guidance.test.ts b/lib/__tests__/stack-guidance.test.ts new file mode 100644 index 0000000..cd94e1c --- /dev/null +++ b/lib/__tests__/stack-guidance.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from 'vitest' + +import { getStackGuidance, supportedGuidanceStacks } from '@/lib/stack-guidance' + +describe('stack-guidance', () => { + it('returns default guidance when stack is missing', () => { + const guidance = getStackGuidance() + expect(guidance).toContain('- Document your preferred architecture') + }) + + it('returns guidance for each supported stack', () => { + supportedGuidanceStacks.forEach((slug) => { + const guidance = getStackGuidance(slug) + expect(guidance).toMatch(/^-/) + expect(guidance.length).toBeGreaterThan(20) + }) + }) + + it('falls back to default for unknown stacks', () => { + const guidance = getStackGuidance('unknown-stack') + expect(guidance).toContain('Document your preferred architecture') + }) +}) diff --git a/lib/__tests__/template-config.test.ts b/lib/__tests__/template-config.test.ts index 8add509..93620a7 100644 --- a/lib/__tests__/template-config.test.ts +++ b/lib/__tests__/template-config.test.ts @@ -121,9 +121,22 @@ describe('template-config', () => { 'copilot-instructions', 'copilot-instructions-react', 'copilot-instructions-nextjs', + 'copilot-instructions-angular', + 'copilot-instructions-vue', + 'copilot-instructions-nuxt', + 'copilot-instructions-svelte', + 'copilot-instructions-astro', + 'copilot-instructions-remix', + 'copilot-instructions-python', 'agents', 'agents-react', + 'agents-angular', 'agents-python', + 'agents-vue', + 'agents-nuxt', + 'agents-svelte', + 'agents-astro', + 'agents-remix', 'cursor-rules', 'json-rules', 'instructions-md' diff --git a/lib/icon-utils.ts b/lib/icon-utils.ts index 0f10c08..f5b4d9a 100644 --- a/lib/icon-utils.ts +++ b/lib/icon-utils.ts @@ -63,7 +63,7 @@ const getSimpleIconMarkup = (icon: SimpleIcon) => { return markup } -const normalizeHex = (hex: string) => { +export const normalizeHex = (hex: string) => { const trimmed = hex.trim().replace(/^#/, "") if (trimmed.length === 3) { return trimmed @@ -102,7 +102,7 @@ export const getAccessibleIconColor = (hex: string) => { return `#${normalized}` } -const normalizeIconSlug = (raw?: string) => { +export const normalizeIconSlug = (raw?: string) => { if (!raw) { return null } diff --git a/lib/stack-guidance.ts b/lib/stack-guidance.ts new file mode 100644 index 0000000..1b1f60d --- /dev/null +++ b/lib/stack-guidance.ts @@ -0,0 +1,66 @@ +const asMarkdownList = (lines: string[]) => lines.map((line) => `- ${line}`).join("\n") + +const stackGuidanceBySlug: Record = { + default: asMarkdownList([ + "Document your preferred architecture and reference this file before accepting AI suggestions.", + "Highlight coding patterns that save review time for your team.", + "Call out testing expectations so automated output stays production-ready.", + ]), + react: asMarkdownList([ + "Prefer modern function components with Hooks; avoid new class components.", + "Use React Testing Library + Jest for unit coverage when possible.", + "Reach for Context or dedicated state libraries instead of prop drilling complex data.", + ]), + nextjs: asMarkdownList([ + "Clarify if routes belong in the App Router and whether they run on the server or client.", + "Use built-in data fetching helpers (Server Components, Route Handlers, Server Actions) before custom fetch logic.", + "Keep shared UI and server utilities in clearly named directories to support bundler boundaries.", + ]), + angular: asMarkdownList([ + "Favor standalone components and provide module guidance if legacy NgModules remain.", + "Leverage Angular's dependency injection and RxJS patterns rather than ad-hoc state management.", + "Keep schematics and CLI commands documented for generating new features consistently.", + ]), + vue: asMarkdownList([ + "Specify when to use the Composition API versus Options API within new components.", + "Pinia should be the default store unless a legacy Vuex module is explicitly required.", + "Encourage single-file components with script/setup for new work unless otherwise noted.", + ]), + nuxt: asMarkdownList([ + "Indicate default rendering mode (SSR, SSG, ISR) and when to override it per route.", + "Rely on `useFetch` and server routes before introducing ad-hoc API clients.", + "Document deployment adapters (Vercel, Netlify, Node) so build output matches hosting.", + ]), + svelte: asMarkdownList([ + "Prefer SvelteKit for routing and server endpoints unless a standalone widget is needed.", + "Use stores for shared state and highlight when external libraries such as Zustand are acceptable.", + "Note styling defaults (scoped styles, Tailwind, Vanilla Extract) to keep components consistent.", + ]), + astro: asMarkdownList([ + "Clarify which framework powers interactive islands (React, Vue, Svelte).", + "State the default rendering strategy (SSG, SSR, ISR) and when to opt into alternatives.", + "Document where content collections live and how frontmatter schema is enforced.", + ]), + remix: asMarkdownList([ + "Loaders and actions should remain the default data flow; document when client fetches are acceptable.", + "Capture which runtime the project targets (Vercel Edge, Fly.io, Express) and needed adapters.", + "Keep links, forms, and nested routes aligned with Remix conventions to benefit from built-in optimizations.", + ]), + python: asMarkdownList([ + "Call out whether FastAPI, Django, or Flask is the project's default framework.", + "Define typing expectations (mypy, Ruff, or dynamic) to keep contributions consistent.", + "Describe package management commands (Poetry, pip-tools, uv) for installing and locking dependencies.", + ]), +} + +export const getStackGuidance = (stack?: string) => { + if (!stack) { + return stackGuidanceBySlug.default + } + + const normalized = stack.trim().toLowerCase() + + return stackGuidanceBySlug[normalized] ?? stackGuidanceBySlug.default +} + +export const supportedGuidanceStacks = Object.keys(stackGuidanceBySlug).filter((slug) => slug !== "default") diff --git a/lib/template-config.ts b/lib/template-config.ts index 6c17174..bfe87ff 100644 --- a/lib/template-config.ts +++ b/lib/template-config.ts @@ -30,6 +30,36 @@ export const templateCombinations: Record = { template: 'copilot-instructions-template.md', outputFileName: 'copilot-instructions.md', }, + // Copilot Instructions + Vue (specific combination) + 'copilot-instructions-vue': { + template: 'copilot-instructions-template.md', + outputFileName: 'copilot-instructions.md', + }, + // Copilot Instructions + Nuxt (specific combination) + 'copilot-instructions-nuxt': { + template: 'copilot-instructions-template.md', + outputFileName: 'copilot-instructions.md', + }, + // Copilot Instructions + Svelte (specific combination) + 'copilot-instructions-svelte': { + template: 'copilot-instructions-template.md', + outputFileName: 'copilot-instructions.md', + }, + // Copilot Instructions + Astro (specific combination) + 'copilot-instructions-astro': { + template: 'copilot-instructions-template.md', + outputFileName: 'copilot-instructions.md', + }, + // Copilot Instructions + Remix (specific combination) + 'copilot-instructions-remix': { + template: 'copilot-instructions-template.md', + outputFileName: 'copilot-instructions.md', + }, + // Copilot Instructions + Python (specific combination) + 'copilot-instructions-python': { + template: 'copilot-instructions-template.md', + outputFileName: 'copilot-instructions.md', + }, // Agents guide (general) agents: { template: 'agents-template.md', @@ -50,6 +80,31 @@ export const templateCombinations: Record = { template: 'agents-template.md', outputFileName: 'agents.md', }, + // Agents guide + Vue (specific combination) + 'agents-vue': { + template: 'agents-template.md', + outputFileName: 'agents.md', + }, + // Agents guide + Nuxt (specific combination) + 'agents-nuxt': { + template: 'agents-template.md', + outputFileName: 'agents.md', + }, + // Agents guide + Svelte (specific combination) + 'agents-svelte': { + template: 'agents-template.md', + outputFileName: 'agents.md', + }, + // Agents guide + Astro (specific combination) + 'agents-astro': { + template: 'agents-template.md', + outputFileName: 'agents.md', + }, + // Agents guide + Remix (specific combination) + 'agents-remix': { + template: 'agents-template.md', + outputFileName: 'agents.md', + }, // Cursor rules 'cursor-rules': { template: 'cursor-rules-template.json', diff --git a/lib/wizard-utils.ts b/lib/wizard-utils.ts index c901c21..37455b6 100644 --- a/lib/wizard-utils.ts +++ b/lib/wizard-utils.ts @@ -50,6 +50,7 @@ export const buildStepFromQuestionSet = ( allowMultiple: question.allowMultiple, responseKey: question.responseKey, isReadOnlyOnSummary: question.isReadOnlyOnSummary, + enableFilter: question.enableFilter, answers: question.answers.map(mapAnswerSourceToWizard), })), }) diff --git a/types/wizard.ts b/types/wizard.ts index 378ab3f..9acab45 100644 --- a/types/wizard.ts +++ b/types/wizard.ts @@ -21,6 +21,7 @@ export type DataQuestionSource = { allowMultiple?: boolean responseKey?: string isReadOnlyOnSummary?: boolean + enableFilter?: boolean answers: DataAnswerSource[] } @@ -57,6 +58,7 @@ export type WizardQuestion = { allowMultiple?: boolean responseKey?: string isReadOnlyOnSummary?: boolean + enableFilter?: boolean answers: WizardAnswer[] } @@ -71,6 +73,7 @@ export type WizardConfirmationIntent = "change-file" | "reset" export type InstructionsWizardProps = { selectedFileId?: string | null onClose?: () => void + initialStackId?: string | null } export type Responses = Record