Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 25 additions & 16 deletions app/existing/[repoUrl]/repo-scan-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,26 +112,26 @@ export default function RepoScanClient({ initialRepoUrl }: RepoScanClientProps)
}))
}, [scanResult])

const handleStartScan = () => {
if (!repoUrlForScan) {
return
}

setHasConfirmed(true)
setScanToken((token) => token + 1)
const handleStartScan = () => {
if (!repoUrlForScan) {
return
}

track(ANALYTICS_EVENTS.REPO_SCAN_START, { repo: repoUrlForScan })
}
setHasConfirmed(true)
setScanToken((token) => token + 1)

const handleRetryScan = () => {
if (!repoUrlForScan) {
return
track(ANALYTICS_EVENTS.REPO_SCAN_START, { repo: repoUrlForScan })
}

setScanToken((token) => token + 1)
const handleRetryScan = () => {
if (!repoUrlForScan) {
return
}

track(ANALYTICS_EVENTS.REPO_SCAN_RETRY, { repo: repoUrlForScan })
}
setScanToken((token) => token + 1)

track(ANALYTICS_EVENTS.REPO_SCAN_RETRY, { repo: repoUrlForScan })
}

const warnings = scanResult?.warnings ?? []
const stackMeta = scanResult?.conventions ?? null
Expand Down Expand Up @@ -372,7 +372,16 @@ export default function RepoScanClient({ initialRepoUrl }: RepoScanClientProps)
disabled={busy}
className="flex h-[36px] items-center rounded-full px-4 py-0 text-sm"
>
{busy ? `Generating ${file.filename}…` : `Generate ${file.filename}`}
<div className="flex items-center gap-2">
<span>
{busy ? `Generating ${file.filename}…` : `Generate ${file.filename}`}
</span>
{file.isLegacy && (
<span className="rounded-full bg-amber-950/30 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider text-white">
Legacy
</span>
)}
</div>
</Button>
)
})}
Expand Down
13 changes: 10 additions & 3 deletions app/new/stack/stack-summary-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -388,9 +388,16 @@ export function StackSummaryPage({ stackId, mode }: StackSummaryPageProps) {
disabled={isGenerating}
className="flex h-[38px] min-w-[190px] items-center justify-center rounded-full px-6 py-0 text-base leading-none shadow-lg shadow-primary/20"
>
<span className="text-sm font-semibold text-foreground">
{isGenerating ? `Generating ${file.filename}…` : `Generate ${file.filename}`}
</span>
<div className="flex items-center gap-2">
<span className="text-sm font-semibold text-foreground">
{isGenerating ? `Generating ${file.filename}…` : `Generate ${file.filename}`}
</span>
{file.isLegacy && (
<span className="rounded-full bg-amber-950/30 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider text-white">
Legacy
</span>
)}
</div>
</Button>
)
})}
Expand Down
42 changes: 41 additions & 1 deletion components/final-output-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,51 @@ export default function FinalOutputView({ fileName, fileContent, mimeType, onClo
}, COPY_RESET_DELAY)
}, [currentContent])

