Skip to content
Draft
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
1 change: 1 addition & 0 deletions app/[userId]/[ruleId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface CursorRule {
id: string
title: string
content: string
type: string
ruleType: string
views: number
createdAt: string
Expand Down
11 changes: 7 additions & 4 deletions app/api/cursor-rules/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export async function GET(request: NextRequest) {
id: cursorRule.id,
title: cursorRule.title,
content: cursorRule.content,
type: cursorRule.type,
ruleType: cursorRule.ruleType,
isPublic: cursorRule.isPublic,
views: cursorRule.views,
Expand Down Expand Up @@ -121,7 +122,7 @@ export async function POST(request: NextRequest) {
}

const body = await request.json()
const { title, content, ruleType = "always", isPublic = false } = body
const { title, content, type = "rule", ruleType = "always", isPublic = false } = body

if (!title || !content) {
return NextResponse.json({ error: "Title and content are required" }, { status: 400 })
Expand All @@ -135,14 +136,15 @@ export async function POST(request: NextRequest) {
userId: session.user.id,
title,
content,
type,
ruleType,
isPublic,
views: 0,
createdAt: now,
updatedAt: now,
}).returning()

await track('Rule Created', { ruleId: id, isPublic, ruleType })
await track('Rule Created', { ruleId: id, isPublic, type, ruleType })
return NextResponse.json(newRule)
} catch (error) {
console.error("Error creating cursor rule:", error)
Expand Down Expand Up @@ -182,7 +184,7 @@ export async function PUT(request: NextRequest) {
}

const body = await request.json()
const { id, title, content, ruleType, isPublic } = body
const { id, title, content, type, ruleType, isPublic } = body

if (!id || !title || !content) {
return NextResponse.json({ error: "ID, title and content are required" }, { status: 400 })
Expand All @@ -193,6 +195,7 @@ export async function PUT(request: NextRequest) {
.set({
title,
content,
type,
ruleType,
isPublic,
updatedAt: new Date(),
Expand All @@ -207,7 +210,7 @@ export async function PUT(request: NextRequest) {
return NextResponse.json({ error: "Rule not found or unauthorized" }, { status: 404 })
}

await track('Rule Updated', { ruleId: id, isPublic: Boolean(isPublic), ruleType })
await track('Rule Updated', { ruleId: id, isPublic: Boolean(isPublic), type, ruleType })
return NextResponse.json(updatedRule)
} catch (error) {
console.error("Error updating cursor rule:", error)
Expand Down
1 change: 1 addition & 0 deletions app/api/feed/hot/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export async function GET(request: NextRequest) {
id: cursorRule.id,
title: cursorRule.title,
content: cursorRule.content,
type: cursorRule.type,
ruleType: cursorRule.ruleType,
views: cursorRule.views,
createdAt: cursorRule.createdAt,
Expand Down
1 change: 1 addition & 0 deletions app/api/feed/new/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export async function GET(request: NextRequest) {
id: cursorRule.id,
title: cursorRule.title,
content: cursorRule.content,
type: cursorRule.type,
ruleType: cursorRule.ruleType,
views: cursorRule.views,
createdAt: cursorRule.createdAt,
Expand Down
1 change: 1 addition & 0 deletions app/api/my-rules/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export async function GET(request: NextRequest) {
id: cursorRule.id,
title: cursorRule.title,
content: cursorRule.content,
type: cursorRule.type,
ruleType: cursorRule.ruleType,
isPublic: cursorRule.isPublic,
views: cursorRule.views,
Expand Down
1 change: 1 addition & 0 deletions app/api/public-rule/[userId]/[ruleId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export async function GET(
id: cursorRule.id,
title: cursorRule.title,
content: cursorRule.content,
type: cursorRule.type,
ruleType: cursorRule.ruleType,
views: cursorRule.views,
createdAt: cursorRule.createdAt,
Expand Down
9 changes: 7 additions & 2 deletions app/api/registry/[ruleId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export async function GET(
id: cursorRule.id,
title: cursorRule.title,
content: cursorRule.content,
type: cursorRule.type,
ruleType: cursorRule.ruleType,
userId: cursorRule.userId,
user: {
Expand All @@ -36,15 +37,19 @@ export async function GET(
}

// Generate universal registry item with content
const isCommand = rule.type === 'command';
const directory = isCommand ? 'commands' : 'rules';
const extension = isCommand ? 'md' : 'mdc';

const registryItem = {
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": rule.title,
"type": "registry:item", // Changed to registry:item for universal items
"files": [
{
"path": `cursor.link/rules/${rule.title}.mdc`, // Source path (not used but required)
"path": `cursor.link/${directory}/${rule.title}.${extension}`, // Source path (not used but required)
"type": "registry:file",
"target": `~/.cursor/rules/${rule.title}.mdc`, // Explicit target makes it universal
"target": `~/.cursor/${directory}/${rule.title}.${extension}`, // Explicit target makes it universal
"content": rule.content // Include the actual content
}
]
Expand Down
8 changes: 5 additions & 3 deletions app/api/registry/rules/[ruleId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export async function GET(
id: cursorRule.id,
title: cursorRule.title,
content: cursorRule.content,
type: cursorRule.type,
ruleType: cursorRule.ruleType,
})
.from(cursorRule)
Expand All @@ -30,20 +31,21 @@ export async function GET(
return NextResponse.json({ error: "Rule file not found" }, { status: 404 })
}

// Generate .mdc file content dynamically
const mdcContent = `---
// Generate file content dynamically based on type
const fileContent = `---
description: ${rule.title}
globs:
alwaysApply: ${rule.ruleType === 'always' ? 'true' : 'false'}
---

${rule.content}`

return new NextResponse(mdcContent, {
return new NextResponse(fileContent, {
status: 200,
headers: {
"Content-Type": "text/markdown",
"Cache-Control": "public, max-age=300, s-maxage=300", // 5 minute cache
"Content-Disposition": `attachment; filename="${rule.title}.${rule.type === 'command' ? 'md' : 'mdc'}"`,
},
})
} catch (error) {
Expand Down
1 change: 1 addition & 0 deletions app/api/rule/[slug]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export async function GET(
id: cursorRule.id,
title: cursorRule.title,
content: cursorRule.content,
type: cursorRule.type,
ruleType: cursorRule.ruleType,
isPublic: cursorRule.isPublic,
views: cursorRule.views,
Expand Down
8 changes: 8 additions & 0 deletions app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ interface CursorRule {
id: string
title: string
content: string
type: string
ruleType: string
isPublic: boolean
views: number
Expand Down Expand Up @@ -442,6 +443,13 @@ export default function DashboardPage() {
<h3 className="font-medium text-white text-sm truncate">
{rule.title}
</h3>
<span className={`px-2 py-0.5 text-xs rounded-full flex-shrink-0 ${
rule.type === 'command'
? 'bg-purple-500/10 text-purple-400'
: 'bg-blue-500/10 text-blue-400'
}`}>
{rule.type === 'command' ? 'Command' : 'Rule'}
</span>
<button
onClick={(e) => {
e.stopPropagation()
Expand Down
8 changes: 8 additions & 0 deletions app/feed/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface CursorRule {
id: string
title: string
content: string
type: string
ruleType: string
views: number
createdAt: string
Expand Down Expand Up @@ -284,6 +285,13 @@ export default function FeedPage() {
<h3 className="font-medium text-white text-sm group-hover:text-[#70A7D7] transition-colors line-clamp-2">
{rule.title}
</h3>
<span className={`px-2 py-0.5 text-xs rounded-full flex-shrink-0 ${
rule.type === 'command'
? 'bg-purple-500/10 text-purple-400'
: 'bg-blue-500/10 text-blue-400'
}`}>
{rule.type === 'command' ? 'Command' : 'Rule'}
</span>
</div>
<p className="text-xs text-gray-400 mb-1 line-clamp-2 group-hover:text-gray-300 transition-colors">
{firstLine(rule.content)}
Expand Down
46 changes: 43 additions & 3 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ function HomePage() {
defaultValue: "false",
shallow: true
})
const [urlType, setUrlType] = useQueryState("type", {
defaultValue: "rule",
shallow: true
})
const [urlEditId, setUrlEditId] = useQueryState("editId", {
defaultValue: "",
shallow: true
Expand All @@ -52,6 +56,7 @@ function HomePage() {
const [tokenCount, setTokenCount] = useState(0)
const [isDropdownOpen, setIsDropdownOpen] = useState(false)
const [selectedRule, setSelectedRule] = useState(urlRuleType || "always")
const [selectedType, setSelectedType] = useState(urlType || "rule")
const [isSignInDialogOpen, setIsSignInDialogOpen] = useState(false)
const [signInSuccess, setSignInSuccess] = useState(false)
const [sentEmail, setSentEmail] = useState("")
Expand Down Expand Up @@ -106,6 +111,10 @@ function HomePage() {
setSelectedRule(urlRuleType || "always")
}, [urlRuleType])

useEffect(() => {
setSelectedType(urlType || "rule")
}, [urlType])

useEffect(() => {
setSavedRuleId(urlEditId || null)
const isRulePublic = urlIsPublic === "true"
Expand Down Expand Up @@ -262,6 +271,7 @@ function HomePage() {
...(savedRuleId && { id: savedRuleId }),
title: localTitle,
content: localContent,
type: selectedType,
ruleType: selectedRule,
isPublic: false
}
Expand All @@ -278,6 +288,7 @@ function HomePage() {
setSavedRuleId(rule.id)
track("Rule Saved", {
isUpdate: Boolean(savedRuleId),
type: selectedType,
ruleType: selectedRule,
titleLength: localTitle.length,
contentLength: localContent.length,
Expand Down Expand Up @@ -325,6 +336,7 @@ function HomePage() {
id: savedRuleId,
title: localTitle,
content: localContent,
type: selectedType,
ruleType: selectedRule,
isPublic: true
})
Expand All @@ -336,6 +348,7 @@ function HomePage() {
await navigator.clipboard.writeText(publicUrl)
track("Rule Shared", {
ruleId: savedRuleId,
type: selectedType,
ruleType: selectedRule,
contentLength: localContent.length,
})
Expand Down Expand Up @@ -498,17 +511,33 @@ function HomePage() {
<main className="mx-auto max-w-4xl p-4 sm:p-6">
<Header />
<div className="space-y-4">
{/* Title Input with Dropdown */}
{/* Title Input with Dropdowns */}
<div className="space-y-3">
<div className="flex items-end gap-3">
<Input
id="title"
placeholder="index.mdc"
placeholder={selectedType === "command" ? "my-command.md" : "index.mdc"}
value={localTitle}
onChange={(e) => handleTitleChange(e.target.value)}
className="h-auto py-0 bg-transparent border-0 border-b border-white/10 text-white placeholder:text-gray-500 focus:border-white/20 focus:ring-0 focus-visible:ring-0 focus-visible:ring-offset-0 text-lg font-medium rounded-none px-0 flex-1"
/>

{/* Type Selector */}
<div className="relative">
<select
value={selectedType}
onChange={(e) => {
setSelectedType(e.target.value)
setUrlType(e.target.value)
track("Type Changed", { type: e.target.value })
}}
className="px-3 py-1.5 text-sm font-medium text-white bg-[#2A2D32] border border-white/10 rounded-md hover:bg-[#34373C] transition-colors focus:outline-none focus:ring-2 focus:ring-white/20"
>
<option value="rule">Rule</option>
<option value="command">Command</option>
</select>
</div>

<div className="relative" ref={dropdownRef}>
<button
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
Expand Down Expand Up @@ -549,7 +578,18 @@ function HomePage() {
<Textarea
ref={textareaRef}
id="content"
placeholder={`# Cursor Rules
placeholder={selectedType === "command" ? `# Custom Command

This command helps with specific development tasks.

## Usage
Use this command by typing /${localTitle || 'my-command'} in Cursor.

## Description
Describe what this command does and when to use it.

## Examples
Provide examples of how this command should be used.` : `# Cursor Rules

You are an expert in TypeScript, Node.js, Next.js App Router, React, Shadcn UI, Radix UI and Tailwind.

Expand Down
1 change: 1 addition & 0 deletions app/rule/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface CursorRule {
id: string
title: string
content: string
type: string
ruleType: string
isPublic: boolean
views: number
Expand Down
16 changes: 10 additions & 6 deletions cursor-link-cli/src/commands/pull.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export async function pullCommand(options: PullOptions) {

for (const rule of selectedRules) {
const filename = generateFilename(rule.title);
const exists = fileManager.ruleFileExists(filename);
const exists = fileManager.ruleFileExists(filename, rule.type as 'rule' | 'command');
conflicts.push({ rule, exists });
}

Expand All @@ -85,7 +85,8 @@ export async function pullCommand(options: PullOptions) {
console.log(chalk.yellow(`\n⚠️ ${existingFiles.length} file(s) already exist locally:`));
existingFiles.forEach(({ rule }) => {
const filename = generateFilename(rule.title);
console.log(chalk.gray(` • ${filename}.mdc`));
const extension = rule.type === 'command' ? '.md' : '.mdc';
console.log(chalk.gray(` • ${filename}${extension}`));
});

const { overwrite } = await inquirer.prompt([
Expand Down Expand Up @@ -134,10 +135,12 @@ export async function pullCommand(options: PullOptions) {
filename,
title: rule.title,
content: rule.content,
type: rule.type as 'rule' | 'command',
alwaysApply,
});

pullSpinner.succeed(chalk.green(`Saved "${rule.title}" → ${filename}.mdc`));
const extension = rule.type === 'command' ? '.md' : '.mdc';
pullSpinner.succeed(chalk.green(`Saved "${rule.title}" → ${filename}${extension}`));
results.success++;
} catch (error: any) {
pullSpinner.fail(chalk.red(`Failed to save "${rule.title}"`));
Expand Down Expand Up @@ -177,13 +180,14 @@ function displayRulesList(rules: RemoteRule[]) {
rules.forEach((rule, index) => {
const number = String(index + 1).padStart(2, ' ');
const title = rule.title;
const type = rule.ruleType;
const itemType = rule.type === 'command' ? 'Command' : 'Rule';
const ruleType = rule.ruleType;
const visibility = rule.isPublic ? 'public' : 'private';
const views = rule.views;
const date = new Date(rule.updatedAt).toLocaleDateString();

console.log(chalk.white(`${number}. ${chalk.bold(title)}`));
console.log(chalk.gray(` Type: ${type} | Visibility: ${visibility} | Views: ${views} | Updated: ${date}`));
console.log(chalk.white(`${number}. ${chalk.bold(title)} ${chalk.cyan(`[${itemType}]`)}`));
console.log(chalk.gray(` Rule Type: ${ruleType} | Visibility: ${visibility} | Views: ${views} | Updated: ${date}`));

// Show content preview (first line)
const firstLine = rule.content.split('\n')[0]?.trim();
Expand Down
Loading