@cpmai/cli 0.2.0-beta.1 → 0.3.0-beta.2

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.
Files changed (2) hide show
  1. package/dist/index.js +763 -423
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4,107 +4,6 @@
4
4
  import { Command } from "commander";
5
5
  import chalk5 from "chalk";
6
6
 
7
- // src/adapters/base.ts
8
- var PlatformAdapter = class {
9
- };
10
-
11
- // src/adapters/handlers/handler-registry.ts
12
- var HandlerRegistry = class {
13
- /**
14
- * Internal storage for handlers.
15
- * Maps package type strings to their handler instances.
16
- * Using Map for O(1) lookup performance.
17
- */
18
- handlers = /* @__PURE__ */ new Map();
19
- /**
20
- * Register a handler for a specific package type.
21
- *
22
- * When you create a new handler (like RulesHandler), you call this method
23
- * to add it to the registry so it can be found later.
24
- *
25
- * @param handler - The handler instance to register. The handler's
26
- * packageType property determines which type it handles.
27
- *
28
- * @example
29
- * ```typescript
30
- * const rulesHandler = new RulesHandler();
31
- * registry.register(rulesHandler);
32
- * // Now "rules" type packages will use RulesHandler
33
- * ```
34
- */
35
- register(handler) {
36
- this.handlers.set(handler.packageType, handler);
37
- }
38
- /**
39
- * Get the handler for a specific package type.
40
- *
41
- * Use this when you need to install or uninstall a package.
42
- * It returns the appropriate handler based on the package type.
43
- *
44
- * @param type - The package type to find a handler for (e.g., "rules", "skill", "mcp")
45
- * @returns The handler that can process this package type
46
- * @throws Error if no handler is registered for the given type
47
- *
48
- * @example
49
- * ```typescript
50
- * const handler = registry.getHandler("skill");
51
- * const files = await handler.install(manifest, context);
52
- * ```
53
- */
54
- getHandler(type) {
55
- const handler = this.handlers.get(type);
56
- if (!handler) {
57
- throw new Error(`No handler registered for package type: ${type}`);
58
- }
59
- return handler;
60
- }
61
- /**
62
- * Check if a handler exists for a specific package type.
63
- *
64
- * Useful when you want to check availability before attempting
65
- * to get a handler, avoiding the need for try-catch blocks.
66
- *
67
- * @param type - The package type to check
68
- * @returns true if a handler is registered, false otherwise
69
- *
70
- * @example
71
- * ```typescript
72
- * if (registry.hasHandler("mcp")) {
73
- * const handler = registry.getHandler("mcp");
74
- * // ... use handler
75
- * } else {
76
- * console.log("MCP packages not supported");
77
- * }
78
- * ```
79
- */
80
- hasHandler(type) {
81
- return this.handlers.has(type);
82
- }
83
- /**
84
- * Get a list of all registered package types.
85
- *
86
- * Useful for debugging, displaying supported types to users,
87
- * or iterating over all available handlers.
88
- *
89
- * @returns Array of package type strings that have registered handlers
90
- *
91
- * @example
92
- * ```typescript
93
- * const types = registry.getRegisteredTypes();
94
- * console.log("Supported types:", types.join(", "));
95
- * // Output: "Supported types: rules, skill, mcp"
96
- * ```
97
- */
98
- getRegisteredTypes() {
99
- return Array.from(this.handlers.keys());
100
- }
101
- };
102
- var handlerRegistry = new HandlerRegistry();
103
-
104
- // src/adapters/handlers/rules-handler.ts
105
- import fs2 from "fs-extra";
106
- import path6 from "path";
107
-
108
7
  // src/constants.ts
