@cpmai/cli 0.3.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 +557 -244
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -47,10 +47,7 @@ var SEARCH_SORT_OPTIONS = [
47
47
  "name"
48
48
  // Alphabetical order
49
49
  ];
50
- var VALID_PLATFORMS = [
51
- "claude-code"
52
- // Currently the only supported platform
53
- ];
50
+ var VALID_PLATFORMS = ["claude-code", "cursor"];
54
51
  var ALLOWED_MCP_COMMANDS = [
55
52
  "npx",
56
53
  // Node package executor
@@ -76,6 +73,8 @@ var BLOCKED_MCP_ARG_PATTERNS = [
76
73
  // Concatenated eval flag (e.g., -eCODE)
77
74
  /-c(?:\s|$)/,
78
75
  // Command flag (with space or at end)
76
+ /^-c\S/,
77
+ // Concatenated command flag (e.g., -cCODE)
79
78
  /\bcurl\b/i,
80
79
  // curl command (data exfiltration)
81
80
  /\bwget\b/i,
@@ -147,15 +146,15 @@ function isSkillManifest(manifest) {
147
146
  function isMcpManifest(manifest) {
148
147
  return manifest.type === "mcp";
149
148
  }
150
- function getTypeFromPath(path14) {
151
- if (path14.startsWith("skills/")) return "skill";
152
- if (path14.startsWith("rules/")) return "rules";
153
- if (path14.startsWith("mcp/")) return "mcp";
154
- if (path14.startsWith("agents/")) return "agent";
155
- if (path14.startsWith("hooks/")) return "hook";
156
- if (path14.startsWith("workflows/")) return "workflow";
157
- if (path14.startsWith("templates/")) return "template";
158
- if (path14.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";
159
158
  return null;
160
159
  }
161
160
  function resolvePackageType(pkg) {
@@ -265,11 +264,12 @@ var HandlerRegistry = class {
265
264
  var handlerRegistry = new HandlerRegistry();
266
265
 
267
266
  // src/adapters/handlers/rules-handler.ts
268
- import fs2 from "fs-extra";
269
- import path5 from "path";
267
+ import fs3 from "fs-extra";
268
+ import path6 from "path";
270
269
 
271
270
  // src/utils/platform.ts
272
271
  import path2 from "path";
272
+ import os2 from "os";
273
273
 
274
274
  // src/utils/config.ts
275
275
  import path from "path";
@@ -283,13 +283,28 @@ async function ensureClaudeDirs() {
283
283
  await fs.ensureDir(path.join(claudeHome, "rules"));
284
284
  await fs.ensureDir(path.join(claudeHome, "skills"));
285
285
  }
286
+ async function ensureCursorDirs(projectPath) {
287
+ await fs.ensureDir(path.join(projectPath, ".cursor", "rules"));
288
+ }
286
289
 
287
290
  // src/utils/platform.ts
288
- function getRulesPath(platform) {
289
- if (platform !== "claude-code") {
290
- 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");
291
300
  }
292
- 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}`);
293
308
  }
294
309
  function getSkillsPath() {
295
310
  return path2.join(getClaudeHome(), "skills");
@@ -504,17 +519,70 @@ function isPathWithinDirectory(filePath, directory) {
504
519
  return resolvedPath.startsWith(resolvedDir + path4.sep) || resolvedPath === resolvedDir;
505
520
  }
506
521
 
507
- // src/adapters/handlers/rules-handler.ts
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 };
575
+ }
576
+
577
+ // src/adapters/handlers/metadata.ts
578
+ import fs2 from "fs-extra";
579
+ import path5 from "path";
508
580
  async function writePackageMetadata(packageDir, manifest) {
509
581
  const metadata = {
510
582
  name: manifest.name,
511
- // e.g., "@cpm/typescript-strict"
512
583
  version: manifest.version,
513
- // e.g., "1.0.0"
514
584
  type: manifest.type,
515
- // e.g., "rules"
516
585
  installedAt: (/* @__PURE__ */ new Date()).toISOString()
517
- // ISO timestamp for when it was installed
518
586
  };
519
587
  const metadataPath = path5.join(packageDir, ".cpm.json");
520
588
  try {
@@ -526,6 +594,8 @@ async function writePackageMetadata(packageDir, manifest) {
526
594
  }
527
595
  return metadataPath;
528
596
  }
597
+
598
+ // src/adapters/handlers/rules-handler.ts
529
599
  var RulesHandler = class {
530
600
  /**
531
601
  * Identifies this handler as handling "rules" type packages.
@@ -549,10 +619,10 @@ var RulesHandler = class {
549
619
  const filesWritten = [];
550
620
  const rulesBaseDir = getRulesPath("claude-code");
551
621
  const folderName = sanitizeFolderName(manifest.name);
552
- const rulesDir = path5.join(rulesBaseDir, folderName);
553
- await fs2.ensureDir(rulesDir);
554
- if (context.packagePath && await fs2.pathExists(context.packagePath)) {
555
- const files = await fs2.readdir(context.packagePath);
622
+ const rulesDir = path6.join(rulesBaseDir, folderName);
623
+ await fs3.ensureDir(rulesDir);
624
+ if (context.packagePath && await fs3.pathExists(context.packagePath)) {
625
+ const files = await fs3.readdir(context.packagePath);
556
626
  const mdFiles = files.filter(
557
627
  (f) => f.endsWith(".md") && f.toLowerCase() !== "cpm.yaml"
558
628
  );
@@ -563,13 +633,18 @@ var RulesHandler = class {
563
633
  logger.warn(`Skipping unsafe file: ${file} (${validation.error})`);
564
634
  continue;
565
635
  }
566
- const srcPath = path5.join(context.packagePath, file);
567
- const destPath = path5.join(rulesDir, validation.sanitized);
636
+ const srcPath = path6.join(context.packagePath, file);
637
+ const destPath = path6.join(rulesDir, validation.sanitized);
568
638
  if (!isPathWithinDirectory(destPath, rulesDir)) {
569
639
  logger.warn(`Blocked path traversal attempt: ${file}`);
570
640
  continue;
571
641
  }
572
- 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);
573
648
  filesWritten.push(destPath);
574
649
  }
575
650
  const metadataPath2 = await writePackageMetadata(rulesDir, manifest);
@@ -579,14 +654,14 @@ var RulesHandler = class {
579
654
  }
580
655
  const rulesContent = this.getRulesContent(manifest);
581
656
  if (!rulesContent) return filesWritten;
582
- const rulesPath = path5.join(rulesDir, "RULES.md");
657
+ const rulesPath = path6.join(rulesDir, "RULES.md");
583
658
  const content = `# ${manifest.name}
584
659
 
585
660
  ${manifest.description}
586
661
 
587
662
  ${rulesContent.trim()}
588
663
  `;
589
- await fs2.writeFile(rulesPath, content, "utf-8");
664
+ await fs3.writeFile(rulesPath, content, "utf-8");
590
665
  filesWritten.push(rulesPath);
591
666
  const metadataPath = await writePackageMetadata(rulesDir, manifest);
592
667
  filesWritten.push(metadataPath);
@@ -605,9 +680,9 @@ ${rulesContent.trim()}
605
680
  const filesRemoved = [];
606
681
  const folderName = sanitizeFolderName(packageName);
607
682
  const rulesBaseDir = getRulesPath("claude-code");
608
- const rulesPath = path5.join(rulesBaseDir, folderName);
609
- if (await fs2.pathExists(rulesPath)) {
610
- await fs2.remove(rulesPath);
683
+ const rulesPath = path6.join(rulesBaseDir, folderName);
684
+ if (await fs3.pathExists(rulesPath)) {
685
+ await fs3.remove(rulesPath);
611
686
  filesRemoved.push(rulesPath);
612
687
  }
613
688
  return filesRemoved;
@@ -631,29 +706,8 @@ ${rulesContent.trim()}
631
706
  };
632
707
 
633
708
  // src/adapters/handlers/skill-handler.ts
634
- import fs3 from "fs-extra";
635
- import path6 from "path";
636
- async function writePackageMetadata2(packageDir, manifest) {
637
- const metadata = {
638
- name: manifest.name,
639
- // e.g., "@cpm/commit-skill"
640
- version: manifest.version,
641
- // e.g., "1.0.0"
642
- type: manifest.type,
643
- // e.g., "skill"
644
- installedAt: (/* @__PURE__ */ new Date()).toISOString()
645
- // ISO timestamp for when it was installed
646
- };
647
- const metadataPath = path6.join(packageDir, ".cpm.json");
648
- try {
649
- await fs3.writeJson(metadataPath, metadata, { spaces: 2 });
650
- } catch (error) {
651
- logger.warn(
652
- `Could not write metadata: ${error instanceof Error ? error.message : "Unknown error"}`
653
- );
654
- }
655
- return metadataPath;
656
- }
709
+ import fs4 from "fs-extra";
710
+ import path7 from "path";
657
711
  function formatSkillMd(manifest) {
658
712
  const skill = manifest.skill;
659
713
  const content = manifest.universal?.prompt || manifest.universal?.rules || "";
@@ -696,10 +750,10 @@ var SkillHandler = class {
696
750
  const filesWritten = [];
697
751
  const skillsDir = getSkillsPath();
698
752
  const folderName = sanitizeFolderName(manifest.name);
699
- const skillDir = path6.join(skillsDir, folderName);
700
- await fs3.ensureDir(skillDir);
701
- if (context.packagePath && await fs3.pathExists(context.packagePath)) {
702
- const files = await fs3.readdir(context.packagePath);
753
+ const skillDir = path7.join(skillsDir, folderName);
754
+ await fs4.ensureDir(skillDir);
755
+ if (context.packagePath && await fs4.pathExists(context.packagePath)) {
756
+ const files = await fs4.readdir(context.packagePath);
703
757
  const contentFiles = files.filter(
704
758
  (f) => f.endsWith(".md") && f.toLowerCase() !== "cpm.yaml"
705
759
  );
@@ -710,40 +764,45 @@ var SkillHandler = class {
710
764
  logger.warn(`Skipping unsafe file: ${file} (${validation.error})`);
711
765
  continue;
712
766
  }
713
- const srcPath = path6.join(context.packagePath, file);
714
- const destPath = path6.join(skillDir, validation.sanitized);
767
+ const srcPath = path7.join(context.packagePath, file);
768
+ const destPath = path7.join(skillDir, validation.sanitized);
715
769
  if (!isPathWithinDirectory(destPath, skillDir)) {
716
770
  logger.warn(`Blocked path traversal attempt: ${file}`);
717
771
  continue;
718
772
  }
719
- 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);
720
779
  filesWritten.push(destPath);
721
780
  }
722
- const metadataPath = await writePackageMetadata2(skillDir, manifest);
781
+ const metadataPath = await writePackageMetadata(skillDir, manifest);
723
782
  filesWritten.push(metadataPath);
724
783
  return filesWritten;
725
784
  }
726
785
  }
727
786
  if (isSkillManifest(manifest)) {
728
787
  const skillContent = formatSkillMd(manifest);
729
- const skillPath = path6.join(skillDir, "SKILL.md");
730
- await fs3.writeFile(skillPath, skillContent, "utf-8");
788
+ const skillPath = path7.join(skillDir, "SKILL.md");
789
+ await fs4.writeFile(skillPath, skillContent, "utf-8");
731
790
  filesWritten.push(skillPath);
732
- const metadataPath = await writePackageMetadata2(skillDir, manifest);
791
+ const metadataPath = await writePackageMetadata(skillDir, manifest);
733
792
  filesWritten.push(metadataPath);
734
793
  } else {
735
794
  const content = this.getUniversalContent(manifest);
736
795
  if (content) {
737
- const skillPath = path6.join(skillDir, "SKILL.md");
796
+ const skillPath = path7.join(skillDir, "SKILL.md");
738
797
  const skillContent = `# ${manifest.name}
739
798
 
740
799
  ${manifest.description}
741
800
 
742
801
  ${content.trim()}
743
802
  `;
744
- await fs3.writeFile(skillPath, skillContent, "utf-8");
803
+ await fs4.writeFile(skillPath, skillContent, "utf-8");
745
804
  filesWritten.push(skillPath);
746
- const metadataPath = await writePackageMetadata2(skillDir, manifest);
805
+ const metadataPath = await writePackageMetadata(skillDir, manifest);
747
806
  filesWritten.push(metadataPath);
748
807
  }
749
808
  }
@@ -762,9 +821,9 @@ ${content.trim()}
762
821
  const filesRemoved = [];
763
822
  const folderName = sanitizeFolderName(packageName);
764
823
  const skillsDir = getSkillsPath();
765
- const skillPath = path6.join(skillsDir, folderName);
766
- if (await fs3.pathExists(skillPath)) {
767
- await fs3.remove(skillPath);
824
+ const skillPath = path7.join(skillsDir, folderName);
825
+ if (await fs4.pathExists(skillPath)) {
826
+ await fs4.remove(skillPath);
768
827
  filesRemoved.push(skillPath);
769
828
  }
770
829
  return filesRemoved;
@@ -787,28 +846,66 @@ ${content.trim()}
787
846
  };
788
847
 
789
848
  // src/adapters/handlers/mcp-handler.ts
790
- import fs4 from "fs-extra";
791
- import path7 from "path";
792
- var McpHandler = class {
793
- /**
794
- * Identifies this handler as handling "mcp" type packages.
795
- * The registry uses this to route MCP packages to this handler.
796
- */
849
+ import path9 from "path";
850
+
851
+ // src/adapters/handlers/base-mcp-handler.ts
852
+ import fs6 from "fs-extra";
853
+ import path8 from "path";
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 {
797
908
  packageType = "mcp";
798
- /**
799
- * Install an MCP package.
800
- *
801
- * The installation process:
802
- * 1. Validate the MCP configuration for security
803
- * 2. Read the existing ~/.claude.json configuration
804
- * 3. Add the new MCP server to the mcpServers section
805
- * 4. Write the updated configuration back
806
- *
807
- * @param manifest - The package manifest with MCP configuration
808
- * @param _context - Install context (not used for MCP, but required by interface)
809
- * @returns Array containing the path to the modified config file
810
- * @throws Error if MCP configuration fails security validation
811
- */
812
909
  async install(manifest, _context) {
813
910
  const filesWritten = [];
814
911
  if (!isMcpManifest(manifest)) {
@@ -818,78 +915,68 @@ var McpHandler = class {
818
915
  if (!mcpValidation.valid) {
819
916
  throw new Error(`MCP security validation failed: ${mcpValidation.error}`);
820
917
  }
821
- const claudeHome = getClaudeHome();
822
- const mcpConfigPath = path7.join(path7.dirname(claudeHome), ".claude.json");
823
- let existingConfig = {};
824
- if (await fs4.pathExists(mcpConfigPath)) {
825
- try {
826
- existingConfig = await fs4.readJson(mcpConfigPath);
827
- } catch {
828
- const backupPath = `${mcpConfigPath}.backup.${Date.now()}`;
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)) {
829
923
  try {
830
- await fs4.copy(mcpConfigPath, backupPath);
831
- logger.warn(
832
- `Could not parse ${mcpConfigPath}, backup saved to ${backupPath}`
833
- );
924
+ existingConfig = await fs6.readJson(mcpConfigPath);
834
925
  } catch {
835
- logger.warn(`Could not parse ${mcpConfigPath}, creating new config`);
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 = {};
836
938
  }
837
- existingConfig = {};
838
939
  }
839
- }
840
- const sanitizedName = sanitizeFolderName(manifest.name);
841
- const existingMcpServers = existingConfig.mcpServers || {};
842
- const updatedConfig = {
843
- ...existingConfig,
844
- // Preserve all other config settings
845
- mcpServers: {
846
- ...existingMcpServers,
847
- // Preserve other MCP servers
848
- [sanitizedName]: {
849
- // Add/update this package's MCP server
850
- command: manifest.mcp.command,
851
- // e.g., "npx"
852
- args: manifest.mcp.args,
853
- // e.g., ["-y", "@supabase/mcp"]
854
- env: manifest.mcp.env
855
- // e.g., { "SUPABASE_URL": "..." }
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
+ }
856
951
  }
857
- }
858
- };
859
- await fs4.writeJson(mcpConfigPath, updatedConfig, { spaces: 2 });
860
- filesWritten.push(mcpConfigPath);
952
+ };
953
+ await fs6.writeJson(mcpConfigPath, updatedConfig, { spaces: 2 });
954
+ filesWritten.push(mcpConfigPath);
955
+ });
861
956
  return filesWritten;
862
957
  }
863
- /**
864
- * Uninstall an MCP package.
865
- *
866
- * This removes the MCP server entry from ~/.claude.json
867
- *
868
- * @param packageName - The name of the package to remove
869
- * @param _context - Uninstall context (not used for MCP, but required by interface)
870
- * @returns Array containing the path to the modified config file
871
- */
872
958
  async uninstall(packageName, _context) {
873
959
  const filesWritten = [];
874
960
  const folderName = sanitizeFolderName(packageName);
875
- const claudeHome = getClaudeHome();
876
- const mcpConfigPath = path7.join(path7.dirname(claudeHome), ".claude.json");
877
- if (!await fs4.pathExists(mcpConfigPath)) {
961
+ const mcpConfigPath = this.getConfigPath();
962
+ if (!await fs6.pathExists(mcpConfigPath)) {
878
963
  return filesWritten;
879
964
  }
880
965
  try {
881
- const config = await fs4.readJson(mcpConfigPath);
882
- const mcpServers = config.mcpServers;
883
- if (!mcpServers || !mcpServers[folderName]) {
884
- return filesWritten;
885
- }
886
- const { [folderName]: _removed, ...remainingServers } = mcpServers;
887
- const updatedConfig = {
888
- ...config,
889
- mcpServers: remainingServers
890
- };
891
- await fs4.writeJson(mcpConfigPath, updatedConfig, { spaces: 2 });
892
- 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
+ });
893
980
  } catch (error) {
894
981
  logger.warn(
895
982
  `Could not update MCP config: ${error instanceof Error ? error.message : "Unknown error"}`
@@ -899,6 +986,14 @@ var McpHandler = class {
899
986
  }
900
987
  };
901
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
+
902
997
  // src/adapters/handlers/index.ts
903
998
  function initializeHandlers() {
904
999
  handlerRegistry.register(new RulesHandler());
@@ -908,8 +1003,6 @@ function initializeHandlers() {
908
1003
  initializeHandlers();
909
1004
 
910
1005
  // src/adapters/claude-code.ts
911
- import fs5 from "fs-extra";
912
- import path8 from "path";
913
1006
  var ClaudeCodeAdapter = class extends PlatformAdapter {
914
1007
  platform = "claude-code";
915
1008
  displayName = "Claude Code";
@@ -938,25 +1031,14 @@ var ClaudeCodeAdapter = class extends PlatformAdapter {
938
1031
  }
939
1032
  async uninstall(packageName, projectPath) {
940
1033
  const filesWritten = [];
941
- const folderName = sanitizeFolderName(packageName);
942
1034
  const context = { projectPath };
943
1035
  try {
944
- const rulesBaseDir = getRulesPath("claude-code");
945
- const rulesPath = path8.join(rulesBaseDir, folderName);
946
- if (await fs5.pathExists(rulesPath)) {
947
- await fs5.remove(rulesPath);
948
- filesWritten.push(rulesPath);
949
- }
950
- const skillsDir = getSkillsPath();
951
- const skillPath = path8.join(skillsDir, folderName);
952
- if (await fs5.pathExists(skillPath)) {
953
- await fs5.remove(skillPath);
954
- filesWritten.push(skillPath);
955
- }
956
- if (handlerRegistry.hasHandler("mcp")) {
957
- const mcpHandler = handlerRegistry.getHandler("mcp");
958
- const mcpFiles = await mcpHandler.uninstall(packageName, context);
959
- filesWritten.push(...mcpFiles);
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
+ }
960
1042
  }
961
1043
  return {
962
1044
  success: true,
@@ -999,22 +1081,229 @@ var ClaudeCodeAdapter = class extends PlatformAdapter {
999
1081
  }
1000
1082
  };
1001
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;
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 = [];
1206
+ try {
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
+ };
1217
+ }
1218
+ const result = await this.installByType(manifest, context);
1219
+ filesWritten.push(...result);
1220
+ return {
1221
+ success: true,
1222
+ platform: "cursor",
1223
+ filesWritten
1224
+ };
1225
+ } catch (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
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 [];
1270
+ }
1271
+ }
1272
+ };
1273
+
1002
1274
  // src/adapters/index.ts
1003
1275
  var adapters = {
1004
- "claude-code": new ClaudeCodeAdapter()
1276
+ "claude-code": new ClaudeCodeAdapter(),
1277
+ cursor: new CursorAdapter()
1005
1278
  };
1006
1279
  function getAdapter(platform) {
1007
- 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;
1008
1285
  }
1009
1286
 
1010
1287
  // src/utils/registry.ts
1011
1288
  import got from "got";
1012
- import fs6 from "fs-extra";
1013
- import path9 from "path";
1014
- import os2 from "os";
1015
- var DEFAULT_REGISTRY_URL = process.env.CPM_REGISTRY_URL || "https://raw.githubusercontent.com/cpmai-dev/packages/main/registry.json";
1016
- var CACHE_DIR = path9.join(os2.homedir(), ".cpm", "cache");
1017
- var CACHE_FILE = path9.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");
1018
1307
  var comparators = {
1019
1308
  downloads: (a, b) => (b.downloads ?? 0) - (a.downloads ?? 0),
1020
1309
  stars: (a, b) => (b.stars ?? 0) - (a.stars ?? 0),
@@ -1076,11 +1365,11 @@ var Registry = class {
1076
1365
  }
1077
1366
  async loadFileCache() {
1078
1367
  try {
1079
- await fs6.ensureDir(CACHE_DIR);
1080
- if (await fs6.pathExists(CACHE_FILE)) {
1081
- 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);
1082
1371
  if (Date.now() - stat.mtimeMs < LIMITS.CACHE_TTL_MS) {
1083
- return await fs6.readJson(CACHE_FILE);
1372
+ return await fs8.readJson(CACHE_FILE);
1084
1373
  }
1085
1374
  }
1086
1375
  } catch {
@@ -1089,8 +1378,8 @@ var Registry = class {
1089
1378
  }
1090
1379
  async saveFileCache(data) {
1091
1380
  try {
1092
- await fs6.ensureDir(CACHE_DIR);
1093
- await fs6.writeJson(CACHE_FILE, data, { spaces: 2 });
1381
+ await fs8.ensureDir(CACHE_DIR);
1382
+ await fs8.writeJson(CACHE_FILE, data, { spaces: 2 });
1094
1383
  } catch {
1095
1384
  }
1096
1385
  }
@@ -1113,8 +1402,8 @@ var Registry = class {
1113
1402
  return this.cache;
1114
1403
  }
1115
1404
  try {
1116
- if (await fs6.pathExists(CACHE_FILE)) {
1117
- const cached = await fs6.readJson(CACHE_FILE);
1405
+ if (await fs8.pathExists(CACHE_FILE)) {
1406
+ const cached = await fs8.readJson(CACHE_FILE);
1118
1407
  this.cache = cached;
1119
1408
  return cached;
1120
1409
  }
@@ -1154,9 +1443,9 @@ var Registry = class {
1154
1443
  var registry = new Registry();
1155
1444
 
1156
1445
  // src/utils/downloader.ts
1157
- import fs8 from "fs-extra";
1158
- import path11 from "path";
1159
- import os3 from "os";
1446
+ import fs10 from "fs-extra";
1447
+ import path13 from "path";
1448
+ import os4 from "os";
1160
1449
 
1161
1450
  // src/sources/manifest-resolver.ts
1162
1451
  var ManifestResolver = class {
@@ -1298,8 +1587,8 @@ var RepositorySource = class {
1298
1587
 
1299
1588
  // src/sources/tarball-source.ts
1300
1589
  import got3 from "got";
1301
- import fs7 from "fs-extra";
1302
- import path10 from "path";
1590
+ import fs9 from "fs-extra";
1591
+ import path12 from "path";
1303
1592
  import * as tar from "tar";
1304
1593
  import yaml2 from "yaml";
1305
1594
  var TarballSource = class {
@@ -1351,12 +1640,12 @@ var TarballSource = class {
1351
1640
  responseType: "buffer"
1352
1641
  // Get raw binary data
1353
1642
  });
1354
- const tarballPath = path10.join(context.tempDir, "package.tar.gz");
1355
- await fs7.writeFile(tarballPath, response.body);
1643
+ const tarballPath = path12.join(context.tempDir, "package.tar.gz");
1644
+ await fs9.writeFile(tarballPath, response.body);
1356
1645
  await this.extractTarball(tarballPath, context.tempDir);
1357
- const manifestPath = path10.join(context.tempDir, "cpm.yaml");
1358
- if (await fs7.pathExists(manifestPath)) {
1359
- 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");
1360
1649
  return yaml2.parse(content);
1361
1650
  }
1362
1651
  return null;
@@ -1378,8 +1667,8 @@ var TarballSource = class {
1378
1667
  * @param destDir - Directory to extract to
1379
1668
  */
1380
1669
  async extractTarball(tarballPath, destDir) {
1381
- await fs7.ensureDir(destDir);
1382
- const resolvedDestDir = path10.resolve(destDir);
1670
+ await fs9.ensureDir(destDir);
1671
+ const resolvedDestDir = path12.resolve(destDir);
1383
1672
  await tar.extract({
1384
1673
  file: tarballPath,
1385
1674
  // The tarball file to extract
@@ -1389,8 +1678,8 @@ var TarballSource = class {
1389
1678
  // Remove the top-level directory (e.g., "package-1.0.0/")
1390
1679
  // Security filter: check each entry before extracting
1391
1680
  filter: (entryPath) => {
1392
- const resolvedPath = path10.resolve(destDir, entryPath);
1393
- const isWithinDest = resolvedPath.startsWith(resolvedDestDir + path10.sep) || resolvedPath === resolvedDestDir;
1681
+ const resolvedPath = path12.resolve(destDir, entryPath);
1682
+ const isWithinDest = resolvedPath.startsWith(resolvedDestDir + path12.sep) || resolvedPath === resolvedDestDir;
1394
1683
  if (!isWithinDest) {
1395
1684
  logger.warn(`Blocked path traversal in tarball: ${entryPath}`);
1396
1685
  return false;
@@ -2071,15 +2360,15 @@ function createDefaultResolver() {
2071
2360
  var defaultResolver = createDefaultResolver();
2072
2361
 
2073
2362
  // src/utils/downloader.ts
2074
- var TEMP_DIR = path11.join(os3.tmpdir(), "cpm-downloads");
2363
+ var TEMP_DIR = path13.join(os4.tmpdir(), "cpm-downloads");
2075
2364
  async function downloadPackage(pkg) {
2076
2365
  try {
2077
- await fs8.ensureDir(TEMP_DIR);
2078
- const packageTempDir = path11.join(
2366
+ await fs10.ensureDir(TEMP_DIR);
2367
+ const packageTempDir = path13.join(
2079
2368
  TEMP_DIR,
2080
2369
  `${pkg.name.replace(/[@/]/g, "_")}-${Date.now()}`
2081
2370
  );
2082
- await fs8.ensureDir(packageTempDir);
2371
+ await fs10.ensureDir(packageTempDir);
2083
2372
  const manifest = await defaultResolver.resolve(pkg, {
2084
2373
  tempDir: packageTempDir
2085
2374
  });
@@ -2094,7 +2383,7 @@ async function downloadPackage(pkg) {
2094
2383
  async function cleanupTempDir(tempDir) {
2095
2384
  try {
2096
2385
  if (tempDir.startsWith(TEMP_DIR)) {
2097
- await fs8.remove(tempDir);
2386
+ await fs10.remove(tempDir);
2098
2387
  }
2099
2388
  } catch {
2100
2389
  }
@@ -2221,7 +2510,7 @@ var SEMANTIC_COLORS = {
2221
2510
 
2222
2511
  // src/commands/ui/formatters.ts
2223
2512
  import chalk2 from "chalk";
2224
- import path12 from "path";
2513
+ import path14 from "path";
2225
2514
  function formatNumber(num) {
2226
2515
  if (num >= 1e6) {
2227
2516
  return `${(num / 1e6).toFixed(1)}M`;
@@ -2232,7 +2521,7 @@ function formatNumber(num) {
2232
2521
  return num.toString();
2233
2522
  }
2234
2523
  function formatPath(filePath) {
2235
- const relativePath = path12.relative(process.cwd(), filePath);
2524
+ const relativePath = path14.relative(process.cwd(), filePath);
2236
2525
  if (relativePath.startsWith("..")) {
2237
2526
  return filePath;
2238
2527
  }
@@ -2490,7 +2779,11 @@ async function installCommand(packageName, rawOptions) {
2490
2779
  tempDir = downloadResult.tempDir;
2491
2780
  const targetPlatforms = [options.platform];
2492
2781
  spinner.update(`Installing to ${targetPlatforms.join(", ")}...`);
2493
- await ensureClaudeDirs();
2782
+ if (options.platform === "cursor") {
2783
+ await ensureCursorDirs(process.cwd());
2784
+ } else {
2785
+ await ensureClaudeDirs();
2786
+ }
2494
2787
  const results = await installToPlatforms(
2495
2788
  manifest,
2496
2789
  tempDir,
@@ -2566,28 +2859,28 @@ async function searchCommand(query, rawOptions) {
2566
2859
 
2567
2860
  // src/commands/list.ts
2568
2861
  import chalk4 from "chalk";
2569
- import fs9 from "fs-extra";
2570
- import path13 from "path";
2571
- import os4 from "os";
2862
+ import fs11 from "fs-extra";
2863
+ import path15 from "path";
2864
+ import os5 from "os";
2572
2865
  async function readPackageMetadata(packageDir) {
2573
- const metadataPath = path13.join(packageDir, ".cpm.json");
2866
+ const metadataPath = path15.join(packageDir, ".cpm.json");
2574
2867
  try {
2575
- if (await fs9.pathExists(metadataPath)) {
2576
- return await fs9.readJson(metadataPath);
2868
+ if (await fs11.pathExists(metadataPath)) {
2869
+ return await fs11.readJson(metadataPath);
2577
2870
  }
2578
2871
  } catch {
2579
2872
  }
2580
2873
  return null;
2581
2874
  }
2582
- async function scanDirectory(dir, type) {
2875
+ async function scanDirectory(dir, type, platform) {
2583
2876
  const items = [];
2584
- if (!await fs9.pathExists(dir)) {
2877
+ if (!await fs11.pathExists(dir)) {
2585
2878
  return items;
2586
2879
  }
2587
- const entries = await fs9.readdir(dir);
2880
+ const entries = await fs11.readdir(dir);
2588
2881
  for (const entry of entries) {
2589
- const entryPath = path13.join(dir, entry);
2590
- const stat = await fs9.stat(entryPath);
2882
+ const entryPath = path15.join(dir, entry);
2883
+ const stat = await fs11.stat(entryPath);
2591
2884
  if (stat.isDirectory()) {
2592
2885
  const metadata = await readPackageMetadata(entryPath);
2593
2886
  items.push({
@@ -2595,27 +2888,28 @@ async function scanDirectory(dir, type) {
2595
2888
  folderName: entry,
2596
2889
  type,
2597
2890
  version: metadata?.version,
2598
- path: entryPath
2891
+ path: entryPath,
2892
+ platform
2599
2893
  });
2600
2894
  }
2601
2895
  }
2602
2896
  return items;
2603
2897
  }
2604
- async function scanMcpServers() {
2898
+ async function scanMcpServersFromConfig(configPath, platform) {
2605
2899
  const items = [];
2606
- const configPath = path13.join(os4.homedir(), ".claude.json");
2607
- if (!await fs9.pathExists(configPath)) {
2900
+ if (!await fs11.pathExists(configPath)) {
2608
2901
  return items;
2609
2902
  }
2610
2903
  try {
2611
- const config = await fs9.readJson(configPath);
2904
+ const config = await fs11.readJson(configPath);
2612
2905
  const mcpServers = config.mcpServers || {};
2613
2906
  for (const name of Object.keys(mcpServers)) {
2614
2907
  items.push({
2615
2908
  name,
2616
2909
  folderName: name,
2617
2910
  type: "mcp",
2618
- path: configPath
2911
+ path: configPath,
2912
+ platform
2619
2913
  });
2620
2914
  }
2621
2915
  } catch {
@@ -2623,13 +2917,24 @@ async function scanMcpServers() {
2623
2917
  return items;
2624
2918
  }
2625
2919
  async function scanInstalledPackages() {
2626
- const claudeHome = path13.join(os4.homedir(), ".claude");
2627
- const [rules, skills, mcp] = await Promise.all([
2628
- scanDirectory(path13.join(claudeHome, "rules"), "rules"),
2629
- scanDirectory(path13.join(claudeHome, "skills"), "skill"),
2630
- 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")
2631
2930
  ]);
2632
- return [...rules, ...skills, ...mcp];
2931
+ return [
2932
+ ...claudeRules,
2933
+ ...claudeSkills,
2934
+ ...claudeMcp,
2935
+ ...cursorRules,
2936
+ ...cursorMcp
2937
+ ];
2633
2938
  }
2634
2939
  function groupByType(packages) {
2635
2940
  return packages.reduce(
@@ -2642,8 +2947,9 @@ function groupByType(packages) {
2642
2947
  }
2643
2948
  function displayPackage(pkg) {
2644
2949
  const version = pkg.version ? SEMANTIC_COLORS.dim(` v${pkg.version}`) : "";
2950
+ const platform = pkg.platform ? SEMANTIC_COLORS.dim(` [${pkg.platform}]`) : "";
2645
2951
  logger.log(
2646
- ` ${SEMANTIC_COLORS.success("\u25C9")} ${chalk4.bold(pkg.name)}${version}`
2952
+ ` ${SEMANTIC_COLORS.success("\u25C9")} ${chalk4.bold(pkg.name)}${version}${platform}`
2647
2953
  );
2648
2954
  }
2649
2955
  function displayByType(byType) {
@@ -2706,15 +3012,22 @@ async function uninstallCommand(packageName) {
2706
3012
  const spinner = createSpinner(spinnerText("Uninstalling", packageName));
2707
3013
  try {
2708
3014
  const folderName = extractFolderName(packageName);
2709
- const adapter = getAdapter("claude-code");
2710
- const result = await adapter.uninstall(folderName, process.cwd());
2711
- 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) {
2712
3027
  spinner.succeed(successText("Uninstalled", packageName));
2713
- displayRemovedFiles(result.filesWritten);
2714
- } else if (result.success) {
2715
- spinner.warn(`Package ${packageName} was not found`);
3028
+ displayRemovedFiles(allFilesRemoved);
2716
3029
  } else {
2717
- spinner.fail(failText("Failed to uninstall", packageName, result.error));
3030
+ spinner.warn(`Package ${packageName} was not found`);
2718
3031
  }
2719
3032
  } catch (error) {
2720
3033
  spinner.fail(failText("Failed to uninstall", packageName));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cpmai/cli",
3
- "version": "0.3.0-beta.1",
3
+ "version": "0.3.0-beta.2",
4
4
  "description": "CPM CLI - cpm-ai.dev",
5
5
  "keywords": [
6
6
  "claude-code",