@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.
@@ -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