@burmese/cursor 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/check-cursor.d.ts +2 -0
- package/dist/check-cursor.d.ts.map +1 -0
- package/dist/check-cursor.js +660 -0
- package/dist/check-cursor.js.map +1 -0
- package/dist/cursor-mcp.d.ts +29 -0
- package/dist/cursor-mcp.d.ts.map +1 -0
- package/dist/cursor-mcp.js +42 -0
- package/dist/cursor-mcp.js.map +1 -0
- package/dist/cursor-previews.d.ts +15 -0
- package/dist/cursor-previews.d.ts.map +1 -0
- package/dist/cursor-previews.js +133 -0
- package/dist/cursor-previews.js.map +1 -0
- package/dist/cursor-rules.d.ts +42 -0
- package/dist/cursor-rules.d.ts.map +1 -0
- package/dist/cursor-rules.js +294 -0
- package/dist/cursor-rules.js.map +1 -0
- package/dist/cursor-status.d.ts +37 -0
- package/dist/cursor-status.d.ts.map +1 -0
- package/dist/cursor-status.js +365 -0
- package/dist/cursor-status.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/package.json +35 -0
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { buildCursorOpenPetsRule, buildCursorRulesPreview, classifyCursorRulesStatus, cursorRulesEndMarker, cursorRulesStartMarker, executeCursorRulesWrite, getCursorProjectRulesPath, isManagedCursorOpenPetsRule, maxCursorRulesBytes, planCursorRulesInstall, planCursorRulesRemove, planCursorRulesReplace, readCursorOpenPetsRules, } from "./cursor-rules.js";
|
|
6
|
+
import { buildCursorMcpEntry, formatCursorMcpConfig, getCursorGlobalMcpPath, getCursorProjectMcpPath, isValidPetId, validateOpenPetsPetId, } from "./cursor-mcp.js";
|
|
7
|
+
import { classifyCursorMcpStatus, executeCursorMcpWrite, maxCursorConfigBytes, planCursorMcpInstall, planCursorMcpRemove, planCursorMcpReplace, readCursorMcpConfig, } from "./cursor-status.js";
|
|
8
|
+
import { buildOpenPetsOnlyPreview, redactCursorConfig } from "./cursor-previews.js";
|
|
9
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "openpets-cursor-")));
|
|
10
|
+
try {
|
|
11
|
+
// Test pet ID validation
|
|
12
|
+
assert.equal(isValidPetId("fixer"), true);
|
|
13
|
+
assert.equal(isValidPetId("my_pet-123"), true);
|
|
14
|
+
assert.equal(isValidPetId("a"), true);
|
|
15
|
+
assert.equal(isValidPetId("a".repeat(64)), true);
|
|
16
|
+
assert.equal(isValidPetId(""), false);
|
|
17
|
+
assert.equal(isValidPetId("-invalid"), false);
|
|
18
|
+
assert.equal(isValidPetId("_invalid"), false);
|
|
19
|
+
assert.equal(isValidPetId("invalid/slash"), false);
|
|
20
|
+
assert.equal(isValidPetId("a".repeat(65)), false);
|
|
21
|
+
assert.equal(validateOpenPetsPetId("fixer"), "fixer");
|
|
22
|
+
assert.throws(() => validateOpenPetsPetId("bad/pet"));
|
|
23
|
+
assert.throws(() => validateOpenPetsPetId(""));
|
|
24
|
+
// Test MCP entry building
|
|
25
|
+
const publishedEntry = buildCursorMcpEntry({ mcpVersion: "2.0.6", petId: "fixer" });
|
|
26
|
+
assert.deepEqual(publishedEntry, {
|
|
27
|
+
type: "stdio",
|
|
28
|
+
command: "npx",
|
|
29
|
+
args: ["-y", "@burmese/mcp@2.0.6", "--pet", "fixer"],
|
|
30
|
+
});
|
|
31
|
+
const publishedNoPet = buildCursorMcpEntry({ mcpVersion: "2.0.6" });
|
|
32
|
+
assert.deepEqual(publishedNoPet, {
|
|
33
|
+
type: "stdio",
|
|
34
|
+
command: "npx",
|
|
35
|
+
args: ["-y", "@burmese/mcp@2.0.6"],
|
|
36
|
+
});
|
|
37
|
+
assert.throws(() => buildCursorMcpEntry({ mcpVersion: "latest" }));
|
|
38
|
+
const localEntry = buildCursorMcpEntry({
|
|
39
|
+
mcpVersion: "2.0.6",
|
|
40
|
+
petId: "helper",
|
|
41
|
+
commandMode: "local",
|
|
42
|
+
mcpEntryPath: join(root, "mcp.js"),
|
|
43
|
+
});
|
|
44
|
+
assert.deepEqual(localEntry, {
|
|
45
|
+
type: "stdio",
|
|
46
|
+
command: "node",
|
|
47
|
+
args: [join(root, "mcp.js"), "--pet", "helper"],
|
|
48
|
+
});
|
|
49
|
+
assert.throws(() => buildCursorMcpEntry({ mcpVersion: "2.0.6", commandMode: "local", mcpEntryPath: "relative.js" }));
|
|
50
|
+
// Test config formatting
|
|
51
|
+
const formatted = formatCursorMcpConfig({ mcpVersion: "2.0.6", petId: "fixer" });
|
|
52
|
+
assert.deepEqual(formatted, {
|
|
53
|
+
mcpServers: {
|
|
54
|
+
openpets: {
|
|
55
|
+
type: "stdio",
|
|
56
|
+
command: "npx",
|
|
57
|
+
args: ["-y", "@burmese/mcp@2.0.6", "--pet", "fixer"],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
// Test path helpers
|
|
62
|
+
assert.equal(getCursorGlobalMcpPath(join(root, "home")), join(root, "home", ".cursor", "mcp.json"));
|
|
63
|
+
assert.equal(getCursorProjectMcpPath(join(root, "project")), join(root, "project", ".cursor", "mcp.json"));
|
|
64
|
+
// Test missing config classification
|
|
65
|
+
const missingPath = join(root, "missing", "mcp.json");
|
|
66
|
+
const missingResult = readCursorMcpConfig(missingPath);
|
|
67
|
+
assert.equal(missingResult.ok, true);
|
|
68
|
+
if (missingResult.ok) {
|
|
69
|
+
assert.equal(missingResult.exists, false);
|
|
70
|
+
assert.deepEqual(missingResult.config, {});
|
|
71
|
+
}
|
|
72
|
+
const missingStatus = classifyCursorMcpStatus(missingResult, missingPath, { mcpVersion: "2.0.6", petId: "fixer" });
|
|
73
|
+
assert.equal(missingStatus.status, "missing");
|
|
74
|
+
assert.equal(missingStatus.canInstall, true);
|
|
75
|
+
assert.equal(missingStatus.canReplace, false);
|
|
76
|
+
assert.equal(missingStatus.canRemove, false);
|
|
77
|
+
const unsafeBasePath = join(root, "file-parent");
|
|
78
|
+
writeFileSync(unsafeBasePath, "not a directory", "utf8");
|
|
79
|
+
const unsafeMissingPath = join(unsafeBasePath, ".cursor", "mcp.json");
|
|
80
|
+
const unsafeMissingResult = readCursorMcpConfig(unsafeMissingPath);
|
|
81
|
+
assert.equal(unsafeMissingResult.ok, false);
|
|
82
|
+
if (!unsafeMissingResult.ok) {
|
|
83
|
+
assert.equal(unsafeMissingResult.reason, "unsafe-path");
|
|
84
|
+
}
|
|
85
|
+
const unsafeMissingStatus = classifyCursorMcpStatus(unsafeMissingResult, unsafeMissingPath, { mcpVersion: "2.0.6" });
|
|
86
|
+
assert.equal(unsafeMissingStatus.status, "invalid");
|
|
87
|
+
assert.equal(unsafeMissingStatus.canInstall, false);
|
|
88
|
+
// Test empty config classification
|
|
89
|
+
const emptyDir = join(root, "empty");
|
|
90
|
+
mkdirSync(emptyDir);
|
|
91
|
+
const emptyPath = join(emptyDir, "mcp.json");
|
|
92
|
+
writeFileSync(emptyPath, "", "utf8");
|
|
93
|
+
const emptyResult = readCursorMcpConfig(emptyPath);
|
|
94
|
+
assert.equal(emptyResult.ok, true);
|
|
95
|
+
if (emptyResult.ok) {
|
|
96
|
+
assert.equal(emptyResult.exists, true);
|
|
97
|
+
assert.deepEqual(emptyResult.config, {});
|
|
98
|
+
}
|
|
99
|
+
const emptyStatus = classifyCursorMcpStatus(emptyResult, emptyPath, { mcpVersion: "2.0.6", petId: "fixer" });
|
|
100
|
+
assert.equal(emptyStatus.status, "missing");
|
|
101
|
+
assert.equal(emptyStatus.canInstall, true);
|
|
102
|
+
// Test installed status
|
|
103
|
+
const installedDir = join(root, "installed");
|
|
104
|
+
mkdirSync(installedDir);
|
|
105
|
+
const installedPath = join(installedDir, "mcp.json");
|
|
106
|
+
const installedConfig = formatCursorMcpConfig({ mcpVersion: "2.0.6", petId: "fixer" });
|
|
107
|
+
writeFileSync(installedPath, JSON.stringify(installedConfig, null, 2), "utf8");
|
|
108
|
+
const installedResult = readCursorMcpConfig(installedPath);
|
|
109
|
+
assert.equal(installedResult.ok, true);
|
|
110
|
+
const installedStatus = classifyCursorMcpStatus(installedResult, installedPath, { mcpVersion: "2.0.6", petId: "fixer" });
|
|
111
|
+
assert.equal(installedStatus.status, "installed");
|
|
112
|
+
assert.equal(installedStatus.canInstall, false);
|
|
113
|
+
assert.equal(installedStatus.canReplace, false);
|
|
114
|
+
assert.equal(installedStatus.canRemove, true);
|
|
115
|
+
const installedReplacePlan = planCursorMcpReplace(installedPath, { mcpVersion: "2.0.6", petId: "fixer" });
|
|
116
|
+
assert.equal("ok" in installedReplacePlan, true);
|
|
117
|
+
if ("ok" in installedReplacePlan) {
|
|
118
|
+
assert.equal(installedReplacePlan.ok, false);
|
|
119
|
+
}
|
|
120
|
+
// Test needs-update status for old version
|
|
121
|
+
const oldVersionDir = join(root, "old-version");
|
|
122
|
+
mkdirSync(oldVersionDir);
|
|
123
|
+
const oldVersionPath = join(oldVersionDir, "mcp.json");
|
|
124
|
+
const oldVersionConfig = formatCursorMcpConfig({ mcpVersion: "2.0.5", petId: "fixer" });
|
|
125
|
+
writeFileSync(oldVersionPath, JSON.stringify(oldVersionConfig, null, 2), "utf8");
|
|
126
|
+
const oldVersionResult = readCursorMcpConfig(oldVersionPath);
|
|
127
|
+
const oldVersionStatus = classifyCursorMcpStatus(oldVersionResult, oldVersionPath, { mcpVersion: "2.0.6", petId: "fixer" });
|
|
128
|
+
assert.equal(oldVersionStatus.status, "needs-update");
|
|
129
|
+
assert.equal(oldVersionStatus.canInstall, true);
|
|
130
|
+
assert.equal(oldVersionStatus.canReplace, true);
|
|
131
|
+
assert.equal(oldVersionStatus.canRemove, true);
|
|
132
|
+
// Test needs-update status for different pet
|
|
133
|
+
const diffPetDir = join(root, "diff-pet");
|
|
134
|
+
mkdirSync(diffPetDir);
|
|
135
|
+
const diffPetPath = join(diffPetDir, "mcp.json");
|
|
136
|
+
const diffPetConfig = formatCursorMcpConfig({ mcpVersion: "2.0.6", petId: "helper" });
|
|
137
|
+
writeFileSync(diffPetPath, JSON.stringify(diffPetConfig, null, 2), "utf8");
|
|
138
|
+
const diffPetResult = readCursorMcpConfig(diffPetPath);
|
|
139
|
+
const diffPetStatus = classifyCursorMcpStatus(diffPetResult, diffPetPath, { mcpVersion: "2.0.6", petId: "fixer" });
|
|
140
|
+
assert.equal(diffPetStatus.status, "needs-update");
|
|
141
|
+
// Test conflict status for non-OpenPets openpets entry
|
|
142
|
+
const conflictDir = join(root, "conflict");
|
|
143
|
+
mkdirSync(conflictDir);
|
|
144
|
+
const conflictPath = join(conflictDir, "mcp.json");
|
|
145
|
+
const conflictConfig = {
|
|
146
|
+
mcpServers: {
|
|
147
|
+
openpets: { type: "stdio", command: "custom", args: ["mcp"] },
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
writeFileSync(conflictPath, JSON.stringify(conflictConfig, null, 2), "utf8");
|
|
151
|
+
const conflictResult = readCursorMcpConfig(conflictPath);
|
|
152
|
+
const conflictStatus = classifyCursorMcpStatus(conflictResult, conflictPath, { mcpVersion: "2.0.6", petId: "fixer" });
|
|
153
|
+
assert.equal(conflictStatus.status, "conflict");
|
|
154
|
+
assert.equal(conflictStatus.canInstall, false);
|
|
155
|
+
assert.equal(conflictStatus.canReplace, true);
|
|
156
|
+
assert.equal(conflictStatus.canRemove, false);
|
|
157
|
+
const unpinnedDir = join(root, "unpinned");
|
|
158
|
+
mkdirSync(unpinnedDir);
|
|
159
|
+
const unpinnedPath = join(unpinnedDir, "mcp.json");
|
|
160
|
+
writeFileSync(unpinnedPath, JSON.stringify({ mcpServers: { openpets: { type: "stdio", command: "npx", args: ["-y", "@burmese/mcp@latest"] } } }), "utf8");
|
|
161
|
+
const unpinnedStatus = classifyCursorMcpStatus(readCursorMcpConfig(unpinnedPath), unpinnedPath, { mcpVersion: "2.0.6" });
|
|
162
|
+
assert.equal(unpinnedStatus.status, "conflict");
|
|
163
|
+
// Test invalid status for parse error
|
|
164
|
+
const parseErrorDir = join(root, "parse-error");
|
|
165
|
+
mkdirSync(parseErrorDir);
|
|
166
|
+
const parseErrorPath = join(parseErrorDir, "mcp.json");
|
|
167
|
+
writeFileSync(parseErrorPath, "{ invalid json", "utf8");
|
|
168
|
+
const parseErrorResult = readCursorMcpConfig(parseErrorPath);
|
|
169
|
+
assert.equal(parseErrorResult.ok, false);
|
|
170
|
+
if (!parseErrorResult.ok) {
|
|
171
|
+
assert.equal(parseErrorResult.reason, "parse");
|
|
172
|
+
}
|
|
173
|
+
const parseErrorStatus = classifyCursorMcpStatus(parseErrorResult, parseErrorPath, { mcpVersion: "2.0.6", petId: "fixer" });
|
|
174
|
+
assert.equal(parseErrorStatus.status, "invalid");
|
|
175
|
+
assert.equal(parseErrorStatus.canInstall, false);
|
|
176
|
+
// Test invalid status for oversized file
|
|
177
|
+
const oversizedDir = join(root, "oversized");
|
|
178
|
+
mkdirSync(oversizedDir);
|
|
179
|
+
const oversizedPath = join(oversizedDir, "mcp.json");
|
|
180
|
+
const largeContent = JSON.stringify({ data: "x".repeat(maxCursorConfigBytes + 1000) });
|
|
181
|
+
writeFileSync(oversizedPath, largeContent, "utf8");
|
|
182
|
+
const oversizedResult = readCursorMcpConfig(oversizedPath);
|
|
183
|
+
assert.equal(oversizedResult.ok, false);
|
|
184
|
+
if (!oversizedResult.ok) {
|
|
185
|
+
assert.equal(oversizedResult.reason, "size");
|
|
186
|
+
}
|
|
187
|
+
// Test symlink rejection
|
|
188
|
+
const symlinkDir = join(root, "symlink-test");
|
|
189
|
+
mkdirSync(symlinkDir);
|
|
190
|
+
const realFile = join(symlinkDir, "real.json");
|
|
191
|
+
const symlinkFile = join(symlinkDir, "symlink.json");
|
|
192
|
+
writeFileSync(realFile, "{}", "utf8");
|
|
193
|
+
symlinkSync(realFile, symlinkFile);
|
|
194
|
+
const symlinkResult = readCursorMcpConfig(symlinkFile);
|
|
195
|
+
assert.equal(symlinkResult.ok, false);
|
|
196
|
+
if (!symlinkResult.ok) {
|
|
197
|
+
assert.equal(symlinkResult.reason, "symlink");
|
|
198
|
+
}
|
|
199
|
+
const danglingConfigSymlink = join(symlinkDir, "dangling-config.json");
|
|
200
|
+
symlinkSync(join(symlinkDir, "missing-config.json"), danglingConfigSymlink);
|
|
201
|
+
const danglingConfigResult = readCursorMcpConfig(danglingConfigSymlink);
|
|
202
|
+
assert.equal(danglingConfigResult.ok, false);
|
|
203
|
+
if (!danglingConfigResult.ok) {
|
|
204
|
+
assert.equal(danglingConfigResult.reason, "symlink");
|
|
205
|
+
}
|
|
206
|
+
// Test non-regular file rejection
|
|
207
|
+
const nonRegularDir = join(root, "non-regular");
|
|
208
|
+
mkdirSync(nonRegularDir);
|
|
209
|
+
const directoryAsConfig = join(nonRegularDir, "mcp.json");
|
|
210
|
+
mkdirSync(directoryAsConfig);
|
|
211
|
+
const nonRegularResult = readCursorMcpConfig(directoryAsConfig);
|
|
212
|
+
assert.equal(nonRegularResult.ok, false);
|
|
213
|
+
if (!nonRegularResult.ok) {
|
|
214
|
+
assert.equal(nonRegularResult.reason, "not-regular");
|
|
215
|
+
}
|
|
216
|
+
const nonRegularPlan = planCursorMcpInstall(directoryAsConfig, { mcpVersion: "2.0.6" });
|
|
217
|
+
assert.equal("ok" in nonRegularPlan, true);
|
|
218
|
+
if ("ok" in nonRegularPlan) {
|
|
219
|
+
assert.equal(nonRegularPlan.ok, false);
|
|
220
|
+
}
|
|
221
|
+
const ioStatus = classifyCursorMcpStatus({ ok: false, reason: "io", message: "simulated io failure" }, join(root, "io", "mcp.json"), { mcpVersion: "2.0.6" });
|
|
222
|
+
assert.equal(ioStatus.status, "error");
|
|
223
|
+
assert.equal(ioStatus.canInstall, false);
|
|
224
|
+
assert.equal(ioStatus.canReplace, false);
|
|
225
|
+
assert.equal(ioStatus.canRemove, false);
|
|
226
|
+
// Test non-object top-level config
|
|
227
|
+
const nonObjectDir = join(root, "non-object");
|
|
228
|
+
mkdirSync(nonObjectDir);
|
|
229
|
+
const nonObjectPath = join(nonObjectDir, "mcp.json");
|
|
230
|
+
writeFileSync(nonObjectPath, "[]", "utf8");
|
|
231
|
+
const nonObjectResult = readCursorMcpConfig(nonObjectPath);
|
|
232
|
+
assert.equal(nonObjectResult.ok, false);
|
|
233
|
+
if (!nonObjectResult.ok) {
|
|
234
|
+
assert.equal(nonObjectResult.reason, "invalid-schema");
|
|
235
|
+
}
|
|
236
|
+
// Test non-object mcpServers
|
|
237
|
+
const badServersDir = join(root, "bad-servers");
|
|
238
|
+
mkdirSync(badServersDir);
|
|
239
|
+
const badServersPath = join(badServersDir, "mcp.json");
|
|
240
|
+
writeFileSync(badServersPath, JSON.stringify({ mcpServers: [] }), "utf8");
|
|
241
|
+
const badServersResult = readCursorMcpConfig(badServersPath);
|
|
242
|
+
assert.equal(badServersResult.ok, false);
|
|
243
|
+
if (!badServersResult.ok) {
|
|
244
|
+
assert.equal(badServersResult.reason, "invalid-schema");
|
|
245
|
+
}
|
|
246
|
+
// Test malformed mcpServers.openpets (not an object)
|
|
247
|
+
const malformedEntryDir = join(root, "malformed-entry");
|
|
248
|
+
mkdirSync(malformedEntryDir);
|
|
249
|
+
const malformedEntryPath = join(malformedEntryDir, "mcp.json");
|
|
250
|
+
writeFileSync(malformedEntryPath, JSON.stringify({ mcpServers: { openpets: "string" } }), "utf8");
|
|
251
|
+
const malformedEntryResult = readCursorMcpConfig(malformedEntryPath);
|
|
252
|
+
assert.equal(malformedEntryResult.ok, true);
|
|
253
|
+
const malformedEntryStatus = classifyCursorMcpStatus(malformedEntryResult, malformedEntryPath, { mcpVersion: "2.0.6", petId: "fixer" });
|
|
254
|
+
assert.equal(malformedEntryStatus.status, "conflict");
|
|
255
|
+
// Test backup creation
|
|
256
|
+
const backupDir = join(root, "backup");
|
|
257
|
+
mkdirSync(backupDir);
|
|
258
|
+
const backupPath = join(backupDir, "mcp.json");
|
|
259
|
+
const originalContent = JSON.stringify({ mcpServers: { other: { type: "stdio", command: "test", args: [] } } }, null, 2);
|
|
260
|
+
writeFileSync(backupPath, originalContent, "utf8");
|
|
261
|
+
const installPlan = planCursorMcpInstall(backupPath, { mcpVersion: "2.0.6", petId: "fixer" });
|
|
262
|
+
assert.equal("targetPath" in installPlan, true);
|
|
263
|
+
if ("targetPath" in installPlan) {
|
|
264
|
+
assert.equal(installPlan.backupPath !== undefined, true);
|
|
265
|
+
executeCursorMcpWrite(installPlan);
|
|
266
|
+
assert.equal(existsSync(backupPath), true);
|
|
267
|
+
assert.equal(existsSync(installPlan.backupPath), true);
|
|
268
|
+
const backupContent = readFileSync(installPlan.backupPath, "utf8");
|
|
269
|
+
assert.equal(backupContent, originalContent);
|
|
270
|
+
}
|
|
271
|
+
// Test atomic write result
|
|
272
|
+
const atomicDir = join(root, "atomic");
|
|
273
|
+
mkdirSync(atomicDir);
|
|
274
|
+
const atomicPath = join(atomicDir, "mcp.json");
|
|
275
|
+
const atomicPlan = planCursorMcpInstall(atomicPath, { mcpVersion: "2.0.6", petId: "fixer" });
|
|
276
|
+
assert.equal("targetPath" in atomicPlan, true);
|
|
277
|
+
if ("targetPath" in atomicPlan) {
|
|
278
|
+
executeCursorMcpWrite(atomicPlan);
|
|
279
|
+
assert.equal(existsSync(atomicPath), true);
|
|
280
|
+
const writtenContent = JSON.parse(readFileSync(atomicPath, "utf8"));
|
|
281
|
+
assert.deepEqual(writtenContent.mcpServers.openpets, {
|
|
282
|
+
type: "stdio",
|
|
283
|
+
command: "npx",
|
|
284
|
+
args: ["-y", "@burmese/mcp@2.0.6", "--pet", "fixer"],
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
// Test uninstall removes only OpenPets entry
|
|
288
|
+
const uninstallDir = join(root, "uninstall");
|
|
289
|
+
mkdirSync(uninstallDir);
|
|
290
|
+
const uninstallPath = join(uninstallDir, "mcp.json");
|
|
291
|
+
const uninstallConfig = {
|
|
292
|
+
mcpServers: {
|
|
293
|
+
openpets: { type: "stdio", command: "npx", args: ["-y", "@burmese/mcp@2.0.6", "--pet", "fixer"] },
|
|
294
|
+
other: { type: "stdio", command: "test", args: [] },
|
|
295
|
+
},
|
|
296
|
+
otherField: "keep",
|
|
297
|
+
};
|
|
298
|
+
writeFileSync(uninstallPath, JSON.stringify(uninstallConfig, null, 2), "utf8");
|
|
299
|
+
const removePlan = planCursorMcpRemove(uninstallPath);
|
|
300
|
+
assert.equal("targetPath" in removePlan, true);
|
|
301
|
+
if ("targetPath" in removePlan) {
|
|
302
|
+
executeCursorMcpWrite(removePlan);
|
|
303
|
+
const removedContent = JSON.parse(readFileSync(uninstallPath, "utf8"));
|
|
304
|
+
assert.equal(removedContent.mcpServers.openpets, undefined);
|
|
305
|
+
assert.deepEqual(removedContent.mcpServers.other, { type: "stdio", command: "test", args: [] });
|
|
306
|
+
assert.equal(removedContent.otherField, "keep");
|
|
307
|
+
}
|
|
308
|
+
// Test no write on invalid
|
|
309
|
+
const noWriteInvalidDir = join(root, "no-write-invalid");
|
|
310
|
+
mkdirSync(noWriteInvalidDir);
|
|
311
|
+
const noWriteInvalidPath = join(noWriteInvalidDir, "mcp.json");
|
|
312
|
+
writeFileSync(noWriteInvalidPath, "{ invalid", "utf8");
|
|
313
|
+
const noWriteInvalidPlan = planCursorMcpInstall(noWriteInvalidPath, { mcpVersion: "2.0.6", petId: "fixer" });
|
|
314
|
+
assert.equal("ok" in noWriteInvalidPlan, true);
|
|
315
|
+
if ("ok" in noWriteInvalidPlan) {
|
|
316
|
+
assert.equal(noWriteInvalidPlan.ok, false);
|
|
317
|
+
}
|
|
318
|
+
// Test no write on conflict unless explicit replace
|
|
319
|
+
const noWriteConflictDir = join(root, "no-write-conflict");
|
|
320
|
+
mkdirSync(noWriteConflictDir);
|
|
321
|
+
const noWriteConflictPath = join(noWriteConflictDir, "mcp.json");
|
|
322
|
+
writeFileSync(noWriteConflictPath, JSON.stringify({ mcpServers: { openpets: { type: "stdio", command: "custom", args: [] } } }), "utf8");
|
|
323
|
+
const noWriteConflictPlan = planCursorMcpInstall(noWriteConflictPath, { mcpVersion: "2.0.6", petId: "fixer" });
|
|
324
|
+
assert.equal("ok" in noWriteConflictPlan, true);
|
|
325
|
+
if ("ok" in noWriteConflictPlan) {
|
|
326
|
+
assert.equal(noWriteConflictPlan.ok, false);
|
|
327
|
+
}
|
|
328
|
+
const noRemoveConflictPlan = planCursorMcpRemove(noWriteConflictPath);
|
|
329
|
+
assert.equal("ok" in noRemoveConflictPlan, true);
|
|
330
|
+
if ("ok" in noRemoveConflictPlan) {
|
|
331
|
+
assert.equal(noRemoveConflictPlan.ok, false);
|
|
332
|
+
}
|
|
333
|
+
// Test explicit replace overwrites only openpets and preserves unrelated servers
|
|
334
|
+
const replaceDir = join(root, "replace");
|
|
335
|
+
mkdirSync(replaceDir);
|
|
336
|
+
const replacePath = join(replaceDir, "mcp.json");
|
|
337
|
+
const replaceConfig = {
|
|
338
|
+
mcpServers: {
|
|
339
|
+
openpets: { type: "stdio", command: "custom", args: [] },
|
|
340
|
+
other: { type: "stdio", command: "test", args: [] },
|
|
341
|
+
},
|
|
342
|
+
topLevelField: "preserve",
|
|
343
|
+
};
|
|
344
|
+
writeFileSync(replacePath, JSON.stringify(replaceConfig, null, 2), "utf8");
|
|
345
|
+
const replacePlan = planCursorMcpReplace(replacePath, { mcpVersion: "2.0.6", petId: "fixer" });
|
|
346
|
+
assert.equal("targetPath" in replacePlan, true);
|
|
347
|
+
if ("targetPath" in replacePlan) {
|
|
348
|
+
executeCursorMcpWrite(replacePlan);
|
|
349
|
+
const replacedContent = JSON.parse(readFileSync(replacePath, "utf8"));
|
|
350
|
+
assert.deepEqual(replacedContent.mcpServers.openpets, {
|
|
351
|
+
type: "stdio",
|
|
352
|
+
command: "npx",
|
|
353
|
+
args: ["-y", "@burmese/mcp@2.0.6", "--pet", "fixer"],
|
|
354
|
+
});
|
|
355
|
+
assert.deepEqual(replacedContent.mcpServers.other, { type: "stdio", command: "test", args: [] });
|
|
356
|
+
assert.equal(replacedContent.topLevelField, "preserve");
|
|
357
|
+
}
|
|
358
|
+
// Test preview redaction
|
|
359
|
+
const redactedConfig = {
|
|
360
|
+
mcpServers: {
|
|
361
|
+
openpets: { type: "stdio", command: "npx", args: ["-y", "@burmese/mcp@2.0.6"] },
|
|
362
|
+
other: {
|
|
363
|
+
type: "stdio",
|
|
364
|
+
command: "test",
|
|
365
|
+
args: ["--token=secret123", "--api-key=abc"],
|
|
366
|
+
env: { SECRET: "hidden", TOKEN: "hidden" },
|
|
367
|
+
headers: { Authorization: "Bearer token123" },
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
const redacted = redactCursorConfig(redactedConfig);
|
|
372
|
+
assert.deepEqual(redacted.mcpServers?.openpets, { type: "stdio", command: "npx", args: ["-y", "@burmese/mcp@2.0.6"] });
|
|
373
|
+
const otherServer = redacted.mcpServers?.other;
|
|
374
|
+
assert.deepEqual(otherServer.args, ["--token=[REDACTED]", "--api-key=[REDACTED]"]);
|
|
375
|
+
assert.equal(otherServer.env, "[REDACTED]");
|
|
376
|
+
assert.equal(otherServer.headers, "[REDACTED]");
|
|
377
|
+
// Test recursive and case-insensitive redaction
|
|
378
|
+
const recursiveConfig = {
|
|
379
|
+
mcpServers: {
|
|
380
|
+
server1: {
|
|
381
|
+
type: "stdio",
|
|
382
|
+
command: "test",
|
|
383
|
+
ENV: { secretValue: "hidden" },
|
|
384
|
+
Auth: { password: "secret" },
|
|
385
|
+
nested: {
|
|
386
|
+
TOKEN: "bearer123",
|
|
387
|
+
credentials: { apiKey: "key123" },
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
const recursiveRedacted = redactCursorConfig(recursiveConfig);
|
|
393
|
+
const server1 = recursiveRedacted.mcpServers?.server1;
|
|
394
|
+
assert.equal(server1.ENV, "[REDACTED]");
|
|
395
|
+
assert.equal(server1.Auth, "[REDACTED]");
|
|
396
|
+
const nested = server1.nested;
|
|
397
|
+
assert.equal(nested.TOKEN, "[REDACTED]");
|
|
398
|
+
assert.equal(nested.credentials, "[REDACTED]");
|
|
399
|
+
// Test URL with token-like query params redaction
|
|
400
|
+
const urlConfig = {
|
|
401
|
+
mcpServers: {
|
|
402
|
+
server1: {
|
|
403
|
+
type: "stdio",
|
|
404
|
+
command: "test",
|
|
405
|
+
args: ["https://example.com/api?token=secret&other=value"],
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
const urlRedacted = redactCursorConfig(urlConfig);
|
|
410
|
+
const urlServer = urlRedacted.mcpServers?.server1;
|
|
411
|
+
const urlArgs = urlServer.args;
|
|
412
|
+
assert.ok(urlArgs[0].includes("[REDACTED]"));
|
|
413
|
+
assert.ok(!urlArgs[0].includes("secret"));
|
|
414
|
+
// Test OpenPets-only preview
|
|
415
|
+
const preview = buildOpenPetsOnlyPreview({ mcpVersion: "2.0.6", petId: "fixer" });
|
|
416
|
+
assert.deepEqual(preview.openpets, {
|
|
417
|
+
type: "stdio",
|
|
418
|
+
command: "npx",
|
|
419
|
+
args: ["-y", "@burmese/mcp@2.0.6", "--pet", "fixer"],
|
|
420
|
+
});
|
|
421
|
+
// Test existing unrelated MCP servers preserved during install
|
|
422
|
+
const preserveDir = join(root, "preserve");
|
|
423
|
+
mkdirSync(preserveDir);
|
|
424
|
+
const preservePath = join(preserveDir, "mcp.json");
|
|
425
|
+
const preserveConfig = {
|
|
426
|
+
mcpServers: {
|
|
427
|
+
other: { type: "stdio", command: "test", args: [] },
|
|
428
|
+
},
|
|
429
|
+
topLevel: "keep",
|
|
430
|
+
};
|
|
431
|
+
writeFileSync(preservePath, JSON.stringify(preserveConfig, null, 2), "utf8");
|
|
432
|
+
const preservePlan = planCursorMcpInstall(preservePath, { mcpVersion: "2.0.6", petId: "fixer" });
|
|
433
|
+
assert.equal("targetPath" in preservePlan, true);
|
|
434
|
+
if ("targetPath" in preservePlan) {
|
|
435
|
+
executeCursorMcpWrite(preservePlan);
|
|
436
|
+
const preservedContent = JSON.parse(readFileSync(preservePath, "utf8"));
|
|
437
|
+
assert.deepEqual(preservedContent.mcpServers.other, { type: "stdio", command: "test", args: [] });
|
|
438
|
+
assert.equal(preservedContent.topLevel, "keep");
|
|
439
|
+
assert.deepEqual(preservedContent.mcpServers.openpets, {
|
|
440
|
+
type: "stdio",
|
|
441
|
+
command: "npx",
|
|
442
|
+
args: ["-y", "@burmese/mcp@2.0.6", "--pet", "fixer"],
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
// Test symlink parent rejection
|
|
446
|
+
const symlinkParentDir = join(root, "symlink-parent");
|
|
447
|
+
const realParent = join(root, "real-parent");
|
|
448
|
+
mkdirSync(realParent);
|
|
449
|
+
symlinkSync(realParent, symlinkParentDir);
|
|
450
|
+
const symlinkParentPath = join(symlinkParentDir, "mcp.json");
|
|
451
|
+
const symlinkParentPlan = planCursorMcpInstall(symlinkParentPath, { mcpVersion: "2.0.6", petId: "fixer" });
|
|
452
|
+
assert.equal("ok" in symlinkParentPlan, true);
|
|
453
|
+
if ("ok" in symlinkParentPlan) {
|
|
454
|
+
assert.equal(symlinkParentPlan.ok, false);
|
|
455
|
+
}
|
|
456
|
+
// Test nested symlink ancestor rejection for missing and existing config files
|
|
457
|
+
const nestedReal = join(root, "nested-real");
|
|
458
|
+
mkdirSync(join(nestedReal, "sub", ".cursor"), { recursive: true });
|
|
459
|
+
const nestedLink = join(root, "nested-link");
|
|
460
|
+
symlinkSync(nestedReal, nestedLink);
|
|
461
|
+
const nestedMissingThroughLink = join(nestedLink, "missing", ".cursor", "mcp.json");
|
|
462
|
+
const nestedMissingResult = readCursorMcpConfig(nestedMissingThroughLink);
|
|
463
|
+
assert.equal(nestedMissingResult.ok, false);
|
|
464
|
+
if (!nestedMissingResult.ok) {
|
|
465
|
+
assert.equal(nestedMissingResult.reason, "symlink");
|
|
466
|
+
}
|
|
467
|
+
const nestedExistingThroughLink = join(nestedLink, "sub", ".cursor", "mcp.json");
|
|
468
|
+
writeFileSync(join(nestedReal, "sub", ".cursor", "mcp.json"), "{}", "utf8");
|
|
469
|
+
const nestedExistingResult = readCursorMcpConfig(nestedExistingThroughLink);
|
|
470
|
+
assert.equal(nestedExistingResult.ok, false);
|
|
471
|
+
if (!nestedExistingResult.ok) {
|
|
472
|
+
assert.equal(nestedExistingResult.reason, "symlink");
|
|
473
|
+
}
|
|
474
|
+
const danglingLink = join(root, "dangling-link");
|
|
475
|
+
symlinkSync(join(root, "missing-target"), danglingLink);
|
|
476
|
+
const danglingPath = join(danglingLink, ".cursor", "mcp.json");
|
|
477
|
+
const danglingResult = readCursorMcpConfig(danglingPath);
|
|
478
|
+
assert.equal(danglingResult.ok, false);
|
|
479
|
+
if (!danglingResult.ok) {
|
|
480
|
+
assert.equal(danglingResult.reason, "symlink");
|
|
481
|
+
}
|
|
482
|
+
const traversalPath = `${nestedLink}/../traversal/.cursor/mcp.json`;
|
|
483
|
+
const traversalResult = readCursorMcpConfig(traversalPath);
|
|
484
|
+
assert.equal(traversalResult.ok, false);
|
|
485
|
+
if (!traversalResult.ok) {
|
|
486
|
+
assert.equal(traversalResult.reason, "unsafe-path");
|
|
487
|
+
}
|
|
488
|
+
// Test empty mcpServers kept as empty object after remove
|
|
489
|
+
const emptyAfterRemoveDir = join(root, "empty-after-remove");
|
|
490
|
+
mkdirSync(emptyAfterRemoveDir);
|
|
491
|
+
const emptyAfterRemovePath = join(emptyAfterRemoveDir, "mcp.json");
|
|
492
|
+
writeFileSync(emptyAfterRemovePath, JSON.stringify({ mcpServers: { openpets: { type: "stdio", command: "npx", args: ["-y", "@burmese/mcp@2.0.6"] } } }), "utf8");
|
|
493
|
+
const emptyRemovePlan = planCursorMcpRemove(emptyAfterRemovePath);
|
|
494
|
+
assert.equal("targetPath" in emptyRemovePlan, true);
|
|
495
|
+
if ("targetPath" in emptyRemovePlan) {
|
|
496
|
+
executeCursorMcpWrite(emptyRemovePlan);
|
|
497
|
+
const emptyRemovedContent = JSON.parse(readFileSync(emptyAfterRemovePath, "utf8"));
|
|
498
|
+
assert.deepEqual(emptyRemovedContent.mcpServers, {});
|
|
499
|
+
}
|
|
500
|
+
// Test Cursor rules path and content generation
|
|
501
|
+
const rulesProject = join(root, "rules-project");
|
|
502
|
+
mkdirSync(rulesProject);
|
|
503
|
+
const rulesPath = getCursorProjectRulesPath(rulesProject);
|
|
504
|
+
assert.equal(rulesPath, join(rulesProject, ".cursor", "rules", "openpets.mdc"));
|
|
505
|
+
const expectedRule = buildCursorOpenPetsRule();
|
|
506
|
+
assert.equal(buildCursorRulesPreview(), expectedRule);
|
|
507
|
+
assert.match(expectedRule, /description: Use OpenPets MCP tools/);
|
|
508
|
+
assert.doesNotMatch(expectedRule, /alwaysApply:\s*true/);
|
|
509
|
+
assert.match(expectedRule, /burmese_say/);
|
|
510
|
+
assert.match(expectedRule, /Do not send prompts, tool input\/output/);
|
|
511
|
+
const missingRulesResult = readCursorOpenPetsRules(rulesProject);
|
|
512
|
+
assert.equal(missingRulesResult.ok, true);
|
|
513
|
+
if (missingRulesResult.ok) {
|
|
514
|
+
assert.equal(missingRulesResult.exists, false);
|
|
515
|
+
}
|
|
516
|
+
const missingRulesStatus = classifyCursorRulesStatus(missingRulesResult, rulesPath);
|
|
517
|
+
assert.equal(missingRulesStatus.status, "missing");
|
|
518
|
+
assert.equal(missingRulesStatus.canInstall, true);
|
|
519
|
+
assert.equal(missingRulesStatus.canReplace, false);
|
|
520
|
+
assert.equal(missingRulesStatus.canRemove, false);
|
|
521
|
+
const rulesInstallPlan = planCursorRulesInstall(rulesProject);
|
|
522
|
+
assert.equal("targetPath" in rulesInstallPlan, true);
|
|
523
|
+
if ("targetPath" in rulesInstallPlan) {
|
|
524
|
+
executeCursorRulesWrite(rulesInstallPlan);
|
|
525
|
+
assert.equal(readFileSync(rulesPath, "utf8"), expectedRule);
|
|
526
|
+
}
|
|
527
|
+
const installedRulesStatus = classifyCursorRulesStatus(readCursorOpenPetsRules(rulesProject), rulesPath);
|
|
528
|
+
assert.equal(installedRulesStatus.status, "installed");
|
|
529
|
+
assert.equal(installedRulesStatus.canInstall, false);
|
|
530
|
+
assert.equal(installedRulesStatus.canRemove, true);
|
|
531
|
+
assert.equal(isManagedCursorOpenPetsRule(expectedRule), true);
|
|
532
|
+
const changedManagedRule = expectedRule.replace("major milestones", "meaningful milestones");
|
|
533
|
+
writeFileSync(rulesPath, changedManagedRule, "utf8");
|
|
534
|
+
const needsUpdateRulesStatus = classifyCursorRulesStatus(readCursorOpenPetsRules(rulesProject), rulesPath);
|
|
535
|
+
assert.equal(needsUpdateRulesStatus.status, "needs-update");
|
|
536
|
+
const updateRulesPlan = planCursorRulesInstall(rulesProject);
|
|
537
|
+
assert.equal("targetPath" in updateRulesPlan, true);
|
|
538
|
+
if ("targetPath" in updateRulesPlan) {
|
|
539
|
+
assert.equal(updateRulesPlan.backupPath !== undefined, true);
|
|
540
|
+
executeCursorRulesWrite(updateRulesPlan);
|
|
541
|
+
assert.equal(readFileSync(rulesPath, "utf8"), expectedRule);
|
|
542
|
+
assert.equal(existsSync(updateRulesPlan.backupPath), true);
|
|
543
|
+
}
|
|
544
|
+
const removeRulesPlan = planCursorRulesRemove(rulesProject);
|
|
545
|
+
assert.equal("targetPath" in removeRulesPlan, true);
|
|
546
|
+
if ("targetPath" in removeRulesPlan) {
|
|
547
|
+
executeCursorRulesWrite(removeRulesPlan);
|
|
548
|
+
assert.equal(existsSync(rulesPath), false);
|
|
549
|
+
assert.equal(existsSync(join(rulesProject, ".cursor", "rules")), true);
|
|
550
|
+
assert.equal(existsSync(removeRulesPlan.backupPath), true);
|
|
551
|
+
}
|
|
552
|
+
// Test rules conflicts and marker/frontmatter edge cases
|
|
553
|
+
mkdirSync(join(rulesProject, ".cursor", "rules"), { recursive: true });
|
|
554
|
+
writeFileSync(rulesPath, "User-authored Cursor rule\n", "utf8");
|
|
555
|
+
const unmanagedStatus = classifyCursorRulesStatus(readCursorOpenPetsRules(rulesProject), rulesPath);
|
|
556
|
+
assert.equal(unmanagedStatus.status, "conflict");
|
|
557
|
+
assert.equal(unmanagedStatus.canInstall, false);
|
|
558
|
+
assert.equal(unmanagedStatus.canReplace, true);
|
|
559
|
+
assert.equal(unmanagedStatus.canRemove, false);
|
|
560
|
+
const noWriteRulesConflict = planCursorRulesInstall(rulesProject);
|
|
561
|
+
assert.equal("ok" in noWriteRulesConflict, true);
|
|
562
|
+
if ("ok" in noWriteRulesConflict) {
|
|
563
|
+
assert.equal(noWriteRulesConflict.ok, false);
|
|
564
|
+
}
|
|
565
|
+
const noRemoveRulesConflict = planCursorRulesRemove(rulesProject);
|
|
566
|
+
assert.equal("ok" in noRemoveRulesConflict, true);
|
|
567
|
+
if ("ok" in noRemoveRulesConflict) {
|
|
568
|
+
assert.equal(noRemoveRulesConflict.ok, false);
|
|
569
|
+
}
|
|
570
|
+
const replaceRulesPlan = planCursorRulesReplace(rulesProject);
|
|
571
|
+
assert.equal("targetPath" in replaceRulesPlan, true);
|
|
572
|
+
if ("targetPath" in replaceRulesPlan) {
|
|
573
|
+
assert.equal(replaceRulesPlan.backupPath !== undefined, true);
|
|
574
|
+
executeCursorRulesWrite(replaceRulesPlan);
|
|
575
|
+
assert.equal(readFileSync(rulesPath, "utf8"), expectedRule);
|
|
576
|
+
assert.equal(readFileSync(replaceRulesPlan.backupPath, "utf8"), "User-authored Cursor rule\n");
|
|
577
|
+
}
|
|
578
|
+
const duplicateMarkers = expectedRule.replace(cursorRulesEndMarker, `${cursorRulesEndMarker}\n${cursorRulesEndMarker}`);
|
|
579
|
+
const reversedMarkers = `---\ndescription: Use OpenPets MCP tools for lightweight coding-status feedback.\n---\n\n${cursorRulesEndMarker}\nbody\n${cursorRulesStartMarker}\n`;
|
|
580
|
+
const missingMarker = expectedRule.replace(cursorRulesStartMarker, "");
|
|
581
|
+
const userBefore = `User note\n${expectedRule}`;
|
|
582
|
+
const userAfter = `${expectedRule}\nUser note\n`;
|
|
583
|
+
const unknownFrontmatter = expectedRule.replace("---\ndescription", "---\nalwaysApply: true\ndescription");
|
|
584
|
+
for (const content of [duplicateMarkers, reversedMarkers, missingMarker, userBefore, userAfter, unknownFrontmatter]) {
|
|
585
|
+
writeFileSync(rulesPath, content, "utf8");
|
|
586
|
+
const status = classifyCursorRulesStatus(readCursorOpenPetsRules(rulesProject), rulesPath);
|
|
587
|
+
assert.equal(status.status, "conflict");
|
|
588
|
+
}
|
|
589
|
+
// Test rules invalid path, symlink, non-regular, and oversized handling
|
|
590
|
+
const rulesFileParentProject = join(root, "rules-file-parent");
|
|
591
|
+
mkdirSync(rulesFileParentProject);
|
|
592
|
+
writeFileSync(join(rulesFileParentProject, ".cursor"), "not a dir", "utf8");
|
|
593
|
+
const rulesFileParentResult = readCursorOpenPetsRules(rulesFileParentProject);
|
|
594
|
+
assert.equal(rulesFileParentResult.ok, false);
|
|
595
|
+
if (!rulesFileParentResult.ok)
|
|
596
|
+
assert.equal(rulesFileParentResult.reason, "unsafe-path");
|
|
597
|
+
const rulesSymlinkProject = join(root, "rules-symlink");
|
|
598
|
+
const rulesSymlinkOutside = join(root, "rules-outside");
|
|
599
|
+
mkdirSync(rulesSymlinkProject);
|
|
600
|
+
mkdirSync(rulesSymlinkOutside);
|
|
601
|
+
symlinkSync(rulesSymlinkOutside, join(rulesSymlinkProject, ".cursor"));
|
|
602
|
+
const rulesSymlinkResult = readCursorOpenPetsRules(rulesSymlinkProject);
|
|
603
|
+
assert.equal(rulesSymlinkResult.ok, false);
|
|
604
|
+
if (!rulesSymlinkResult.ok)
|
|
605
|
+
assert.equal(rulesSymlinkResult.reason, "symlink");
|
|
606
|
+
const rulesFileSymlinkProject = join(root, "rules-file-symlink");
|
|
607
|
+
mkdirSync(join(rulesFileSymlinkProject, ".cursor", "rules"), { recursive: true });
|
|
608
|
+
const rulesFileSymlinkTarget = join(root, "rules-file-target.mdc");
|
|
609
|
+
writeFileSync(rulesFileSymlinkTarget, expectedRule, "utf8");
|
|
610
|
+
symlinkSync(rulesFileSymlinkTarget, getCursorProjectRulesPath(rulesFileSymlinkProject));
|
|
611
|
+
const rulesFileSymlinkResult = readCursorOpenPetsRules(rulesFileSymlinkProject);
|
|
612
|
+
assert.equal(rulesFileSymlinkResult.ok, false);
|
|
613
|
+
if (!rulesFileSymlinkResult.ok)
|
|
614
|
+
assert.equal(rulesFileSymlinkResult.reason, "symlink");
|
|
615
|
+
const rulesFileSymlinkPlan = planCursorRulesInstall(rulesFileSymlinkProject);
|
|
616
|
+
assert.equal("ok" in rulesFileSymlinkPlan, true);
|
|
617
|
+
if ("ok" in rulesFileSymlinkPlan)
|
|
618
|
+
assert.equal(rulesFileSymlinkPlan.ok, false);
|
|
619
|
+
const danglingRulesSymlinkProject = join(root, "rules-dangling-symlink");
|
|
620
|
+
mkdirSync(join(danglingRulesSymlinkProject, ".cursor", "rules"), { recursive: true });
|
|
621
|
+
symlinkSync(join(root, "missing-rules-target.mdc"), getCursorProjectRulesPath(danglingRulesSymlinkProject));
|
|
622
|
+
const danglingRulesResult = readCursorOpenPetsRules(danglingRulesSymlinkProject);
|
|
623
|
+
assert.equal(danglingRulesResult.ok, false);
|
|
624
|
+
if (!danglingRulesResult.ok)
|
|
625
|
+
assert.equal(danglingRulesResult.reason, "symlink");
|
|
626
|
+
const nonRegularRulesProject = join(root, "rules-non-regular");
|
|
627
|
+
mkdirSync(join(nonRegularRulesProject, ".cursor", "rules"), { recursive: true });
|
|
628
|
+
mkdirSync(getCursorProjectRulesPath(nonRegularRulesProject));
|
|
629
|
+
const nonRegularRules = readCursorOpenPetsRules(nonRegularRulesProject);
|
|
630
|
+
assert.equal(nonRegularRules.ok, false);
|
|
631
|
+
if (!nonRegularRules.ok)
|
|
632
|
+
assert.equal(nonRegularRules.reason, "not-regular");
|
|
633
|
+
const nonRegularRulesInstallPlan = planCursorRulesInstall(nonRegularRulesProject);
|
|
634
|
+
assert.equal("ok" in nonRegularRulesInstallPlan, true);
|
|
635
|
+
if ("ok" in nonRegularRulesInstallPlan)
|
|
636
|
+
assert.equal(nonRegularRulesInstallPlan.ok, false);
|
|
637
|
+
assert.equal(lstatSync(getCursorProjectRulesPath(nonRegularRulesProject)).isDirectory(), true);
|
|
638
|
+
const oversizedRulesProject = join(root, "rules-oversized");
|
|
639
|
+
mkdirSync(join(oversizedRulesProject, ".cursor", "rules"), { recursive: true });
|
|
640
|
+
writeFileSync(getCursorProjectRulesPath(oversizedRulesProject), "x".repeat(maxCursorRulesBytes + 1), "utf8");
|
|
641
|
+
const oversizedRules = readCursorOpenPetsRules(oversizedRulesProject);
|
|
642
|
+
assert.equal(oversizedRules.ok, false);
|
|
643
|
+
if (!oversizedRules.ok)
|
|
644
|
+
assert.equal(oversizedRules.reason, "size");
|
|
645
|
+
const oversizedBefore = readFileSync(getCursorProjectRulesPath(oversizedRulesProject), "utf8");
|
|
646
|
+
const oversizedInstallPlan = planCursorRulesInstall(oversizedRulesProject);
|
|
647
|
+
assert.equal("ok" in oversizedInstallPlan, true);
|
|
648
|
+
if ("ok" in oversizedInstallPlan)
|
|
649
|
+
assert.equal(oversizedInstallPlan.ok, false);
|
|
650
|
+
const oversizedRemovePlan = planCursorRulesRemove(oversizedRulesProject);
|
|
651
|
+
assert.equal("ok" in oversizedRemovePlan, true);
|
|
652
|
+
if ("ok" in oversizedRemovePlan)
|
|
653
|
+
assert.equal(oversizedRemovePlan.ok, false);
|
|
654
|
+
assert.equal(readFileSync(getCursorProjectRulesPath(oversizedRulesProject), "utf8"), oversizedBefore);
|
|
655
|
+
console.error("Cursor validation passed.");
|
|
656
|
+
}
|
|
657
|
+
finally {
|
|
658
|
+
rmSync(root, { recursive: true, force: true });
|
|
659
|
+
}
|
|
660
|
+
//# sourceMappingURL=check-cursor.js.map
|