const handleDownloadClick = useCallback(() => {
const handleDownloadClick = useCallback(async () => {
if (!currentContent) {
return
}

if (normalizedFileName.endsWith('.zip')) {
try {
const { default: JSZip } = await import('jszip')
const zip = new JSZip()

// Split combined content by the FILE delimiter
const parts = currentContent.split('--- FILE: ')
let foundFiles = false

for (const part of parts) {
if (!part.trim()) continue

const lines = part.split('\n')
const firstLine = lines[0].trim()
if (firstLine.endsWith(' ---')) {
const fileName = firstLine.replace(' ---', '').trim()
const content = lines.slice(1).join('\n').trim()
zip.file(fileName, content)
foundFiles = true
}
}

if (foundFiles) {
const blob = await zip.generateAsync({ type: 'blob' })
const url = URL.createObjectURL(blob)
const link = document.createElement("a")
link.href = url
link.download = normalizedFileName
link.style.display = "none"
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
return
}
} catch (error) {
console.error('Failed to generate ZIP:', error)
}
}

const downloadMimeType = mimeType ?? "text/plain;charset=utf-8"
const blob = new Blob([currentContent], { type: downloadMimeType })
const url = URL.createObjectURL(blob)
Expand Down
17 changes: 13 additions & 4 deletions data/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,22 @@
"enabled": true
},
{
"value": "cursor-rules",
"label": "Cursor Rules",
"value": "cursor-rules-legacy",
"label": "Cursor Rules (JSON)",
"filename": ".cursor/rules",
"format": "json",
"docs": "https://docs.cursor.com/workflows/rules",
"docs": "https://docs.cursor.com/context/rules",
"enabled": true,
"isLegacy": true
},
{
"value": "cursor-rules",
"label": "Cursor Rules (.mdc + ZIP)",
"filename": "cursor-rules.zip",
"format": "zip",
"docs": "https://cursor.com/docs/context/rules",
"enabled": true
}
]
}
]
]
79 changes: 79 additions & 0 deletions file-templates/cursor-rules-template-mdc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
--- FILE: .cursor/rules/tech-stack.mdc ---
---
description: Application core tech stack and framework guidance
globs: ["**/*"]
alwaysApply: true
---

# Tech Stack: {{stackSelection}}

- **Language**: {{language}}
- **Framework**: {{stackSelection}}
- **Tooling**: {{tooling}}

## Core Guidance
{{stackGuidance}}

--- FILE: .cursor/rules/naming.mdc ---
---
description: Naming conventions for variables, files, and components
globs: ["**/*.{ts,tsx,js,jsx,py,go,rs}"]
alwaysApply: false
---

# Naming Conventions

- **Variables & Functions**: {{variableNaming}}
- **Files & Modules**: {{fileNaming}}
- **Components & Types**: {{componentNaming}}
- **Exports**: {{exports}}

--- FILE: .cursor/rules/testing.mdc ---
---
description: Testing strategy and quality assurance
globs: ["**/*.test.{ts,tsx,js,jsx}", "**/*_test.go", "**/test_*.py", "**/__tests__/**/*"]
alwaysApply: false
---

# Testing Strategy

- **Unit Testing**: {{testingUT}}
- **E2E Testing**: {{testingE2E}}

## Requirements
- Use descriptive test names.
- Cover both success and failure cases.
- Place tests in appropriate directories based on project structure.

--- FILE: .cursor/rules/security.mdc ---
---
description: Security, validation, and logging rules
globs: ["**/*"]
alwaysApply: false
---

# Security & Quality

- **Auth & Secrets**: {{auth}}
- **Validation**: {{validation}}
- **Logging**: {{logging}}

## Rules
- Never commit secrets to version control.
- Validate all external data inputs.
- Use structured logging; avoid logging sensitive information.

--- FILE: .cursor/rules/collaboration.mdc ---
---
description: Commit messages and PR conventions
globs: [".git/**/*", "**/*"]
alwaysApply: false
---

# Collaboration

- **Commit Style**: {{commitStyle}}
- **PR Rules**: {{prRules}}
- **Collaboration Style**: {{collaboration}}