109
8
  var TIMEOUTS = {
110
9
  MANIFEST_FETCH: 5e3,
@@ -148,10 +47,7 @@ var SEARCH_SORT_OPTIONS = [
148
47
  "name"
149
48
  // Alphabetical order
150
49
  ];
151
- var VALID_PLATFORMS = [
152
- "claude-code"
153
- // Currently the only supported platform
154
- ];
50
+ var VALID_PLATFORMS = ["claude-code", "cursor"];
155
51
  var ALLOWED_MCP_COMMANDS = [
156
52
  "npx",
157
53
  // Node package executor
@@ -171,15 +67,19 @@ var ALLOWED_MCP_COMMANDS = [
171
67
  var BLOCKED_MCP_ARG_PATTERNS = [
172
68
  /--eval/i,
173
69
  // Node.js eval flag
174
- /-e\s/,
175
- // Short eval flag with space
176
- /-c\s/,
177
- // Command flag with space
70
+ /-e(?:\s|$)/,
71
+ // Short eval flag (with space or at end)
72
+ /^-e\S/,
73
+ // Concatenated eval flag (e.g., -eCODE)
74
+ /-c(?:\s|$)/,
75
+ // Command flag (with space or at end)
76
+ /^-c\S/,
77
+ // Concatenated command flag (e.g., -cCODE)
178
78
  /\bcurl\b/i,
179
79
  // curl command (data exfiltration)
180
80
  /\bwget\b/i,
181
81
  // wget command (data exfiltration)
182
- /\brm\s/i,
82
+ /\brm(?:\s|$)/i,
183
83
  // rm command (file deletion)
184
84
  /\bsudo\b/i,
185
85
  // sudo command (privilege escalation)
@@ -187,8 +87,44 @@ var BLOCKED_MCP_ARG_PATTERNS = [
187
87
  // chmod command (permission changes)
188
88
  /\bchown\b/i,
189
89
  // chown command (ownership changes)
190
- /[|;&`$]/
90
+ /[|;&`$]/,
191
91
  // Shell metacharacters (command chaining/injection)
92
+ /--inspect/i,
93
+ // Node.js debugger (remote code execution)
94
+ /--allow-all/i,
95
+ // Deno sandbox bypass
96
+ /--allow-run/i,
97
+ // Deno run permission
98
+ /--allow-write/i,
99
+ // Deno write permission
100
+ /--allow-net/i,
101
+ // Deno net permission
102
+ /^https?:\/\//i
103
+ // Remote URLs as standalone args (script loading)
104
+ ];
105
+ var BLOCKED_MCP_ENV_KEYS = [
106
+ "PATH",
107
+ "LD_PRELOAD",
108
+ "LD_LIBRARY_PATH",
109
+ "DYLD_INSERT_LIBRARIES",
110
+ "DYLD_LIBRARY_PATH",
111
+ "NODE_OPTIONS",
112
+ "NODE_PATH",
113
+ "PYTHONPATH",
114
+ "PYTHONSTARTUP",
115
+ "PYTHONHOME",
116
+ "RUBYOPT",
117
+ "PERL5OPT",
118
+ "BASH_ENV",
119
+ "ENV",
120
+ "CDPATH",
121
+ "HOME",
122
+ "USERPROFILE",
123
+ "NPM_CONFIG_REGISTRY",
124
+ "NPM_CONFIG_PREFIX",
125
+ "NPM_CONFIG_GLOBALCONFIG",
126
+ "DENO_DIR",
127
+ "BUN_INSTALL"
192
128
  ];
193
129
 
194
130
  // src/types.ts
@@ -210,15 +146,15 @@ function isSkillManifest(manifest) {
210
146
  function isMcpManifest(manifest) {
211
147
  return manifest.type === "mcp";
212
148
  }
213
- function getTypeFromPath(path15) {
214
- if (path15.startsWith("skills/")) return "skill";
215
- if (path15.startsWith("rules/")) return "rules";
216
- if (path15.startsWith("mcp/")) return "mcp";
217
- if (path15.startsWith("agents/")) return "agent";
218
- if (path15.startsWith("hooks/")) return "hook";
219
- if (path15.startsWith("workflows/")) return "workflow";
220
- if (path15.startsWith("templates/")) return "template";
221
- if (path15.startsWith("bundles/")) return "bundle";
149
+ function getTypeFromPath(path16) {
150
+ if (path16.startsWith("skills/")) return "skill";
151
+ if (path16.startsWith("rules/")) return "rules";
152
+ if (path16.startsWith("mcp/")) return "mcp";
153
+ if (path16.startsWith("agents/")) return "agent";
154
+ if (path16.startsWith("hooks/")) return "hook";
155
+ if (path16.startsWith("workflows/")) return "workflow";
156
+ if (path16.startsWith("templates/")) return "template";
157
+ if (path16.startsWith("bundles/")) return "bundle";
222
158
  return null;
223
159
  }
224
160
  function resolvePackageType(pkg) {
@@ -230,8 +166,110 @@ function resolvePackageType(pkg) {
230
166
  throw new Error(`Cannot determine type for package: ${pkg.name}`);
231
167
  }
232
168
 
169
+ // src/adapters/base.ts
170
+ var PlatformAdapter = class {
171
+ };
172
+
173
+ // src/adapters/handlers/handler-registry.ts
174
+ var HandlerRegistry = class {
175
+ /**
176
+ * Internal storage for handlers.
177
+ * Maps package type strings to their handler instances.
178
+ * Using Map for O(1) lookup performance.
179
+ */
180
+ handlers = /* @__PURE__ */ new Map();
181
+ /**
182
+ * Register a handler for a specific package type.
183
+ *
184
+ * When you create a new handler (like RulesHandler), you call this method
185
+ * to add it to the registry so it can be found later.
186
+ *
187
+ * @param handler - The handler instance to register. The handler's
188
+ * packageType property determines which type it handles.
189
+ *
190
+ * @example
191
+ * ```typescript
192
+ * const rulesHandler = new RulesHandler();
193
+ * registry.register(rulesHandler);
194
+ * // Now "rules" type packages will use RulesHandler
195
+ * ```
196
+ */
197
+ register(handler) {
198
+ this.handlers.set(handler.packageType, handler);
199
+ }
200
+ /**
201
+ * Get the handler for a specific package type.
202
+ *
203
+ * Use this when you need to install or uninstall a package.
204
+ * It returns the appropriate handler based on the package type.
205
+ *
206
+ * @param type - The package type to find a handler for (e.g., "rules", "skill", "mcp")
207
+ * @returns The handler that can process this package type
208
+ * @throws Error if no handler is registered for the given type
209
+ *
210
+ * @example
211
+ * ```typescript
212
+ * const handler = registry.getHandler("skill");
213
+ * const files = await handler.install(manifest, context);
214
+ * ```
215
+ */
216
+ getHandler(type) {
217
+ const handler = this.handlers.get(type);
218
+ if (!handler) {
219
+ throw new Error(`No handler registered for package type: ${type}`);
220
+ }
221
+ return handler;
222
+ }
223
+ /**
224
+ * Check if a handler exists for a specific package type.
225
+ *
226
+ * Useful when you want to check availability before attempting
227
+ * to get a handler, avoiding the need for try-catch blocks.
228
+ *
229
+ * @param type - The package type to check
230
+ * @returns true if a handler is registered, false otherwise
231
+ *
232
+ * @example
233
+ * ```typescript
234
+ * if (registry.hasHandler("mcp")) {
235
+ * const handler = registry.getHandler("mcp");
236
+ * // ... use handler
237
+ * } else {
238
+ * console.log("MCP packages not supported");
239
+ * }
240
+ * ```
241
+ */
242
+ hasHandler(type) {
243
+ return this.handlers.has(type);
244
+ }
245
+ /**
246
+ * Get a list of all registered package types.
247
+ *
248
+ * Useful for debugging, displaying supported types to users,
249
+ * or iterating over all available handlers.
250
+ *
251
+ * @returns Array of package type strings that have registered handlers
252
+ *
253
+ * @example
254
+ * ```typescript
255
+ * const types = registry.getRegisteredTypes();
256
+ * console.log("Supported types:", types.join(", "));
257
+ * // Output: "Supported types: rules, skill, mcp"
258
+ * ```
259
+ */
260
+ getRegisteredTypes() {
261
+ return Array.from(this.handlers.keys());
262
+ }
263
+ };
264
+ var handlerRegistry = new HandlerRegistry();
265
+
266
+ // src/adapters/handlers/rules-handler.ts
267
+ import fs3 from "fs-extra";
268
+ import path6 from "path";
269
+
233
270
  // src/utils/platform.ts
234
271
  import path2 from "path";
272
+ import os2 from "os";
235
273
 
236
274
  // src/utils/config.ts
237
275
  import path from "path";
@@ -245,13 +283,28 @@ async function ensureClaudeDirs() {
245
283
  await fs.ensureDir(path.join(claudeHome, "rules"));
246
284
  await fs.ensureDir(path.join(claudeHome, "skills"));
247
285
  }
286
+ async function ensureCursorDirs(projectPath) {
287
+ await fs.ensureDir(path.join(projectPath, ".cursor", "rules"));
288
+ }
248
289
 
249
290
  // src/utils/platform.ts
250
- function getRulesPath(platform) {
251
- if (platform !== "claude-code") {
252
- throw new Error(`Rules path is not supported for platform: ${platform}`);
291
+ function getCursorHome() {
292
+ return path2.join(os2.homedir(), ".cursor");
293
+ }
294
+ function getCursorMcpConfigPath() {
295
+ return path2.join(getCursorHome(), "mcp.json");
296
+ }
297
+ function getRulesPath(platform, projectPath) {
298
+ if (platform === "claude-code") {
299
+ return path2.join(getClaudeHome(), "rules");
253
300
  }
254
- return path2.join(getClaudeHome(), "rules");
301
+ if (platform === "cursor") {
302
+ if (!projectPath) {
303
+ return path2.join(process.cwd(), ".cursor", "rules");
304
+ }
305
+ return path2.join(projectPath, ".cursor", "rules");
306
+ }
307
+ throw new Error(`Rules path is not supported for platform: ${platform}`);
255
308
  }
256
309
  function getSkillsPath() {
257
310
  return path2.join(getClaudeHome(), "skills");
@@ -331,14 +384,22 @@ var logger = {
331
384
  };
332
385
 
333
386
  // src/security/mcp-validator.ts
334
- import path3 from "path";
335
387
  function isAllowedCommand(command) {
336
- const baseCommand = path3.basename(command);
388
+ if (command.includes("/") || command.includes("\\")) {
389
+ return false;
390
+ }
337
391
  return ALLOWED_MCP_COMMANDS.includes(
338
- baseCommand
392
+ command
339
393
  );
340
394
  }
341
395
  function containsBlockedPattern(args) {
396
+ for (const arg of args) {
397
+ for (const pattern of BLOCKED_MCP_ARG_PATTERNS) {
398
+ if (pattern.test(arg)) {
399
+ return pattern;
400
+ }
401
+ }
402
+ }
342
403
  const argsString = args.join(" ");
343
404
  for (const pattern of BLOCKED_MCP_ARG_PATTERNS) {
344
405
  if (pattern.test(argsString)) {
@@ -347,15 +408,23 @@ function containsBlockedPattern(args) {
347
408
  }
348
409
  return null;
349
410
  }
411
+ function containsBlockedEnvKey(env) {
412
+ const blockedSet = new Set(BLOCKED_MCP_ENV_KEYS.map((k) => k.toUpperCase()));
413
+ for (const key of Object.keys(env)) {
414
+ if (blockedSet.has(key.toUpperCase())) {
415
+ return key;
416
+ }
417
+ }
418
+ return null;
419
+ }
350
420
  function validateMcpConfig(mcp) {
351
421
  if (!mcp?.command) {
352
422
  return { valid: false, error: "MCP command is required" };
353
423
  }
354
- const baseCommand = path3.basename(mcp.command);
355
424
  if (!isAllowedCommand(mcp.command)) {
356
425
  return {
357
426
  valid: false,
358
- error: `MCP command '${baseCommand}' is not allowed. Allowed: ${ALLOWED_MCP_COMMANDS.join(", ")}`
427
+ error: `MCP command '${mcp.command}' is not allowed. Allowed: ${ALLOWED_MCP_COMMANDS.join(", ")}`
359
428
  };
360
429
  }
361
430
  if (mcp.args) {
@@ -367,16 +436,25 @@ function validateMcpConfig(mcp) {
367
436
  };
368
437
  }
369
438
  }
439
+ if (mcp.env) {
440
+ const blockedKey = containsBlockedEnvKey(mcp.env);
441
+ if (blockedKey) {
442
+ return {
443
+ valid: false,
444
+ error: `MCP environment variable '${blockedKey}' is not allowed. It could be used to bypass command security restrictions.`
445
+ };
446
+ }
447
+ }
370
448
  return { valid: true };
371
449
  }
372
450
 
373
451
  // src/security/file-sanitizer.ts
374
- import path4 from "path";
452
+ import path3 from "path";
375
453
  function sanitizeFileName(fileName) {
376
454
  if (!fileName || typeof fileName !== "string") {
377
455
  return { valid: false, error: "File name cannot be empty", sanitized: "" };
378
456
  }
379
- const baseName = path4.basename(fileName);
457
+ const baseName = path3.basename(fileName);
380
458
  if (baseName.includes("\0")) {
381
459
  return {
382
460
  valid: false,
@@ -421,12 +499,12 @@ function sanitizeFolderName(name) {
421
499
  if (!sanitized || sanitized.startsWith(".")) {
422
500
  throw new Error(`Invalid package name: ${name}`);
423
501
  }
424
- const normalized = path4.normalize(sanitized);
502
+ const normalized = path3.normalize(sanitized);
425
503
  if (normalized !== sanitized || normalized.includes("..")) {
426
504
  throw new Error(`Invalid package name (path traversal detected): ${name}`);
427
505
  }
428
- const testPath = path4.join("/test", sanitized);
429
- const resolved = path4.resolve(testPath);
506
+ const testPath = path3.join("/test", sanitized);
507
+ const resolved = path3.resolve(testPath);
430
508
  if (!resolved.startsWith("/test/")) {
431
509
  throw new Error(`Invalid package name (path traversal detected): ${name}`);
432
510
  }
@@ -434,26 +512,79 @@ function sanitizeFolderName(name) {
434
512
  }
435
513
 
436
514
  // src/security/path-validator.ts
437
- import path5 from "path";
515
+ import path4 from "path";
438
516
  function isPathWithinDirectory(filePath, directory) {
439
- const resolvedPath = path5.resolve(filePath);
440
- const resolvedDir = path5.resolve(directory);
441
- return resolvedPath.startsWith(resolvedDir + path5.sep) || resolvedPath === resolvedDir;
517
+ const resolvedPath = path4.resolve(filePath);
518
+ const resolvedDir = path4.resolve(directory);
519
+ return resolvedPath.startsWith(resolvedDir + path4.sep) || resolvedPath === resolvedDir;
520
+ }
521
+
522
+ // src/security/glob-validator.ts
523
+ var BLOCKED_GLOB_PATTERNS = [
524
+ // Environment and secret files
525
+ { pattern: /\.env\b/i, reason: "targets environment/secret files" },
526
+ { pattern: /\.secret/i, reason: "targets secret files" },
527
+ { pattern: /credentials/i, reason: "targets credential files" },
528
+ { pattern: /\.pem$/i, reason: "targets PEM certificate/key files" },
529
+ { pattern: /\.key$/i, reason: "targets key files" },
530
+ { pattern: /\.p12$/i, reason: "targets PKCS12 certificate files" },
531
+ { pattern: /\.pfx$/i, reason: "targets PFX certificate files" },
532
+ // SSH and GPG
533
+ { pattern: /\.ssh\//i, reason: "targets SSH directory" },
534
+ { pattern: /id_rsa/i, reason: "targets SSH private keys" },
535
+ { pattern: /id_ed25519/i, reason: "targets SSH private keys" },
536
+ { pattern: /\.gnupg\//i, reason: "targets GPG directory" },
537
+ // Git internals
538
+ { pattern: /\.git\//, reason: "targets git internals" },
539
+ // Config files with potential secrets
540
+ { pattern: /\.claude\.json$/i, reason: "targets Claude Code config" },
541
+ { pattern: /\.npmrc$/i, reason: "targets npm config (may contain tokens)" },
542
+ { pattern: /\.pypirc$/i, reason: "targets PyPI config (may contain tokens)" },
543
+ // System files
544
+ { pattern: /\/etc\//, reason: "targets system configuration" },
545
+ { pattern: /\/passwd/, reason: "targets system password file" },
546
+ { pattern: /\/shadow/, reason: "targets system shadow file" },
547
+ // Path traversal in globs
548
+ { pattern: /\.\.\//, reason: "contains path traversal" }
549
+ ];
550
+ function validateGlob(glob) {
551
+ if (!glob || typeof glob !== "string") {
552
+ return { valid: false, error: "Glob pattern cannot be empty" };
553
+ }
554
+ if (glob.includes("\0")) {
555
+ return { valid: false, error: "Glob pattern contains null bytes" };
556
+ }
557
+ for (const { pattern, reason } of BLOCKED_GLOB_PATTERNS) {
558
+ if (pattern.test(glob)) {
559
+ return {
560
+ valid: false,
561
+ error: `Glob pattern "${glob}" is blocked: ${reason}`
562
+ };
563
+ }
564
+ }
565
+ return { valid: true };
566
+ }
567
+ function validateGlobs(globs) {
568
+ for (const glob of globs) {
569
+ const result = validateGlob(glob);
570
+ if (!result.valid) {
571
+ return result;
572
+ }
573
+ }
574
+ return { valid: true };
442
575
  }
443
576
 
444
- // src/adapters/handlers/rules-handler.ts
577
+ // src/adapters/handlers/metadata.ts
578
+ import fs2 from "fs-extra";
579
+ import path5 from "path";
445
580
  async function writePackageMetadata(packageDir, manifest) {
446
581
  const metadata = {
447
582
  name: manifest.name,
448
- // e.g., "@cpm/typescript-strict"
449
583
  version: manifest.version,
450
- // e.g., "1.0.0"
451
584
  type: manifest.type,
452
- // e.g., "rules"
453
585
  installedAt: (/* @__PURE__ */ new Date()).toISOString()
454
- // ISO timestamp for when it was installed
455
586
  };
456
- const metadataPath = path6.join(packageDir, ".cpm.json");
587
+ const metadataPath = path5.join(packageDir, ".cpm.json");
457
588
  try {
458
589
  await fs2.writeJson(metadataPath, metadata, { spaces: 2 });
459
590
  } catch (error) {
@@ -463,6 +594,8 @@ async function writePackageMetadata(packageDir, manifest) {
463
594
  }
464
595
  return metadataPath;
465
596
  }
597
+
598
+ // src/adapters/handlers/rules-handler.ts
466
599
  var RulesHandler = class {
467
600
  /**
468
601
  * Identifies this handler as handling "rules" type packages.
@@ -487,9 +620,9 @@ var RulesHandler = class {
487
620
  const rulesBaseDir = getRulesPath("claude-code");
488
621
  const folderName = sanitizeFolderName(manifest.name);
489
622
  const rulesDir = path6.join(rulesBaseDir, folderName);
490
- await fs2.ensureDir(rulesDir);
491
- if (context.packagePath && await fs2.pathExists(context.packagePath)) {
492
- const files = await fs2.readdir(context.packagePath);
623
+ await fs3.ensureDir(rulesDir);
624
+ if (context.packagePath && await fs3.pathExists(context.packagePath)) {
625
+ const files = await fs3.readdir(context.packagePath);
493
626
  const mdFiles = files.filter(
494
627
  (f) => f.endsWith(".md") && f.toLowerCase() !== "cpm.yaml"
495
628
  );
@@ -506,7 +639,12 @@ var RulesHandler = class {
506
639
  logger.warn(`Blocked path traversal attempt: ${file}`);
507
640
  continue;
508
641
  }
509
- await fs2.copy(srcPath, destPath);
642
+ const srcStat = await fs3.lstat(srcPath);
643
+ if (srcStat.isSymbolicLink()) {
644
+ logger.warn(`Blocked symlink in package: ${file}`);
645
+ continue;
646
+ }
647
+ await fs3.copy(srcPath, destPath);
510
648
  filesWritten.push(destPath);
511
649
  }
512
650
  const metadataPath2 = await writePackageMetadata(rulesDir, manifest);
@@ -523,7 +661,7 @@ ${manifest.description}
523
661
 
524
662
  ${rulesContent.trim()}
525
663
  `;
526
- await fs2.writeFile(rulesPath, content, "utf-8");
664
+ await fs3.writeFile(rulesPath, content, "utf-8");
527
665
  filesWritten.push(rulesPath);
528
666
  const metadataPath = await writePackageMetadata(rulesDir, manifest);
529
667
  filesWritten.push(metadataPath);
@@ -543,8 +681,8 @@ ${rulesContent.trim()}
543
681
  const folderName = sanitizeFolderName(packageName);
544
682
  const rulesBaseDir = getRulesPath("claude-code");
545
683
  const rulesPath = path6.join(rulesBaseDir, folderName);
546
- if (await fs2.pathExists(rulesPath)) {
547
- await fs2.remove(rulesPath);
684
+ if (await fs3.pathExists(rulesPath)) {
685
+ await fs3.remove(rulesPath);
548
686
  filesRemoved.push(rulesPath);
549
687
  }
550
688
  return filesRemoved;
@@ -568,29 +706,8 @@ ${rulesContent.trim()}
568
706
  };
569
707
 
570
708
  // src/adapters/handlers/skill-handler.ts
571
- import fs3 from "fs-extra";
709
+ import fs4 from "fs-extra";
572
710
  import path7 from "path";
573
- async function writePackageMetadata2(packageDir, manifest) {
574
- const metadata = {
575
- name: manifest.name,
576
- // e.g., "@cpm/commit-skill"
577
- version: manifest.version,
578
- // e.g., "1.0.0"
579
- type: manifest.type,
580
- // e.g., "skill"
581
- installedAt: (/* @__PURE__ */ new Date()).toISOString()
582
- // ISO timestamp for when it was installed
583
- };
584
- const metadataPath = path7.join(packageDir, ".cpm.json");
585
- try {
586
- await fs3.writeJson(metadataPath, metadata, { spaces: 2 });
587
- } catch (error) {
588
- logger.warn(
589
- `Could not write metadata: ${error instanceof Error ? error.message : "Unknown error"}`
590
- );
591
- }
592
- return metadataPath;
593
- }
594
711
  function formatSkillMd(manifest) {
595
712
  const skill = manifest.skill;
596
713
  const content = manifest.universal?.prompt || manifest.universal?.rules || "";
@@ -634,9 +751,9 @@ var SkillHandler = class {
634
751
  const skillsDir = getSkillsPath();
635
752
  const folderName = sanitizeFolderName(manifest.name);
636
753
  const skillDir = path7.join(skillsDir, folderName);
637
- await fs3.ensureDir(skillDir);
638
- if (context.packagePath && await fs3.pathExists(context.packagePath)) {
639
- const files = await fs3.readdir(context.packagePath);
754
+ await fs4.ensureDir(skillDir);
755
+ if (context.packagePath && await fs4.pathExists(context.packagePath)) {
756
+ const files = await fs4.readdir(context.packagePath);
640
757
  const contentFiles = files.filter(
641
758
  (f) => f.endsWith(".md") && f.toLowerCase() !== "cpm.yaml"
642
759
  );
@@ -653,10 +770,15 @@ var SkillHandler = class {
653
770
  logger.warn(`Blocked path traversal attempt: ${file}`);
654
771
  continue;
655
772
  }
656
- await fs3.copy(srcPath, destPath);
773
+ const srcStat = await fs4.lstat(srcPath);
774
+ if (srcStat.isSymbolicLink()) {
775
+ logger.warn(`Blocked symlink in package: ${file}`);
776
+ continue;
777
+ }
778
+ await fs4.copy(srcPath, destPath);
657
779
  filesWritten.push(destPath);
658
780
  }
659
- const metadataPath = await writePackageMetadata2(skillDir, manifest);
781
+ const metadataPath = await writePackageMetadata(skillDir, manifest);
660
782
  filesWritten.push(metadataPath);
661
783
  return filesWritten;
662
784
  }
@@ -664,9 +786,9 @@ var SkillHandler = class {
664
786
  if (isSkillManifest(manifest)) {
665
787
  const skillContent = formatSkillMd(manifest);
666
788
  const skillPath = path7.join(skillDir, "SKILL.md");
667
- await fs3.writeFile(skillPath, skillContent, "utf-8");
789
+ await fs4.writeFile(skillPath, skillContent, "utf-8");
668
790
  filesWritten.push(skillPath);
669
- const metadataPath = await writePackageMetadata2(skillDir, manifest);
791
+ const metadataPath = await writePackageMetadata(skillDir, manifest);
670
792
  filesWritten.push(metadataPath);
671
793
  } else {
672
794
  const content = this.getUniversalContent(manifest);
@@ -678,9 +800,9 @@ ${manifest.description}
678
800
 
679
801
  ${content.trim()}
680
802
  `;
681
- await fs3.writeFile(skillPath, skillContent, "utf-8");
803
+ await fs4.writeFile(skillPath, skillContent, "utf-8");
682
804
  filesWritten.push(skillPath);
683
- const metadataPath = await writePackageMetadata2(skillDir, manifest);
805
+ const metadataPath = await writePackageMetadata(skillDir, manifest);
684
806
  filesWritten.push(metadataPath);
685
807
  }
686
808
  }
@@ -700,8 +822,8 @@ ${content.trim()}
700
822
  const folderName = sanitizeFolderName(packageName);
701
823
  const skillsDir = getSkillsPath();
702
824
  const skillPath = path7.join(skillsDir, folderName);
703
- if (await fs3.pathExists(skillPath)) {
704
- await fs3.remove(skillPath);
825
+ if (await fs4.pathExists(skillPath)) {
826
+ await fs4.remove(skillPath);
705
827
  filesRemoved.push(skillPath);
706
828
  }
707
829
  return filesRemoved;
@@ -724,28 +846,66 @@ ${content.trim()}
724
846
  };
725
847
 
726
848
  // src/adapters/handlers/mcp-handler.ts
727
- import fs4 from "fs-extra";
849
+ import path9 from "path";
850
+
851
+ // src/adapters/handlers/base-mcp-handler.ts
852
+ import fs6 from "fs-extra";
728
853
  import path8 from "path";
729
- var McpHandler = class {
730
- /**
731
- * Identifies this handler as handling "mcp" type packages.
732
- * The registry uses this to route MCP packages to this handler.
733
- */
854
+ import crypto from "crypto";
855
+
856
+ // src/utils/file-lock.ts
857
+ import fs5 from "fs-extra";
858
+ var LOCK_STALE_MS = 1e4;
859
+ var LOCK_RETRY_MS = 100;
860
+ var LOCK_MAX_RETRIES = 50;
861
+ async function acquireLock(filePath) {
862
+ const lockPath = `${filePath}.lock`;
863
+ for (let i = 0; i < LOCK_MAX_RETRIES; i++) {
864
+ try {
865
+ await fs5.writeFile(lockPath, String(Date.now()), { flag: "wx" });
866
+ return async () => {
867
+ try {
868
+ await fs5.remove(lockPath);
869
+ } catch {
870
+ }
871
+ };
872
+ } catch (error) {
873
+ const err = error;
874
+ if (err.code === "EEXIST") {
875
+ try {
876
+ const content = await fs5.readFile(lockPath, "utf-8");
877
+ const lockTime = parseInt(content, 10);
878
+ if (!isNaN(lockTime) && Date.now() - lockTime > LOCK_STALE_MS) {
879
+ await fs5.remove(lockPath);
880
+ continue;
881
+ }
882
+ } catch {
883
+ await fs5.remove(lockPath).catch(() => {
884
+ });
885
+ continue;
886
+ }
887
+ await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_MS));
888
+ continue;
889
+ }
890
+ throw error;
891
+ }
892
+ }
893
+ throw new Error(
894
+ `Could not acquire lock for ${filePath} after ${LOCK_MAX_RETRIES} retries`
895
+ );
896
+ }
897
+ async function withFileLock(filePath, fn) {
898
+ const release = await acquireLock(filePath);
899
+ try {
900
+ return await fn();
901
+ } finally {
902
+ await release();
903
+ }
904
+ }
905
+
906
+ // src/adapters/handlers/base-mcp-handler.ts
907
+ var BaseMcpHandler = class {
734
908
  packageType = "mcp";
735
- /**
736
- * Install an MCP package.
737
- *
738
- * The installation process:
739
- * 1. Validate the MCP configuration for security
740
- * 2. Read the existing ~/.claude.json configuration
741
- * 3. Add the new MCP server to the mcpServers section
742
- * 4. Write the updated configuration back
743
- *
744
- * @param manifest - The package manifest with MCP configuration
745
- * @param _context - Install context (not used for MCP, but required by interface)
746
- * @returns Array containing the path to the modified config file
747
- * @throws Error if MCP configuration fails security validation
748
- */
749
909
  async install(manifest, _context) {
750
910
  const filesWritten = [];
751
911
  if (!isMcpManifest(manifest)) {
@@ -755,70 +915,68 @@ var McpHandler = class {
755
915
  if (!mcpValidation.valid) {
756
916
  throw new Error(`MCP security validation failed: ${mcpValidation.error}`);
757
917
  }
758
- const claudeHome = getClaudeHome();
759
- const mcpConfigPath = path8.join(path8.dirname(claudeHome), ".claude.json");
760
- let existingConfig = {};
761
- if (await fs4.pathExists(mcpConfigPath)) {
762
- try {
763
- existingConfig = await fs4.readJson(mcpConfigPath);
764
- } catch {
765
- logger.warn(`Could not parse ${mcpConfigPath}, creating new config`);
766
- existingConfig = {};
767
- }
768
- }
769
- const sanitizedName = sanitizeFolderName(manifest.name);
770
- const existingMcpServers = existingConfig.mcpServers || {};
771
- const updatedConfig = {
772
- ...existingConfig,
773
- // Preserve all other config settings
774
- mcpServers: {
775
- ...existingMcpServers,
776
- // Preserve other MCP servers
777
- [sanitizedName]: {
778
- // Add/update this package's MCP server
779
- command: manifest.mcp.command,
780
- // e.g., "npx"
781
- args: manifest.mcp.args,
782
- // e.g., ["-y", "@supabase/mcp"]
783
- env: manifest.mcp.env
784
- // e.g., { "SUPABASE_URL": "..." }
918
+ const mcpConfigPath = this.getConfigPath();
919
+ await fs6.ensureDir(path8.dirname(mcpConfigPath));
920
+ await withFileLock(mcpConfigPath, async () => {
921
+ let existingConfig = {};
922
+ if (await fs6.pathExists(mcpConfigPath)) {
923
+ try {
924
+ existingConfig = await fs6.readJson(mcpConfigPath);
925
+ } catch {
926
+ const backupPath = `${mcpConfigPath}.backup.${crypto.randomBytes(8).toString("hex")}`;
927
+ try {
928
+ await fs6.copy(mcpConfigPath, backupPath);
929
+ logger.warn(
930
+ `Could not parse ${mcpConfigPath}, backup saved to ${backupPath}`
931
+ );
932
+ } catch {
933
+ logger.warn(
934
+ `Could not parse ${mcpConfigPath}, creating new config`
935
+ );
936
+ }
937
+ existingConfig = {};
785
938
  }
786
939
  }
787
- };
788
- await fs4.writeJson(mcpConfigPath, updatedConfig, { spaces: 2 });
789
- filesWritten.push(mcpConfigPath);
940
+ const sanitizedName = sanitizeFolderName(manifest.name);
941
+ const existingMcpServers = existingConfig.mcpServers || {};
942
+ const updatedConfig = {
943
+ ...existingConfig,
944
+ mcpServers: {
945
+ ...existingMcpServers,
946
+ [sanitizedName]: {
947
+ command: manifest.mcp.command,
948
+ args: manifest.mcp.args,
949
+ env: manifest.mcp.env
950
+ }
951
+ }
952
+ };
953
+ await fs6.writeJson(mcpConfigPath, updatedConfig, { spaces: 2 });
954
+ filesWritten.push(mcpConfigPath);
955
+ });
790
956
  return filesWritten;
791
957
  }
792
- /**
793
- * Uninstall an MCP package.
794
- *
795
- * This removes the MCP server entry from ~/.claude.json
796
- *
797
- * @param packageName - The name of the package to remove
798
- * @param _context - Uninstall context (not used for MCP, but required by interface)
799
- * @returns Array containing the path to the modified config file
800
- */
801
958
  async uninstall(packageName, _context) {
802
959
  const filesWritten = [];
803
960
  const folderName = sanitizeFolderName(packageName);
804
- const claudeHome = getClaudeHome();
805
- const mcpConfigPath = path8.join(path8.dirname(claudeHome), ".claude.json");
806
- if (!await fs4.pathExists(mcpConfigPath)) {
961
+ const mcpConfigPath = this.getConfigPath();
962
+ if (!await fs6.pathExists(mcpConfigPath)) {
807
963
  return filesWritten;
808
964
  }
809
965
  try {
810
- const config = await fs4.readJson(mcpConfigPath);
811
- const mcpServers = config.mcpServers;
812
- if (!mcpServers || !mcpServers[folderName]) {
813
- return filesWritten;
814
- }
815
- const { [folderName]: _removed, ...remainingServers } = mcpServers;
816
- const updatedConfig = {
817
- ...config,
818
- mcpServers: remainingServers
819
- };
820
- await fs4.writeJson(mcpConfigPath, updatedConfig, { spaces: 2 });
821
- filesWritten.push(mcpConfigPath);
966
+ await withFileLock(mcpConfigPath, async () => {
967
+ const config = await fs6.readJson(mcpConfigPath);
968
+ const mcpServers = config.mcpServers;
969
+ if (!mcpServers || !mcpServers[folderName]) {
970
+ return;
971
+ }
972
+ const { [folderName]: _removed, ...remainingServers } = mcpServers;
973
+ const updatedConfig = {
974
+ ...config,
975
+ mcpServers: remainingServers
976
+ };
977
+ await fs6.writeJson(mcpConfigPath, updatedConfig, { spaces: 2 });
978
+ filesWritten.push(mcpConfigPath);
979
+ });
822
980
  } catch (error) {
823
981
  logger.warn(
824
982
  `Could not update MCP config: ${error instanceof Error ? error.message : "Unknown error"}`
@@ -828,6 +986,14 @@ var McpHandler = class {
828
986
  }
829
987
  };
830
988
 
989
+ // src/adapters/handlers/mcp-handler.ts
990
+ var McpHandler = class extends BaseMcpHandler {
991
+ getConfigPath() {
992
+ const claudeHome = getClaudeHome();
993
+ return path9.join(path9.dirname(claudeHome), ".claude.json");
994
+ }
995
+ };
996
+
831
997
  // src/adapters/handlers/index.ts
832
998
  function initializeHandlers() {
833
999
  handlerRegistry.register(new RulesHandler());
@@ -837,8 +1003,6 @@ function initializeHandlers() {
837
1003
  initializeHandlers();
838
1004
 
839
1005
  // src/adapters/claude-code.ts
840
- import fs5 from "fs-extra";
841
- import path9 from "path";
842
1006
  var ClaudeCodeAdapter = class extends PlatformAdapter {
843
1007
  platform = "claude-code";
844
1008
  displayName = "Claude Code";
@@ -865,23 +1029,17 @@ var ClaudeCodeAdapter = class extends PlatformAdapter {
865
1029
  };
866
1030
  }
867
1031
  }
868
- async uninstall(packageName, _projectPath) {
1032
+ async uninstall(packageName, projectPath) {
869
1033
  const filesWritten = [];
870
- const folderName = sanitizeFolderName(packageName);
1034
+ const context = { projectPath };
871
1035
  try {
872
- const rulesBaseDir = getRulesPath("claude-code");
873
- const rulesPath = path9.join(rulesBaseDir, folderName);
874
- if (await fs5.pathExists(rulesPath)) {
875
- await fs5.remove(rulesPath);
876
- filesWritten.push(rulesPath);
877
- }
878
- const skillsDir = getSkillsPath();
879
- const skillPath = path9.join(skillsDir, folderName);
880
- if (await fs5.pathExists(skillPath)) {
881
- await fs5.remove(skillPath);
882
- filesWritten.push(skillPath);
1036
+ for (const type of ["rules", "skill", "mcp"]) {
1037
+ if (handlerRegistry.hasHandler(type)) {
1038
+ const handler = handlerRegistry.getHandler(type);
1039
+ const files = await handler.uninstall(packageName, context);
1040
+ filesWritten.push(...files);
1041
+ }
883
1042
  }
884
- await this.removeMcpServer(folderName, filesWritten);
885
1043
  return {
886
1044
  success: true,
887
1045
  platform: "claude-code",
@@ -904,63 +1062,248 @@ var ClaudeCodeAdapter = class extends PlatformAdapter {
904
1062
  return this.installFallback(manifest, context);
905
1063
  }
906
1064
  async installFallback(manifest, context) {
907
- if ("skill" in manifest && manifest.skill) {
1065
+ logger.warn(
1066
+ `No handler registered for type "${manifest.type}", attempting content-based detection`
1067
+ );
1068
+ if (isSkillManifest(manifest)) {
908
1069
  const handler = handlerRegistry.getHandler("skill");
909
1070
  return handler.install(manifest, context);
910
1071
  }
911
- if ("mcp" in manifest && manifest.mcp) {
1072
+ if (isMcpManifest(manifest)) {
912
1073
  const handler = handlerRegistry.getHandler("mcp");
913
1074
  return handler.install(manifest, context);
914
1075
  }
915
- if ("universal" in manifest && manifest.universal?.rules) {
1076
+ if (isRulesManifest(manifest)) {
916
1077
  const handler = handlerRegistry.getHandler("rules");
917
1078
  return handler.install(manifest, context);
918
1079
  }
919
1080
  return [];
920
1081
  }
921
- async removeMcpServer(serverName, filesWritten) {
922
- const claudeHome = getClaudeHome();
923
- const mcpConfigPath = path9.join(path9.dirname(claudeHome), ".claude.json");
924
- if (!await fs5.pathExists(mcpConfigPath)) {
925
- return;
1082
+ };
1083
+
1084
+ // src/adapters/handlers/cursor-rules-handler.ts
1085
+ import fs7 from "fs-extra";
1086
+ import path10 from "path";
1087
+ function escapeYamlString(value) {
1088
+ if (/[\n\r\t\0:#{}[\]&*?|>!%@`"',]/.test(value) || value.trim() !== value) {
1089
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\0/g, "").replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\t/g, "\\t");
1090
+ return `"${escaped}"`;
1091
+ }
1092
+ return value;
1093
+ }
1094
+ function toMdcContent(description, globs, rulesContent) {
1095
+ const alwaysApply = globs.length === 0;
1096
+ const frontmatter = [
1097
+ "---",
1098
+ `description: ${escapeYamlString(description)}`,
1099
+ `globs: ${JSON.stringify(globs)}`,
1100
+ `alwaysApply: ${alwaysApply}`,
1101
+ "---"
1102
+ ].join("\n");
1103
+ return `${frontmatter}
1104
+ ${rulesContent.trim()}
1105
+ `;
1106
+ }
1107
+ var CursorRulesHandler = class {
1108
+ packageType = "rules";
1109
+ async install(manifest, context) {
1110
+ const filesWritten = [];
1111
+ const rulesBaseDir = getRulesPath("cursor", context.projectPath);
1112
+ const folderName = sanitizeFolderName(manifest.name);
1113
+ const rulesDir = path10.join(rulesBaseDir, folderName);
1114
+ await fs7.ensureDir(rulesDir);
1115
+ const description = manifest.description || manifest.name;
1116
+ const globs = manifest.universal?.globs || [];
1117
+ if (globs.length > 0) {
1118
+ const globValidation = validateGlobs(globs);
1119
+ if (!globValidation.valid) {
1120
+ throw new Error(
1121
+ `Glob security validation failed: ${globValidation.error}`
1122
+ );
1123
+ }
1124
+ }
1125
+ if (context.packagePath && await fs7.pathExists(context.packagePath)) {
1126
+ const files = await fs7.readdir(context.packagePath);
1127
+ const mdFiles = files.filter(
1128
+ (f) => f.endsWith(".md") && f.toLowerCase() !== "cpm.yaml"
1129
+ );
1130
+ if (mdFiles.length > 0) {
1131
+ for (const file of mdFiles) {
1132
+ const validation = sanitizeFileName(file);
1133
+ if (!validation.valid) {
1134
+ logger.warn(`Skipping unsafe file: ${file} (${validation.error})`);
1135
+ continue;
1136
+ }
1137
+ const srcPath = path10.join(context.packagePath, file);
1138
+ const mdcFileName = validation.sanitized.replace(/\.md$/, ".mdc");
1139
+ const destPath = path10.join(rulesDir, mdcFileName);
1140
+ if (!isPathWithinDirectory(destPath, rulesDir)) {
1141
+ logger.warn(`Blocked path traversal attempt: ${file}`);
1142
+ continue;
1143
+ }
1144
+ const srcStat = await fs7.lstat(srcPath);
1145
+ if (srcStat.isSymbolicLink()) {
1146
+ logger.warn(`Blocked symlink in package: ${file}`);
1147
+ continue;
1148
+ }
1149
+ const content = await fs7.readFile(srcPath, "utf-8");
1150
+ const mdcContent2 = toMdcContent(description, globs, content);
1151
+ await fs7.writeFile(destPath, mdcContent2, "utf-8");
1152
+ filesWritten.push(destPath);
1153
+ }
1154
+ const metadataPath2 = await writePackageMetadata(rulesDir, manifest);
1155
+ filesWritten.push(metadataPath2);
1156
+ return filesWritten;
1157
+ }
1158
+ }
1159
+ const rulesContent = this.getRulesContent(manifest);
1160
+ if (!rulesContent) return filesWritten;
1161
+ const rulesPath = path10.join(rulesDir, "RULES.mdc");
1162
+ const mdcContent = toMdcContent(description, globs, rulesContent);
1163
+ await fs7.writeFile(rulesPath, mdcContent, "utf-8");
1164
+ filesWritten.push(rulesPath);
1165
+ const metadataPath = await writePackageMetadata(rulesDir, manifest);
1166
+ filesWritten.push(metadataPath);
1167
+ return filesWritten;
1168
+ }
1169
+ async uninstall(packageName, context) {
1170
+ const filesRemoved = [];
1171
+ const folderName = sanitizeFolderName(packageName);
1172
+ const rulesBaseDir = getRulesPath("cursor", context.projectPath);
1173
+ const rulesPath = path10.join(rulesBaseDir, folderName);
1174
+ if (await fs7.pathExists(rulesPath)) {
1175
+ await fs7.remove(rulesPath);
1176
+ filesRemoved.push(rulesPath);
1177
+ }
1178
+ return filesRemoved;
1179
+ }
1180
+ getRulesContent(manifest) {
1181
+ if (isRulesManifest(manifest)) {
1182
+ return manifest.universal.rules || manifest.universal.prompt;
926
1183
  }
1184
+ return void 0;
1185
+ }
1186
+ };
1187
+
1188
+ // src/adapters/handlers/cursor-mcp-handler.ts
1189
+ var CursorMcpHandler = class extends BaseMcpHandler {
1190
+ getConfigPath() {
1191
+ return getCursorMcpConfigPath();
1192
+ }
1193
+ };
1194
+
1195
+ // src/adapters/cursor.ts
1196
+ var cursorRulesHandler = new CursorRulesHandler();
1197
+ var cursorMcpHandler = new CursorMcpHandler();
1198
+ var CursorAdapter = class extends PlatformAdapter {
1199
+ platform = "cursor";
1200
+ displayName = "Cursor";
1201
+ async isAvailable(_projectPath) {
1202
+ return true;
1203
+ }
1204
+ async install(manifest, projectPath, packagePath) {
1205
+ const filesWritten = [];
927
1206
  try {
928
- const config = await fs5.readJson(mcpConfigPath);
929
- const mcpServers = config.mcpServers;
930
- if (!mcpServers || !mcpServers[serverName]) {
931
- return;
1207
+ const context = { projectPath, packagePath };
1208
+ if (manifest.type === "skill") {
1209
+ logger.warn(
1210
+ `Package "${manifest.name}" is a skill package. Skills are not supported on Cursor \u2014 skipping.`
1211
+ );
1212
+ return {
1213
+ success: true,
1214
+ platform: "cursor",
1215
+ filesWritten
1216
+ };
932
1217
  }
933
- const { [serverName]: _removed, ...remainingServers } = mcpServers;
934
- const updatedConfig = {
935
- ...config,
936
- mcpServers: remainingServers
1218
+ const result = await this.installByType(manifest, context);
1219
+ filesWritten.push(...result);
1220
+ return {
1221
+ success: true,
1222
+ platform: "cursor",
1223
+ filesWritten
937
1224
  };
938
- await fs5.writeJson(mcpConfigPath, updatedConfig, { spaces: 2 });
939
- filesWritten.push(mcpConfigPath);
940
1225
  } catch (error) {
941
- logger.warn(
942
- `Could not update MCP config: ${error instanceof Error ? error.message : "Unknown error"}`
1226
+ return {
1227
+ success: false,
1228
+ platform: "cursor",
1229
+ filesWritten,
1230
+ error: error instanceof Error ? error.message : "Unknown error"
1231
+ };
1232
+ }
1233
+ }
1234
+ async uninstall(packageName, projectPath) {
1235
+ const filesWritten = [];
1236
+ const context = { projectPath };
1237
+ try {
1238
+ const rulesFiles = await cursorRulesHandler.uninstall(
1239
+ packageName,
1240
+ context
943
1241
  );
1242
+ filesWritten.push(...rulesFiles);
1243
+ const mcpFiles = await cursorMcpHandler.uninstall(packageName, context);
1244
+ filesWritten.push(...mcpFiles);
1245
+ return {
1246
+ success: true,
1247
+ platform: "cursor",
1248
+ filesWritten
1249
+ };
1250
+ } catch (error) {
1251
+ return {
1252
+ success: false,
1253
+ platform: "cursor",
1254
+ filesWritten,
1255
+ error: error instanceof Error ? error.message : "Unknown error"
1256
+ };
1257
+ }
1258
+ }
1259
+ async installByType(manifest, context) {
1260
+ switch (manifest.type) {
1261
+ case "rules":
1262
+ return cursorRulesHandler.install(manifest, context);
1263
+ case "mcp":
1264
+ return cursorMcpHandler.install(manifest, context);
1265
+ default:
1266
+ logger.warn(
1267
+ `Package type "${manifest.type}" is not yet supported on Cursor`
1268
+ );
1269
+ return [];
944
1270
  }
945
1271
  }
946
1272
  };
947
1273
 
948
1274
  // src/adapters/index.ts
949
1275
  var adapters = {
950
- "claude-code": new ClaudeCodeAdapter()
1276
+ "claude-code": new ClaudeCodeAdapter(),
1277
+ cursor: new CursorAdapter()
951
1278
  };
952
1279
  function getAdapter(platform) {
953
- return adapters[platform];
1280
+ const adapter = adapters[platform];
1281
+ if (!adapter) {
1282
+ throw new Error(`No adapter available for platform: ${platform}`);
1283
+ }
1284
+ return adapter;
954
1285
  }
955
1286
 
956
1287
  // src/utils/registry.ts
957
1288
  import got from "got";
958
- import fs6 from "fs-extra";
959
- import path10 from "path";
960
- import os2 from "os";
961
- var DEFAULT_REGISTRY_URL = process.env.CPM_REGISTRY_URL || "https://raw.githubusercontent.com/cpmai-dev/packages/main/registry.json";
962
- var CACHE_DIR = path10.join(os2.homedir(), ".cpm", "cache");
963
- var CACHE_FILE = path10.join(CACHE_DIR, "registry.json");
1289
+ import fs8 from "fs-extra";
1290
+ import path11 from "path";
1291
+ import os3 from "os";
1292
+ function getRegistryUrl() {
1293
+ const envUrl = process.env.CPM_REGISTRY_URL;
1294
+ if (envUrl) {
1295
+ if (!envUrl.startsWith("https://")) {
1296
+ throw new Error(
1297
+ "CPM_REGISTRY_URL must use HTTPS. HTTP registries are not allowed for security reasons."
1298
+ );
1299
+ }
1300
+ return envUrl;
1301
+ }
1302
+ return "https://raw.githubusercontent.com/cpmai-dev/packages/main/registry.json";
1303
+ }
1304
+ var DEFAULT_REGISTRY_URL = getRegistryUrl();
1305
+ var CACHE_DIR = path11.join(os3.homedir(), ".cpm", "cache");
1306
+ var CACHE_FILE = path11.join(CACHE_DIR, "registry.json");
964
1307
  var comparators = {
965
1308
  downloads: (a, b) => (b.downloads ?? 0) - (a.downloads ?? 0),
966
1309
  stars: (a, b) => (b.stars ?? 0) - (a.stars ?? 0),
@@ -1022,11 +1365,11 @@ var Registry = class {
1022
1365
  }
1023
1366
  async loadFileCache() {
1024
1367
  try {
1025
- await fs6.ensureDir(CACHE_DIR);
1026
- if (await fs6.pathExists(CACHE_FILE)) {
1027
- const stat = await fs6.stat(CACHE_FILE);
1368
+ await fs8.ensureDir(CACHE_DIR);
1369
+ if (await fs8.pathExists(CACHE_FILE)) {
1370
+ const stat = await fs8.stat(CACHE_FILE);
1028
1371
  if (Date.now() - stat.mtimeMs < LIMITS.CACHE_TTL_MS) {
1029
- return await fs6.readJson(CACHE_FILE);
1372
+ return await fs8.readJson(CACHE_FILE);
1030
1373
  }
1031
1374
  }
1032
1375
  } catch {
@@ -1035,8 +1378,8 @@ var Registry = class {
1035
1378
  }
1036
1379
  async saveFileCache(data) {
1037
1380
  try {
1038
- await fs6.ensureDir(CACHE_DIR);
1039
- await fs6.writeJson(CACHE_FILE, data, { spaces: 2 });
1381
+ await fs8.ensureDir(CACHE_DIR);
1382
+ await fs8.writeJson(CACHE_FILE, data, { spaces: 2 });
1040
1383
  } catch {
1041
1384
  }
1042
1385
  }
@@ -1059,8 +1402,8 @@ var Registry = class {
1059
1402
  return this.cache;
1060
1403
  }
1061
1404
  try {
1062
- if (await fs6.pathExists(CACHE_FILE)) {
1063
- const cached = await fs6.readJson(CACHE_FILE);
1405
+ if (await fs8.pathExists(CACHE_FILE)) {
1406
+ const cached = await fs8.readJson(CACHE_FILE);
1064
1407
  this.cache = cached;
1065
1408
  return cached;
1066
1409
  }
@@ -1100,9 +1443,9 @@ var Registry = class {
1100
1443
  var registry = new Registry();
1101
1444
 
1102
1445
  // src/utils/downloader.ts
1103
- import fs8 from "fs-extra";
1104
- import path12 from "path";
1105
- import os3 from "os";
1446
+ import fs10 from "fs-extra";
1447
+ import path13 from "path";
1448
+ import os4 from "os";
1106
1449
 
1107
1450
  // src/sources/manifest-resolver.ts
1108
1451
  var ManifestResolver = class {
@@ -1195,76 +1538,45 @@ var ManifestResolver = class {
1195
1538
  // src/sources/repository-source.ts
1196
1539
  import got2 from "got";
1197
1540
  import yaml from "yaml";
1541
+ var PACKAGES_REPO_BASE = "https://raw.githubusercontent.com/cpmai-dev/packages/main/packages";
1198
1542
  var RepositorySource = class {
1199
- /**
1200
- * Name of this source for logging and debugging.
1201
- */
1202
1543
  name = "repository";
1203
- /**
1204
- * Priority 1 - this is the first source tried.
1205
- * Repository fetching is fast and provides complete manifests.
1206
- */
1207
1544
  priority = 1;
1208
- /**
1209
- * Check if this source can fetch the given package.
1210
- *
1211
- * We can only fetch from GitHub repositories, so we check
1212
- * if the package has a repository URL that includes "github.com".
1213
- *
1214
- * @param pkg - The registry package to check
1215
- * @returns true if the package has a GitHub repository URL
1216
- *
1217
- * @example
1218
- * ```typescript
1219
- * // Package with GitHub repo
1220
- * canFetch({ repository: "https://github.com/cpm/rules" }) // true
1221
- *
1222
- * // Package without repo
1223
- * canFetch({ repository: undefined }) // false
1224
- *
1225
- * // Package with non-GitHub repo
1226
- * canFetch({ repository: "https://gitlab.com/..." }) // false
1227
- * ```
1228
- */
1229
1545
  canFetch(pkg) {
1230
- return !!pkg.repository?.includes("github.com");
1546
+ return !!pkg.path || !!pkg.repository?.includes("github.com");
1231
1547
  }
1232
- /**
1233
- * Fetch the manifest from the package's GitHub repository.
1234
- *
1235
- * This method:
1236
- * 1. Extracts the owner and repo name from the URL
1237
- * 2. Constructs a raw.githubusercontent.com URL for cpm.yaml
1238
- * 3. Fetches the file with a timeout
1239
- * 4. Parses the YAML content
1240
- *
1241
- * @param pkg - The registry package to fetch
1242
- * @param _context - Fetch context (not used by this source)
1243
- * @returns The parsed manifest, or null if fetch fails
1244
- *
1245
- * @example
1246
- * ```typescript
1247
- * const manifest = await source.fetch({
1248
- * name: "@cpm/typescript-rules",
1249
- * repository: "https://github.com/cpm/typescript-rules",
1250
- * // ... other fields
1251
- * }, context);
1252
- *
1253
- * if (manifest) {
1254
- * console.log("Fetched:", manifest.name, manifest.version);
1255
- * }
1256
- * ```
1257
- */
1258
1548
  async fetch(pkg, _context) {
1259
- if (!pkg.repository) return null;
1549
+ if (pkg.path) {
1550
+ const manifest = await this.fetchFromPackagesRepo(pkg.path);
1551
+ if (manifest) return manifest;
1552
+ }
1553
+ if (pkg.repository) {
1554
+ return this.fetchFromStandaloneRepo(pkg.repository);
1555
+ }
1556
+ return null;
1557
+ }
1558
+ async fetchFromPackagesRepo(packagePath) {
1559
+ if (packagePath.includes("..") || packagePath.startsWith("/") || packagePath.includes("\\")) {
1560
+ return null;
1561
+ }
1562
+ const rawUrl = `${PACKAGES_REPO_BASE}/${packagePath}/cpm.yaml`;
1563
+ try {
1564
+ const response = await got2(rawUrl, {
1565
+ timeout: { request: TIMEOUTS.MANIFEST_FETCH }
1566
+ });
1567
+ return yaml.parse(response.body);
1568
+ } catch {
1569
+ return null;
1570
+ }
1571
+ }
1572
+ async fetchFromStandaloneRepo(repository) {
1260
1573
  try {
1261
- const match = pkg.repository.match(/github\.com\/([^/]+)\/([^/]+)/);
1574
+ const match = repository.match(/github\.com\/([^/]+)\/([^/]+)/);
1262
1575
  if (!match) return null;
1263
1576
  const [, owner, repo] = match;
1264
1577
  const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/main/cpm.yaml`;
1265
1578
  const response = await got2(rawUrl, {
1266
1579
  timeout: { request: TIMEOUTS.MANIFEST_FETCH }
1267
- // 5 second timeout
1268
1580
  });
1269
1581
  return yaml.parse(response.body);
1270
1582
  } catch {
@@ -1275,8 +1587,8 @@ var RepositorySource = class {
1275
1587
 
1276
1588
  // src/sources/tarball-source.ts
1277
1589
  import got3 from "got";
1278
- import fs7 from "fs-extra";
1279
- import path11 from "path";
1590
+ import fs9 from "fs-extra";
1591
+ import path12 from "path";
1280
1592
  import * as tar from "tar";
1281
1593
  import yaml2 from "yaml";
1282
1594
  var TarballSource = class {
@@ -1328,12 +1640,12 @@ var TarballSource = class {
1328
1640
  responseType: "buffer"
1329
1641
  // Get raw binary data
1330
1642
  });
1331
- const tarballPath = path11.join(context.tempDir, "package.tar.gz");
1332
- await fs7.writeFile(tarballPath, response.body);
1643
+ const tarballPath = path12.join(context.tempDir, "package.tar.gz");
1644
+ await fs9.writeFile(tarballPath, response.body);
1333
1645
  await this.extractTarball(tarballPath, context.tempDir);
1334
- const manifestPath = path11.join(context.tempDir, "cpm.yaml");
1335
- if (await fs7.pathExists(manifestPath)) {
1336
- const content = await fs7.readFile(manifestPath, "utf-8");
1646
+ const manifestPath = path12.join(context.tempDir, "cpm.yaml");
1647
+ if (await fs9.pathExists(manifestPath)) {
1648
+ const content = await fs9.readFile(manifestPath, "utf-8");
1337
1649
  return yaml2.parse(content);
1338
1650
  }
1339
1651
  return null;
@@ -1355,8 +1667,8 @@ var TarballSource = class {
1355
1667
  * @param destDir - Directory to extract to
1356
1668
  */
1357
1669
  async extractTarball(tarballPath, destDir) {
1358
- await fs7.ensureDir(destDir);
1359
- const resolvedDestDir = path11.resolve(destDir);
1670
+ await fs9.ensureDir(destDir);
1671
+ const resolvedDestDir = path12.resolve(destDir);
1360
1672
  await tar.extract({
1361
1673
  file: tarballPath,
1362
1674
  // The tarball file to extract
@@ -1366,8 +1678,8 @@ var TarballSource = class {
1366
1678
  // Remove the top-level directory (e.g., "package-1.0.0/")
1367
1679
  // Security filter: check each entry before extracting
1368
1680
  filter: (entryPath) => {
1369
- const resolvedPath = path11.resolve(destDir, entryPath);
1370
- const isWithinDest = resolvedPath.startsWith(resolvedDestDir + path11.sep) || resolvedPath === resolvedDestDir;
1681
+ const resolvedPath = path12.resolve(destDir, entryPath);
1682
+ const isWithinDest = resolvedPath.startsWith(resolvedDestDir + path12.sep) || resolvedPath === resolvedDestDir;
1371
1683
  if (!isWithinDest) {
1372
1684
  logger.warn(`Blocked path traversal in tarball: ${entryPath}`);
1373
1685
  return false;
@@ -2048,15 +2360,15 @@ function createDefaultResolver() {
2048
2360
  var defaultResolver = createDefaultResolver();
2049
2361
 
2050
2362
  // src/utils/downloader.ts
2051
- var TEMP_DIR = path12.join(os3.tmpdir(), "cpm-downloads");
2363
+ var TEMP_DIR = path13.join(os4.tmpdir(), "cpm-downloads");
2052
2364
  async function downloadPackage(pkg) {
2053
2365
  try {
2054
- await fs8.ensureDir(TEMP_DIR);
2055
- const packageTempDir = path12.join(
2366
+ await fs10.ensureDir(TEMP_DIR);
2367
+ const packageTempDir = path13.join(
2056
2368
  TEMP_DIR,
2057
2369
  `${pkg.name.replace(/[@/]/g, "_")}-${Date.now()}`
2058
2370
  );
2059
- await fs8.ensureDir(packageTempDir);
2371
+ await fs10.ensureDir(packageTempDir);
2060
2372
  const manifest = await defaultResolver.resolve(pkg, {
2061
2373
  tempDir: packageTempDir
2062
2374
  });
@@ -2071,7 +2383,7 @@ async function downloadPackage(pkg) {
2071
2383
  async function cleanupTempDir(tempDir) {
2072
2384
  try {
2073
2385
  if (tempDir.startsWith(TEMP_DIR)) {
2074
- await fs8.remove(tempDir);
2386
+ await fs10.remove(tempDir);
2075
2387
  }
2076
2388
  } catch {
2077
2389
  }
@@ -2198,7 +2510,7 @@ var SEMANTIC_COLORS = {
2198
2510
 
2199
2511
  // src/commands/ui/formatters.ts
2200
2512
  import chalk2 from "chalk";
2201
- import path13 from "path";
2513
+ import path14 from "path";
2202
2514
  function formatNumber(num) {
2203
2515
  if (num >= 1e6) {
2204
2516
  return `${(num / 1e6).toFixed(1)}M`;
@@ -2209,7 +2521,7 @@ function formatNumber(num) {
2209
2521
  return num.toString();
2210
2522
  }
2211
2523
  function formatPath(filePath) {
2212
- const relativePath = path13.relative(process.cwd(), filePath);
2524
+ const relativePath = path14.relative(process.cwd(), filePath);
2213
2525
  if (relativePath.startsWith("..")) {
2214
2526
  return filePath;
2215
2527
  }
@@ -2271,8 +2583,12 @@ function formatUsageHints(manifest) {
2271
2583
  if ("mcp" in manifest && manifest.mcp?.env) {
2272
2584
  const envVars = Object.keys(manifest.mcp.env);
2273
2585
  if (envVars.length > 0) {
2274
- hints.push(chalk2.yellow(`
2275
- Required environment variables:`));
2586
+ hints.push(
2587
+ chalk2.yellow(
2588
+ `
2589
+ Configure these environment variables in ~/.claude.json:`
2590
+ )
2591
+ );
2276
2592
  for (const envVar of envVars) {
2277
2593
  hints.push(chalk2.dim(` - ${envVar}`));
2278
2594
  }
@@ -2463,7 +2779,11 @@ async function installCommand(packageName, rawOptions) {
2463
2779
  tempDir = downloadResult.tempDir;
2464
2780
  const targetPlatforms = [options.platform];
2465
2781
  spinner.update(`Installing to ${targetPlatforms.join(", ")}...`);
2466
- await ensureClaudeDirs();
2782
+ if (options.platform === "cursor") {
2783
+ await ensureCursorDirs(process.cwd());
2784
+ } else {
2785
+ await ensureClaudeDirs();
2786
+ }
2467
2787
  const results = await installToPlatforms(
2468
2788
  manifest,
2469
2789
  tempDir,
@@ -2539,28 +2859,28 @@ async function searchCommand(query, rawOptions) {
2539
2859
 
2540
2860
  // src/commands/list.ts
2541
2861
  import chalk4 from "chalk";
2542
- import fs9 from "fs-extra";
2543
- import path14 from "path";
2544
- import os4 from "os";
2862
+ import fs11 from "fs-extra";
2863
+ import path15 from "path";
2864
+ import os5 from "os";
2545
2865
  async function readPackageMetadata(packageDir) {
2546
- const metadataPath = path14.join(packageDir, ".cpm.json");
2866
+ const metadataPath = path15.join(packageDir, ".cpm.json");
2547
2867
  try {
2548
- if (await fs9.pathExists(metadataPath)) {
2549
- return await fs9.readJson(metadataPath);
2868
+ if (await fs11.pathExists(metadataPath)) {
2869
+ return await fs11.readJson(metadataPath);
2550
2870
  }
2551
2871
  } catch {
2552
2872
  }
2553
2873
  return null;
2554
2874
  }
2555
- async function scanDirectory(dir, type) {
2875
+ async function scanDirectory(dir, type, platform) {
2556
2876
  const items = [];
2557
- if (!await fs9.pathExists(dir)) {
2877
+ if (!await fs11.pathExists(dir)) {
2558
2878
  return items;
2559
2879
  }
2560
- const entries = await fs9.readdir(dir);
2880
+ const entries = await fs11.readdir(dir);
2561
2881
  for (const entry of entries) {
2562
- const entryPath = path14.join(dir, entry);
2563
- const stat = await fs9.stat(entryPath);
2882
+ const entryPath = path15.join(dir, entry);
2883
+ const stat = await fs11.stat(entryPath);
2564
2884
  if (stat.isDirectory()) {
2565
2885
  const metadata = await readPackageMetadata(entryPath);
2566
2886
  items.push({
@@ -2568,27 +2888,28 @@ async function scanDirectory(dir, type) {
2568
2888
  folderName: entry,
2569
2889
  type,
2570
2890
  version: metadata?.version,
2571
- path: entryPath
2891
+ path: entryPath,
2892
+ platform
2572
2893
  });
2573
2894
  }
2574
2895
  }
2575
2896
  return items;
2576
2897
  }
2577
- async function scanMcpServers() {
2898
+ async function scanMcpServersFromConfig(configPath, platform) {
2578
2899
  const items = [];
2579
- const configPath = path14.join(os4.homedir(), ".claude.json");
2580
- if (!await fs9.pathExists(configPath)) {
2900
+ if (!await fs11.pathExists(configPath)) {
2581
2901
  return items;
2582
2902
  }
2583
2903
  try {
2584
- const config = await fs9.readJson(configPath);
2904
+ const config = await fs11.readJson(configPath);
2585
2905
  const mcpServers = config.mcpServers || {};
2586
2906
  for (const name of Object.keys(mcpServers)) {
2587
2907
  items.push({
2588
2908
  name,
2589
2909
  folderName: name,
2590
2910
  type: "mcp",
2591
- path: configPath
2911
+ path: configPath,
2912
+ platform
2592
2913
  });
2593
2914
  }
2594
2915
  } catch {
@@ -2596,13 +2917,24 @@ async function scanMcpServers() {
2596
2917
  return items;
2597
2918
  }
2598
2919
  async function scanInstalledPackages() {
2599
- const claudeHome = path14.join(os4.homedir(), ".claude");
2600
- const [rules, skills, mcp] = await Promise.all([
2601
- scanDirectory(path14.join(claudeHome, "rules"), "rules"),
2602
- scanDirectory(path14.join(claudeHome, "skills"), "skill"),
2603
- scanMcpServers()
2920
+ const claudeHome = path15.join(os5.homedir(), ".claude");
2921
+ const cursorRulesDir = path15.join(process.cwd(), ".cursor", "rules");
2922
+ const cursorMcpConfig = getCursorMcpConfigPath();
2923
+ const claudeMcpConfig = path15.join(os5.homedir(), ".claude.json");
2924
+ const [claudeRules, claudeSkills, claudeMcp, cursorRules, cursorMcp] = await Promise.all([
2925
+ scanDirectory(path15.join(claudeHome, "rules"), "rules", "claude-code"),
2926
+ scanDirectory(path15.join(claudeHome, "skills"), "skill", "claude-code"),
2927
+ scanMcpServersFromConfig(claudeMcpConfig, "claude-code"),
2928
+ scanDirectory(cursorRulesDir, "rules", "cursor"),
2929
+ scanMcpServersFromConfig(cursorMcpConfig, "cursor")
2604
2930
  ]);
2605
- return [...rules, ...skills, ...mcp];
2931
+ return [
2932
+ ...claudeRules,
2933
+ ...claudeSkills,
2934
+ ...claudeMcp,
2935
+ ...cursorRules,
2936
+ ...cursorMcp
2937
+ ];
2606
2938
  }
2607
2939
  function groupByType(packages) {
2608
2940
  return packages.reduce(
@@ -2615,8 +2947,9 @@ function groupByType(packages) {
2615
2947
  }
2616
2948
  function displayPackage(pkg) {
2617
2949
  const version = pkg.version ? SEMANTIC_COLORS.dim(` v${pkg.version}`) : "";
2950
+ const platform = pkg.platform ? SEMANTIC_COLORS.dim(` [${pkg.platform}]`) : "";
2618
2951
  logger.log(
2619
- ` ${SEMANTIC_COLORS.success("\u25C9")} ${chalk4.bold(pkg.name)}${version}`
2952
+ ` ${SEMANTIC_COLORS.success("\u25C9")} ${chalk4.bold(pkg.name)}${version}${platform}`
2620
2953
  );
2621
2954
  }
2622
2955
  function displayByType(byType) {
@@ -2679,15 +3012,22 @@ async function uninstallCommand(packageName) {
2679
3012
  const spinner = createSpinner(spinnerText("Uninstalling", packageName));
2680
3013
  try {
2681
3014
  const folderName = extractFolderName(packageName);
2682
- const adapter = getAdapter("claude-code");
2683
- const result = await adapter.uninstall(folderName, process.cwd());
2684
- if (result.success && result.filesWritten.length > 0) {
3015
+ const allFilesRemoved = [];
3016
+ for (const platform of VALID_PLATFORMS) {
3017
+ try {
3018
+ const adapter = getAdapter(platform);
3019
+ const result = await adapter.uninstall(folderName, process.cwd());
3020
+ if (result.success) {
3021
+ allFilesRemoved.push(...result.filesWritten);
3022
+ }
3023
+ } catch {
3024
+ }
3025
+ }
3026
+ if (allFilesRemoved.length > 0) {
2685
3027
  spinner.succeed(successText("Uninstalled", packageName));
2686
- displayRemovedFiles(result.filesWritten);
2687
- } else if (result.success) {
2688
- spinner.warn(`Package ${packageName} was not found`);
3028
+ displayRemovedFiles(allFilesRemoved);
2689
3029
  } else {
2690
- spinner.fail(failText("Failed to uninstall", packageName, result.error));
3030
+ spinner.warn(`Package ${packageName} was not found`);
2691
3031
  }
2692
3032
  } catch (error) {
2693
3033
  spinner.fail(failText("Failed to uninstall", packageName));