Skip to content

Commit e2a9050

Browse files
authored
Merge pull request #2 from AJAmit17/merge-fix
Merge fix
2 parents fe37687 + 22f74c5 commit e2a9050

File tree

8 files changed

+691
-30
lines changed

8 files changed

+691
-30
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,7 @@ Adapters are used by both `export` and `run`. Available adapters:
360360
| `gemini` | Google Gemini CLI (GEMINI.md + settings.json) |
361361
| `openclaw` | OpenClaw format |
362362
| `nanobot` | Nanobot format |
363+
| `cursor` | Cursor `.cursor/rules/*.mdc` files |
363364

364365
```bash
365366
# Export to system prompt

src/adapters/cursor.test.ts

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
/**
2+
* Tests for the Cursor adapter (export + enhanced import).
3+
*
4+
* Uses Node.js built-in test runner (node --test).
5+
*/
6+
import { test, describe } from 'node:test';
7+
import assert from 'node:assert/strict';
8+
import { mkdtempSync, writeFileSync, mkdirSync } from 'node:fs';
9+
import { join } from 'node:path';
10+
import { tmpdir } from 'node:os';
11+
12+
import {
13+
exportToCursor,
14+
exportToCursorString,
15+
parseMdcFile,
16+
readCursorRules,
17+
} from './cursor.js';
18+
19+
// ---------------------------------------------------------------------------
20+
// Helpers
21+
// ---------------------------------------------------------------------------
22+
23+
/** Create a minimal gitagent directory in a temp folder. */
24+
function makeAgentDir(opts: {
25+
name?: string;
26+
description?: string;
27+
soul?: string;
28+
rules?: string;
29+
skills?: Array<{ name: string; description: string; instructions: string; globs?: string }>;
30+
}): string {
31+
const dir = mkdtempSync(join(tmpdir(), 'gitagent-cursor-test-'));
32+
33+
const manifest = {
34+
spec_version: '0.1.0',
35+
name: opts.name ?? 'test-agent',
36+
version: '0.1.0',
37+
description: opts.description ?? 'A test agent',
38+
};
39+
40+
writeFileSync(
41+
join(dir, 'agent.yaml'),
42+
`spec_version: '0.1.0'\nname: ${manifest.name}\nversion: '0.1.0'\ndescription: '${manifest.description}'\n`,
43+
'utf-8',
44+
);
45+
46+
if (opts.soul !== undefined) {
47+
writeFileSync(join(dir, 'SOUL.md'), opts.soul, 'utf-8');
48+
}
49+
50+
if (opts.rules !== undefined) {
51+
writeFileSync(join(dir, 'RULES.md'), opts.rules, 'utf-8');
52+
}
53+
54+
if (opts.skills) {
55+
for (const skill of opts.skills) {
56+
const skillDir = join(dir, 'skills', skill.name);
57+
mkdirSync(skillDir, { recursive: true });
58+
const metadataLine = skill.globs ? `metadata:\n globs: ${skill.globs}\n` : '';
59+
writeFileSync(
60+
join(skillDir, 'SKILL.md'),
61+
`---\nname: ${skill.name}\ndescription: '${skill.description}'\n${metadataLine}---\n\n${skill.instructions}\n`,
62+
'utf-8',
63+
);
64+
}
65+
}
66+
67+
return dir;
68+
}
69+
70+
// ---------------------------------------------------------------------------
71+
// parseMdcFile
72+
// ---------------------------------------------------------------------------
73+
74+
describe('parseMdcFile', () => {
75+
test('parses valid frontmatter + body', () => {
76+
const content = `---\ndescription: "Hello"\nalwaysApply: true\n---\n\n# Body\n\nSome content.\n`;
77+
const result = parseMdcFile(content);
78+
assert.equal(result.frontmatter.description, 'Hello');
79+
assert.equal(result.frontmatter.alwaysApply, true);
80+
assert.match(result.body, /# Body/);
81+
});
82+
83+
test('handles missing frontmatter gracefully', () => {
84+
const content = `Just plain markdown, no frontmatter.`;
85+
const result = parseMdcFile(content);
86+
assert.deepEqual(result.frontmatter, {});
87+
assert.equal(result.body, content);
88+
});
89+
90+
test('parses array globs', () => {
91+
const content = `---\nglobs:\n - "*.ts"\n - "src/**"\nalwaysApply: false\n---\n\nbody\n`;
92+
const result = parseMdcFile(content);
93+
assert.deepEqual(result.frontmatter.globs, ['*.ts', 'src/**']);
94+
});
95+
});
96+
97+
// ---------------------------------------------------------------------------
98+
// readCursorRules
99+
// ---------------------------------------------------------------------------
100+
101+
describe('readCursorRules', () => {
102+
test('returns empty array when no .cursor/rules dir exists', () => {
103+
const dir = mkdtempSync(join(tmpdir(), 'empty-'));
104+
const rules = readCursorRules(dir);
105+
assert.deepEqual(rules, []);
106+
});
107+
108+
test('reads .mdc files from .cursor/rules/', () => {
109+
const dir = mkdtempSync(join(tmpdir(), 'cursor-rules-'));
110+
const rulesDir = join(dir, '.cursor', 'rules');
111+
mkdirSync(rulesDir, { recursive: true });
112+
writeFileSync(join(rulesDir, 'rule-a.mdc'), `---\nalwaysApply: true\n---\n\nbody a\n`, 'utf-8');
113+
writeFileSync(join(rulesDir, 'rule-b.mdc'), `---\nalwaysApply: false\n---\n\nbody b\n`, 'utf-8');
114+
// Non-.mdc file should be ignored
115+
writeFileSync(join(rulesDir, 'ignored.md'), 'should be ignored', 'utf-8');
116+
117+
const rules = readCursorRules(dir);
118+
assert.equal(rules.length, 2);
119+
const filenames = rules.map(r => r.filename).sort();
120+
assert.deepEqual(filenames, ['rule-a.mdc', 'rule-b.mdc']);
121+
});
122+
});
123+
124+
// ---------------------------------------------------------------------------
125+
// exportToCursor — global rule
126+
// ---------------------------------------------------------------------------
127+
128+
describe('exportToCursor — global rule', () => {
129+
test('emits alwaysApply rule when SOUL.md exists', () => {
130+
const dir = makeAgentDir({ soul: '# I am a soul', description: 'My agent' });
131+
const exp = exportToCursor(dir);
132+
const global = exp.rules.find(r => r.content.includes('alwaysApply: true'));
133+
assert.ok(global, 'Expected an alwaysApply: true rule');
134+
assert.match(global!.content, /I am a soul/);
135+
});
136+
137+
test('includes RULES.md content in global rule', () => {
138+
const dir = makeAgentDir({ soul: '# Soul', rules: '# Never lie' });
139+
const exp = exportToCursor(dir);
140+
const global = exp.rules.find(r => r.content.includes('alwaysApply: true'));
141+
assert.ok(global);
142+
assert.match(global!.content, /Never lie/);
143+
});
144+
145+
test('no global rule emitted when neither SOUL.md nor RULES.md exists', () => {
146+
const dir = makeAgentDir({});
147+
const exp = exportToCursor(dir);
148+
const global = exp.rules.filter(r => r.content.includes('alwaysApply: true'));
149+
assert.equal(global.length, 0);
150+
});
151+
});
152+
153+
// ---------------------------------------------------------------------------
154+
// exportToCursor — skill rules
155+
// ---------------------------------------------------------------------------
156+
157+
describe('exportToCursor — skill rules', () => {
158+
test('emits one skill rule per skill', () => {
159+
const dir = makeAgentDir({
160+
skills: [
161+
{ name: 'code-review', description: 'Reviews code', instructions: 'Check for bugs.' },
162+
{ name: 'docs-writer', description: 'Writes docs', instructions: 'Write clear docs.' },
163+
],
164+
});
165+
const exp = exportToCursor(dir);
166+
const skillRules = exp.rules.filter(r => !r.content.includes('alwaysApply: true'));
167+
assert.equal(skillRules.length, 2);
168+
});
169+
170+
test('skill rule filename is slugified skill name', () => {
171+
const dir = makeAgentDir({
172+
skills: [{ name: 'my-skill', description: 'Skill', instructions: 'Do stuff.' }],
173+
});
174+
const exp = exportToCursor(dir);
175+
const rule = exp.rules.find(r => r.filename === 'my-skill.mdc');
176+
assert.ok(rule, 'Expected my-skill.mdc');
177+
});
178+
179+
test('skill rule includes globs when metadata.globs is set', () => {
180+
const dir = makeAgentDir({
181+
skills: [
182+
{
183+
name: 'api-handler',
184+
description: 'API handler review',
185+
instructions: 'Check endpoints.',
186+
globs: 'src/api/** *.route.ts',
187+
},
188+
],
189+
});
190+
const exp = exportToCursor(dir);
191+
const rule = exp.rules.find(r => r.filename === 'api-handler.mdc');
192+
assert.ok(rule);
193+
assert.match(rule!.content, /globs:/);
194+
assert.match(rule!.content, /src\/api\/\*\*/);
195+
assert.match(rule!.content, /\*\.route\.ts/);
196+
});
197+
198+
test('skill rule has alwaysApply: false', () => {
199+
const dir = makeAgentDir({
200+
skills: [{ name: 'linter', description: 'Lints', instructions: 'Lint everything.' }],
201+
});
202+
const exp = exportToCursor(dir);
203+
const rule = exp.rules.find(r => r.filename === 'linter.mdc');
204+
assert.ok(rule);
205+
assert.match(rule!.content, /alwaysApply: false/);
206+
});
207+
});
208+
209+
// ---------------------------------------------------------------------------
210+
// exportToCursorString
211+
// ---------------------------------------------------------------------------
212+
213+
describe('exportToCursorString', () => {
214+
test('returns string with file path headers', () => {
215+
const dir = makeAgentDir({
216+
soul: '# Soul',
217+
skills: [{ name: 'test-skill', description: 'Test', instructions: 'Do test.' }],
218+
});
219+
const output = exportToCursorString(dir);
220+
assert.match(output, /# === \.cursor\/rules\//);
221+
assert.match(output, /\.mdc ===/);
222+
});
223+
224+
test('output is non-empty string', () => {
225+
const dir = makeAgentDir({ soul: '# Soul' });
226+
const output = exportToCursorString(dir);
227+
assert.ok(output.length > 0);
228+
assert.equal(typeof output, 'string');
229+
});
230+
});

0 commit comments

Comments
 (0)