Follow project-specific conventions for small, focused changes.
13 changes: 11 additions & 2 deletions lib/__tests__/template-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ describe('template-config', () => {

it('should return correct config for cursor rules', () => {
const result = getTemplateConfig('cursor-rules')
expect(result).toEqual({
template: 'cursor-rules-template-mdc.md',
outputFileName: 'cursor-rules.zip'
})
})

it('should return correct config for legacy cursor rules', () => {
const result = getTemplateConfig('cursor-rules-legacy')
expect(result).toEqual({
template: 'cursor-rules-template.json',
outputFileName: '.cursor/rules'
Expand Down Expand Up @@ -109,8 +117,8 @@ describe('template-config', () => {
}
const result = getTemplateConfig(key)
expect(result).toEqual({
template: 'cursor-rules-template.json',
outputFileName: '.cursor/rules',
template: 'cursor-rules-template-mdc.md',
outputFileName: 'cursor-rules.zip',
})
})
})
Expand Down Expand Up @@ -138,6 +146,7 @@ describe('template-config', () => {
'agents-astro',
'agents-remix',
'cursor-rules',
'cursor-rules-legacy',
'json-rules',
'instructions-md'
]
Expand Down
2 changes: 1 addition & 1 deletion lib/scan-generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { ANALYTICS_EVENTS } from "@/lib/analytics-events"

const fileOptions = getFileOptions()

export type OutputFileId = "instructions-md" | "agents-md" | "cursor-rules"
export type OutputFileId = "instructions-md" | "agents-md" | "cursor-rules" | "cursor-rules-legacy"

export async function generateFromRepoScan(
scan: RepoScanSummary,
Expand Down
9 changes: 7 additions & 2 deletions lib/template-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,16 @@ export const templateCombinations: Record<string, TemplateConfig> = {
template: 'agents-template.md',
outputFileName: 'agents.md',
},
// Cursor rules
'cursor-rules': {
// Cursor rules (Legacy)
'cursor-rules-legacy': {
template: 'cursor-rules-template.json',
outputFileName: '.cursor/rules',
},
// New Cursor rules (MDC + ZIP)
'cursor-rules': {
template: 'cursor-rules-template-mdc.md',
outputFileName: 'cursor-rules.zip',
},
// Generic JSON rules (placeholder)
'json-rules': {
template: 'copilot-instructions-template.md',
Expand Down
9 changes: 4 additions & 5 deletions lib/template-render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,13 @@ export async function renderTemplate({
}

const replaceVariable = (key: keyof WizardResponses, fallback = 'Not specified') => {
const placeholder = `{{${String(key)}}}`
const placeholder = new RegExp(`{{${String(key)}}}`, 'g')

if (!generatedContent.includes(placeholder)) {
if (!template.includes(`{{${String(key)}}}`)) {
return
}

const value = responses[key]

const defaultMeta = defaultedResponses?.[key]

if (value === null || value === undefined || value === '') {
Expand Down Expand Up @@ -149,8 +148,8 @@ export async function renderTemplate({
replaceVariable('outputFile')

const replaceStaticPlaceholder = (placeholderKey: string, value: string) => {
const placeholder = `{{${placeholderKey}}}`
if (!generatedContent.includes(placeholder)) {
const placeholder = new RegExp(`{{${placeholderKey}}}`, 'g')
if (!template.includes(`{{${placeholderKey}}}`)) {
return
}
const replacement = isJsonTemplate ? escapeForJson(value) : value
Expand Down
27 changes: 15 additions & 12 deletions lib/wizard-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,18 +65,18 @@ export const buildStepFromQuestionSet = (
title: string,
questions: DataQuestionSource[]
): WizardStep => ({
id,
title,
questions: questions.map((question) => ({
id: question.id,
question: question.question,
allowMultiple: question.allowMultiple,
responseKey: question.responseKey,
isReadOnlyOnSummary: question.isReadOnlyOnSummary,
enableFilter: question.enableFilter,
answers: question.answers.map(mapAnswerSourceToWizard),
freeText: question.freeText,
})),
id,
title,
questions: questions.map((question) => ({
id: question.id,
question: question.question,
allowMultiple: question.allowMultiple,
responseKey: question.responseKey,
isReadOnlyOnSummary: question.isReadOnlyOnSummary,
enableFilter: question.enableFilter,
answers: question.answers.map(mapAnswerSourceToWizard),
freeText: question.freeText,
})),
})

export const buildFileOptionsFromQuestion = (
Expand All @@ -98,19 +98,22 @@ export const buildFileOptionsFromQuestion = (
icon: answer.icon,
docs: answer.docs,
isDefault: answer.isDefault,
isLegacy: answer.isLegacy,
}))
}

const formatLabelMap: Record<string, string> = {
markdown: "Markdown",
json: "JSON",
"cursor-rules-json": "JSON",
zip: "ZIP Archive",
}

const formatMimeTypeMap: Record<string, string> = {
markdown: "text/markdown",
json: "application/json",
"cursor-rules-json": "application/json",
zip: "application/zip",
}

/**
Expand Down
Loading